(转载)使用 COM 符号引擎辅助调试

使用 COM 符号引擎辅助调试

发布日期: 4/1/2004 | 更新日期: 4/1/2004

John Robbins

下载本文代码:Bugslayer0800.exe (35KB)

在2000 年 4 月的那期的杂志中,我谈到我曾想构建 DBGHELP.DLL 符号引擎的一个 COM 包装。 我还承认当时遇到了一些问题,因为自己对数据库的了解还不足以开发 OLE DB 提供程序包装,而这些数据库知识是本应该具备的。 有意思的是,很多人都评论说,我不怕承认自己的无知真是太厉害了。 在和各种类型的开发人员进行了一些讨论后,我很惊讶地了解到并不是每个人都能对自己的弱点开诚布公。 我发现,一般而言开发人员都认为承认自己有什么不知道是一种软弱的表现。 对于团队开发而言,这可能会成为问题。

真正让我惊讶的是,许多开发人员在安排开发时间时都有意地尽量不安排提高速度所需要的学习时间。 因为我的专业主要就是寻找程序错误上,开发人员不为学习安排时间这一事实,在我看来,显然是亮出了一面大大的警告旗。 如果开发人员只是想学习正在开发的项目所需要的知识,就会存在两个巨大的漏洞,从中会溜过大量错误。 第一个漏洞是,因为是在未知领域中工作,设计的质量将会差很多。 第二个漏洞是,虽然这样也可以开发出差不多能够运行的程序,但是,由于您必须不停地进行越来越多的最新修补才能使整个程序正常运行,您最终便会发现自己陷入了一个“功能蔓延的泥潭”。 只要有一点行业经验的人就会知道,功能蔓延是错误的最大肇因。 这两个产生错误的漏洞,恰恰会导使人们开始赶不上自己的进度!

知之为知之,不知为不知,是绝对正确的。 今天的计算机程序设计行业和医药行业有些相似,因为每个业内人士都必须成为专家才能生存。 而技术又是如此精深和复杂,以至于要想表现得令人满意,必须专注于几个领域,而不考虑其他领域。 问题在于,不是所有程序员的观念都能与时俱进。 一个健康的团队应该对每个人的状况都有很好的认识,这样才能正确地使用培训费用来提高团队实力。 此外,在项目期间为学习安排时间,就能使设计更优秀,功能蔓延更少,最终更接近预定的发布日期。

好啦,感谢给我机会阐释自己的开发哲学,下面我想转到 COM 符号引擎的实现上来。 当我最开始讨论这些相关问题时,我曾问过,是否有人能提供一些想法,谈一谈应该如何着手解决它们。 撰写这个专栏最大的好处在于,我能够虚拟地与全世界的开发人员会面,他们有极其丰富的经验,对于解决各种开发问题有着一些真正不同凡响的想法。 因此,COM 符号引擎使用了一些有趣的技术,当您必须用崭新的 COM 包装对现有接口进行包装时,可以考虑采用这些技术。 在转到实现细节之前,我要提一下实现 COM 符号引擎时最开始比较关心的一些问题。

*

问题回顾

乍看起来,DBGHELP.DLL 符号引擎之上的 COM 包装中,大多数代码都很琐碎。 许多函数的功能不过是直接从 API 函数映射到一个方法。 当然,DBGHELP.DLL 符号引擎的第一个大缺点就在于,它全部是基于 ANSI 字符串的,因此 COM 包装必须进行大量 ANSI 到 BSTR 的转换。 DBGHELP.DLL 符号引擎中真正的大问题与符号和模块枚举有关,因为它们使用了回调函数。

以前基于 Windows 的开发通常是用 C 语言完成,枚举的回调函数在要隐藏某个特定数据结构的实现时是一种可采用的优秀的技术。 枚举函数可以不公开数据结构,但是需要有一个函数指针,在遍历数据结构时供内部代码调用。 回调方式的缺点在于,COM 中没有函数指针这样的东西。 因为 SymEnumerateModules 和 SymEnumerateSymbols 都是用回调函数实现的,所以必须给出一个解决方案。

COM 包装的设计目标之一是遵守标准的 IEnum* 接口。 虽然非标准枚举方案也能工作(而且也不应该有什么人会对此抱怨什么),但是我想让整个接口的工作方式和其他接口一样。 在开始考虑遵守 IEnum* 接口将采用的方法时,我想到了两种可能的解决方案。 第一种是保存上一个已经枚举的符号,当要求下一个符号时,就调用 C 接口继续枚举到下一个。 虽然这能够运行(因为大多数符号表都有成千上万的项),但是肯定极慢。 另一种解决方案是考虑自己实现枚举,存储结果供以后使用。 那意味着实际上要在内存中复制整个符号数据,因此会使 COM 包装的内存需求翻上两倍都还不止。

正如我在 2000 年 4 月的专栏中所说,两种解决方案看上去都没有太大的吸引力,所以我采取了专栏作家的老办法: 求助于优秀的读者们。 我先介绍对象模型和一些比较容易的实现部分,然后再讨论一些聪明的读者提出的几个优秀的解决方案。

使用和容易实现的部分

COM 符号引擎的使用应该是很简单的。 方法的名称保持与 C 语言的 DBGHELP.DLL 符号引擎的名称相同。 这样,那些对现有符号引擎有经验的人学习起来就比较容易。而那些没有用过符号引擎的人,MSDN Library 中有学会如何使用所需的全部文档。 至于使用 ComSymbolEngine 对象的基于 Visual Basic 的客户端示例,可以参见本月源代码中包含的 VBTest 应用程序。 图 1 显示了 ComSymbolEngine 对象的简单对象模型。

图 1 ComSymbolEngine

图 1 ComSymbolEngine

因为 DBGHELP.DLL 符号引擎中的几个结构是可扩展的(比如 IMAGEHLP_SYMBOL),我们使用老的可以信赖的备用技术 — 活动模板库 (ATL)。 主接口 ISymbolEngine 中大多数方法都是实际函数的简单包装。 图 2 中能看到一些示例,比如 ISymbolEngine::SymInitialize 和 ISymbolEngine::SymLoadModule。 可以看到,大多数工作都与错误检查和字符串转换有关。

因为 DBGHELP.DLL 符号引擎依赖几个结构来返回数据,所以这些结构都能自动转换为各自的接口。 例如,ISymbolInfo 接口只是 IMAGEHLP_SYMBOL 结构的另一个名称。 IMAGEHLP_LINE 结构映射 ILineInfo 接口,而 IMAGEHLP_MODULE 结构映射 IModuleInfo 接口。 到讨论枚举的时候会看到,IModuleInfo 接口并不是简单的结构映射。

主接口 ISymbolEngine 还会激发一些事件。 DBGHELP.DLL 符号引擎提供了一种机制,在某个符号引擎信息通过 SymRegisterCallback 通知发送时接收通知。 因为通知的概念直接映射到 COM 事件,它的实现没有什么太激动人心的地方。 使用 ComSymbolEngine 对象时,可能需要考虑处理这些事件,以查看符号引擎是否有任何问题。

处理枚举

现在让我们转到有趣的部分: 模块和符号枚举。 如果已经见过 ComSymbolEngine 对象,就能看出我实际上是实现了两种不同的模块和符号枚举技术。 我这么做是想说明一点:两种技术对于解决在问题一节中提出的问题而言,都是完全有效的解决方案。

Abu Wawda 和 Chris Sells 各自独立地提出了第一种技术。 对于我严格遵守仅支持 IEnum* 式枚举这一限制,他们认为我追求完美有点太过头了。 他们提出的解决方案就是让 SymEnumerateModules和SymEnumerateSymbols 的回调函数对每个枚举都激发事件。 使用连接点完成这项任务很快,非常易于实现,而且对所有使用 ComSymbolEngine 对象的人来说也很直观。

将连接点用于枚举的一个小问题,是需要为开发人员提供停止枚举的手段。 可以从各自的事件处理程序调用 StopModuleEnumeration 和 StopSymbolEnumeration 方法来停止枚举。 图 3 中显示了 ISymbolEngine:: SymEnumerateSymbols 和 ISymbolEngine:: StopSymbolEnumeration 方法以及支持性的 ComSymbolEngineEnumSyms 回调函数。

第二种处理枚举的方法来自 Chris Treichel 富于才气的建议。 当我收到 Chris 的邮件时,不由得心生崇敬,深受感动。 我一直在寻找一种控制回调执行过程的办法,而纤程正是对此的完美解决之道。 Microsoft 最早在操作系统引入纤程,是为了帮助开发人员将代码从 Unix 移植到 Windows。 似乎许多 Unix 产品都实现了自己的线程处理机制。 因为纤程能够用来精确地控制执行,它对于我的 ComSymbolEngine 对象难题是一种绝好的解决方案。

为了使运行在纤程中的符号和模块枚举代码能够重用,我将它们全部放在称为 FiberCode.h 和 FiberCode.cpp 的文件中。图 4 显示了符号枚举部分代码。 第一个调用的函数是 InitializeSymbolEnumeration,它用传递给纤程函数的数据创建一个小的数据结构。 请注意代码没有显示出我已经通过调用 ConvertThreadToFiber,将控制的主线程转换为纤程。 在填充好共享数据结构之后,我创建了符号枚举纤程,它在调用 SwitchToFiber 之后才会执行。

停止符号枚举非常简单: 只需调用 EndSymbolEnumeration 即可。 唯一需要做的就是删除符号枚举纤程。 对符号进行枚举很容易,这一点可以在 GetNextSymbolData 中看到。 在该函数中,第一步是切换到枚举纤程并让它执行一个枚举。 当枚举进行时,从共享数据结构复制出结果。 最后,如果枚举确实结束,就终止枚举纤程。

图 4 中显示的头两个函数只在符号枚举纤程上下文中执行。 SymbolEnumerationFiber 是实际的纤程函数本身,它会调用 SymEnumerateSymbols 函数。 因为您不能让纤程自己结束,所以它执行的最后一个操作就是在指示枚举结束以后切换回主纤程。 符号枚举回调的实现几乎同样容易。 在 SymSymEnum 中的每次迭代之后,函数将上下文切换回主纤程,这样就实现了受控的符号枚举。

有了简单的纤程处理后,就能用 IEnumVARIANT 派生的接口和标准 COM 集合类完整地支持模块和符号枚举了。 因为这里有趣的地方是核心的纤程实现,所以必须查看源代码才能获得完整的实现。 还可以检查 Visual Basic 示例 VBTest,确保所有枚举都完全符合 Visual Basic 中的 Item 选择和 for...each...next 语义。

要了解关于纤程的更多信息,请参考 MSJ 1996 年 11 月 Jeffrey Richter 的 Win32 Q+A 专栏。我想提到的最后一点,是在调试的时候特别有用的一个灵巧的 ATL 宏。 COM_INTERFACE_ENTRY_BREAK 宏在查询特定接口时都会触发一个断点。 只需把 COM map 宏中的 COM_INTERFACE_ENTRY_BREAK 宏放在要停止的接口中即可。 我发现,与其辛辛苦苦地从 ATL 源代码寻找合适的位置设置断点,不如使用 COM_INTERFACE_ENTRY_BREAK 来得容易得多。

结语

我知道有些读者等待 ComSymbolEngine 对象很长时间了,因为第一次做出这一承诺是几乎 18 个月之前的事情了。 对于正处在从旧系统向新的和已经得到改进的 COM 世界过渡中的读者,可能想了解转换 C 接口必经的辗转过程。 我希望这些努力能够为大家节省一些时间。

技巧

在炎热的夏日里,在您热得发晕的时候,如果您在坐着休息时想到了调试技巧,请记着通过电子邮件发送给我,我的邮件地址是:john@wintellect.com

技巧 35Slorakia 的 Joseph Sebestyen 写道: 在 MSJ 1998 年 6 月 一期中,您演示了如何在 Memory 窗口设置 ESP 寄存器,以查找堆栈中的函数参数。 为了在一个函数的头条指令的堆栈中查找参数,我在 Watch 窗口中使用了 ((int*)@ESP),10(其中 10 是我想看到的参数数量)。 文档尚未记载的 ,# 格式设置(这是我从您的另一个技巧中学到的)会自动扩展为一个数组视图。 当单步执行标准帧函数(那些使用 PUSH EBP 或 MOV EBP, ESP 作为 prolog 序列的函数)时,可以把 Watch 窗口表达式改为 ((int*)@ESP+1),10。

技巧 36 Jonathan M. Gilligan 博士写道: 我在工作中使用过许多这样的类,它们包含指向其他类或者指向同一个类其他对象的指针(例如树和相关数据结构)。 有时候在调试中,确定正在观察的是类的哪个实例可能非常困难,尤其在这个实例是要传递的函数参数或者别名时。

我借助在所有类上都添加“序列号”成员的方法,以辅助确定类似的东西。

			// class declaration
			Class Foo 
			{
			private:
				static unsigned s_uLastSerialNo;
				unsigned m_uSerialNo;
			...
			};

			// in the implementation
			unsigned Foo::s_uLastSerialNo = 0;

			Foo::Foo() : m_uSerialNo(++s_uLastSerialNo)
			{
				// initialization
			}
			

现在如果出现与某个特定对象相关的错误,我就可以用 TRACE 输出或者条件断点将注意力集中在会影响这个特定对象的各种东西上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值