定制调试诊断工具和实用程序
——摆脱DLL“地狱”(DLL Hell)的困扰(七)
原著:Christophe Nasarre
编译:NorthTibet
下载源代码:EscapeFromDLLHell.exe (588KB)
原文出处:Windows XP:Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities,Part 2
本文假设你熟悉 Win32,DLL
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(一)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(二)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(三)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(四)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(五)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(六)
Windows Loader 知道一切
除了 Win32 调试 API 之外,Windows 还提供另外一种很好的关于 DLL
加载地址冲突的信息源。那就是在注册表中设置的一些全局标志(或 GFlags):
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
从而改变 Windows 处理应用程序的方式。GFlags.exe (see Figure 8) 是一个微软调试工具之一,用它可以轻松更改上述的注册表项值。
Figure 8 全局标志
在 1999 九月的 “Under the Hood”专栏中, Matt Pietrek 解释了如何将 FLG_SHOW_LDR_SNAPS
与上述 GFlags 结合使用让 Windows Loader
产生一些有用的跟踪信息。如果你想捕获这些跟踪信息,你有两个选择:第一个是调试应用程序,然后象所做的 CApplicationDebugger
那样解释 OUTPUT_DEBUG_STRING_EVENT。另外一个方法比较容易:使用全局捕获工具。如果你想要生成自己的跟踪信息,使用
或《Inside Windows 2000, Third Edition》CD 中的 DbgView,这个工具还可以显示内核跟踪信息。
在 LoadLibrarySpy 工具中,启动被调试程序之前,与被调试程序对应的 GFlags 值会被 CApplicationDebugger
的 PreLoadingProcess 更新,其以前的值会被保存在 PostMortem 中,也就是说,因为使用“LDR”作为前缀,所以调试器从
Windows Loader 得到的专用输出信息很容易在 OnOutputDebugStringDebugEvent 中过滤。
这种 Loader 日志的一个主要优点是输出信息前都有一个 LDR:自动化 DLL 重定位信息。它解释哪个 DLL 与另外的 DLL
有地址冲突。这既是 CModuleListCtrl 获取 Reason 栏数据的方法。不幸的是,Windows 2000 Loader
好像抑制这种特定的输出信息。如果你过去习惯于通过加载某个进程来存取其资源,如 explorer.exe 的动画或图标,0x400000
加载地址通常已经被你的程序使用,Loader 会自动进行重定位。在这种特殊情况下,即便是在 Windows NT 4.0,它都不会发出 LDR:为动态加载进程自动化
DLL 重定向。
另外一个解决方案是枚举每个加载的 DLL 并与专用的地址空间区域(从 hModule 开始)进行比较,从而找到冲突者(实现细节参见 CLoadLibraryDebugger::OnLoadDLLDebugEvent)。加载器还提供另外一个带前缀“LDR:Loading
(DYNAMIC)”的有趣的信息,同时其后跟随模块的全路径名。当某个 DLL 被显示通过 LoadLibrary 加载时,似乎就是这种情况。
使用这些来自 Windows Loader 的线索,LoadLibrarySpy 根据加载状态为每个 DLL 提供了一个专门的图标,详情参见
带方形图标的 DLLs
是在进程初始化期间加载的,称为静态加载。带圆形图标的则是在进程初始化之后加载的,因此称为动态加载。图标的颜色预示着是否有加载地址冲突,红色表示有,蓝色表示没有。
那些带黑色背景与其它动态 DLLs 之间的区别很微妙:带黑色图标的 DLL 已被加载,要么是用 LoadLibrary
显式加载,要么是用其它类似 CoCreateInstance 的 API 函数加载。没有黑色背景图标的 DLL 已被加载,因为另外一个
DLL需要它。例如,在 Figure 4 中,BROWSEUI.dll 有一个黑色图标,因为它已被动态加载。而 SHDOCVW.dll
图标没有黑色背景,因为它已被 Windows 自动加载。理由很简单:BROWSEUI.dll 是静态链接到 SHDOCVW.dll
的,所以为了加载 BROWSEUI,Windows 也得加载 SHDOCVW。
另一种“盗取”信息的途径
在结束 Win32 调试 API 的讨论之前,我想用一点点时间讨论异常处理机制。当被调试程序中有异常发生时,调试器通过 EXCEPTION_DEBUG_EVENT
收到通知,并且 u.Exception.ExceptionRecord.ExceptionCode 域中会包含此异常编码。异常编码都分布在 WINNT.H
和 WINBASE.H 文件中,因此要获得一个全面而且清晰易读的异常编码清单并不是件容易的事。CApplicationDebugger 的 GetExceptionDescription
方法将这些异常编码转换成可读性更强的字符串。
另一个异常编码清单信息源是 Visual C++
本身。在调试应用程序时,“Debug”菜单中有一个“Exception”菜单项,它允许你选择调试器处理异常的方式,如 Figure 10 所示:
Figure 10 Exceptions Dialog
你可能会感到惊讶,在这里能发现没有定义过的异常编码。不用动手拷贝,从这个列表框“盗取”信息不是很好吗。这便是 WindowDump
的目的。它允许你用鼠标拾取某个窗口(通过其句柄值)并将信息 dump 到一个编辑框中。此外,它还能收集类信息和式样描述信息,如 Figure
11 所示。
Figure 11 WindowDump 中的异常编码
WindowDump 的背后并没有什么玄机。唯一有趣的地方是 Windows 通常允许 GetWindowText 和 WM_GETTEXT
操作不同的进程。但对于列表视图和树型视图这样的公用控件除外。Jeffrey Richter 在他的
Q&A Win32(MSJ September 1997)专栏中解释了如何 dump 另外一个进程中列表视图的内容,附带一个范例程序
LV2Clip。下面是一些 WindowDump 能盗取其内容的窗口类:Edit、ScrollBar、ListBox、ComboBox、ListView
和 TreeView。根据这些类的窗口内容,你还能得到
Figure 12 所列出的信息。
有关 WindowsDump 实现的最后一个重点是进程 ID。从窗口句柄入手,使用 GetWindowThreadProcessId
不难确定线程以及负责创建该线程的进程。如果你还想知道模块名,用 GetWindowModuleFileName
可能会碰壁。与在文档中给出的信息相反,这个 API 函数在 Windows NT、Windows 2000 或 Windows XP
下调用失败。你得钻研知识库的文章
Q228469 查明原因。
在这样情况下,你应该用 PSAPI 及其 GetModuleFileNameEx 函数。它以进程和 hModule
模块句柄为参数,返回对应的路径名。为了查出某一个进程的可执行文件路径名,hModule 应该为 0。不要使用
0x400000:某些进程被加载到不同的地址,如 winlogon 和 Task Manager 在 0x1000000,ntvdm 在
f000000 以及Microsoft Word 2000 在 0x30000000。
参考资料
The Win32 Debugging Application Programming Interface;
Spawn Console Processes with Redirected Standard Handles;
GetWindowModuleFileName & GetModuleFileName Work Only with the Calling Process
在后续文章中,我将介绍 CreateRemoteThread,命令行及其它。
(待续)
作者简介
Christophe Nasarre 是法国 Business Objects 公司的技术经理(technical
manager)。他在 Windows 平台上(3.0 以后的版本)编写了若干个低级工具。他的联系方式:cnasarre@montataire.net.
.
本文出自 MSDN Magazine 的
August 2002 期刊,可通过当地报摊获得,或者最好是
本文由 VCKBASE MTT 翻译