分享
 
 
 

定制调试诊断工具和实用程序——摆脱DLL“地狱”(DLL Hell)的困扰(六)

王朝vc·作者佚名  2006-05-23
窄屏简体版  字體: |||超大  

定制调试诊断工具和实用程序

——摆脱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)的困扰(五)

摘要

本文前面讨论了用几种不同的方法来获取进程及其相关 DLLs 的信息,例如通过 PSAPI、NTDLL 以及TOOLHELP32 库提供的

APIs,在这一部分,作者给出了几种获得系统级信息的非常规方法,你可以轻松将它们集成到自己的工具包中。本文范例包含三个实用工具:

LoadLibrarySpy,监视并扫描应用程序加载了哪些 DLLs;

WindowDump,获取任何窗口的的内容以及窗口的详细描述信息;

FileUsage,重定向控制台程序,揭示哪个进程正在使用打开的文件;

本文前面的部分讨论了如何用有着良好文档描述的 API 函数来获取运行进程列表以及它们加载的 DLLs

信息。接下来我将用不同的方法,或者说是非正式的方法来获取系统级信息,首先,我将深入分析 Win32 调试 API 以及 Windows

加载器(Windows Loader)提供的痕迹来揭示给定进程是如何加载 DLL 的。我将借助我的 CApplicationDebugger

可重用类,用几种不同的方法来分析 DLL 重定位的原因。

接着,我将生成两个工具。LoadLibrarySpy 扫描 DLL 重定位。WindowDump

窃取任何窗口的内容和详细描述信息。最后,在讨论进程环境块(PEB)内部结构之前,我会向你展示如何操纵控制台程序产生的输出以便摸索寻找一些未公开的信息。

回到 DLL Hell

前面我们已经看到获取所有静态或动态加载的 DLLs 列表是很容易的事情。但是对动态加载的DLL而言,情况比想象的稍微复杂一些。例如,DllSpy 和 ProcessSpy 两个工具依据某个时间点获得的快照。因此,有可能出现来不及扫描某个被快速加载和卸载的DLL。Win32 调试 API 提供了对这个问题的解决办法:在调试程序时,

这些 API 可以对被调试程序加载和卸载的任何DLL了如指掌。

要实现我的意图,并不需要一个功能完整,名副其实的调试器,但我必须侦测到新 DLL 何时被加载到进程地址空间。因此,我将讨论 Win32

调试 API 的基本知识以及它们在 Windows NT、Windows 2000 和 Windows XP 操作系统中有用的扩展。

为了调试一个程序,你首先必须使用用下面这些特殊的标志之一调用 CreateProcess 来启动拟调试的程序。DEBUG_PROCESS 表示请求来自被调试程序以及被调试程序启动的每一个进程的事件。DEBUG_ONLY_THIS_PROCESS 表示只请求来自被调试程序的事件(而不是来自其子进程的事件)。

使用 DEBUG_ONLY_THIS_PROCESS 标志时,调试器将接收不到来自被调试程序启动的进程事件。性能监视器(perfmon.exe)就是一个很好的例子,此标志对这个程序没有作用。性能监视器是一个简单的打包程序,其作用

只不过是启动另外一个程序——微软管理控制台(MMC),并传递任何所需的参数使它显示性能计数器。

在被调试程序的生命期内,Windows 通知调试器

Figure 1 所列出的事件。这些事件由 DEBUG_EVENT 结构描述,如

Figure 2 所示。

为了接收这些事件,调试器必须调用 WaitForDebugEvent。该函数阻塞调试器的运行,直到被调试程序发生

Figure 1

所列的事件之一,或者超时参数中给定的秒数为止。当调试器处理某个事件时,它调用 ContinueDebugEvent

让被调试程序继续其生命之旅。注意:在调试器中,当 WaitForDebugEvent 解除阻塞时,所有被调试者线程被冻结,在调用 ContinueDebugEvent

期间被解冻。参见 Figure 3:

Figure 3 调试事件流

CApplicationDebugger

调用 CreateProcess 的线程必须是进入调试循环的线程。既然调试器阻塞于 WaitForDebugEvent,因此最好让这部分代码运行在一个与主UI线程不同的专门线程中。本文将其行为包装在

CApplicationDebugger 类中,其声明参见本文附带源代码中的 ApplicationDebugger.h

文件,这个类的一部分灵感还来自 Matt Pietrek 的 LoadProf32(参见

MSJJul95.exe)。

CApplicationDebugger

是一个虚拟类,因为你得从它派生并实现自己的重写版本,以便特定的调试事件发生时进行相应的调用。这个类被用于生成 LoadLibrarySpy(参见 Figure 4),这是一个调试程序和监控 DLL 加载和卸载的工具,不论是静态加载还是动态加载,也不论是不是有加载地址冲突,它都能监控。

Figure 4 LoadLibrarySpy

调用 CreateProcess 是在 CApplicationDebugger::LoadTheProcess

中进行的,为简单起见,参数使用 DEBUG_ONLY_THIS_PROCESS。如果需要,你可以将 CApplicationDebugger

扩展成能处理来自多个被调试进程的事件,对于 MMC 管理单元(snap-ins)很有用。

CLoadLibrarySpyDlg 类负责对话框自身的处理,同时也是暗中监视 CApplicationDebugger 派生类的线程宿主。CModuleListCtrl

类负责显示附属到每个DLL的详细信息 CModuleInfo*;针对每个 DLL,这个类存储的详细信息见

Figure 5

当某个 DLL 被加载,对话框便调用 AddModule 方法;反之卸载DLL时,则执行 RemoveModule 方法。这两个方法都以 UpdateModule

方法告终,从而更新与该 DLL 对应的 CModuleObject 对象的 m_nLoaded 或 m_nRemoved。如果不存在这样的对象,则会创建一个新的对象,并将它添加到列表框中。

不要为 m_nLoaded 或 m_nRemoved 而困惑。如果你针对某一行的相同 DLL 多次调用 LoadLibrary,调试器只会收到 LOAD_DLL_DEBUG_EVENT

一次,并且 m_nLoaded 被赋值为 1。如果调试器接收到某个 DLL 的 UNLOAD_DLL_DEBUG_EVENT,你便可以确定该

DLL 不再被该进程使用。因此,对于静态 DLLs 而言,你决不会收到此事件,即使可能在进程被启动后,它们被动态加载并用 LoadLibrary/FreeLibrary

卸载。

处理被调试程序的事件

一旦被调试程序的进程启动后,调试器便等待某些事件的发生。这就是为什么它应该在一个与主 UI

线程不同的单独线程中的原因,当主窗口是一个模式对话框时尤其如此!

为了在 CLoadLibrarySpyDlg 中有效地使用 CApplicationDebugger,GoThreadProc

线程过程首先声明一个 CApplicationDebugger 对象,指定要执行的命令行并说明是否截获来自被调试程序的 OutputDebugString

或 TRACE 输出。接着,DebugProcess 阻塞,直到被调试程序终止(接收 EXIT_PROCESS_DEBUG_EVENT

或第二次的未处理异常),或者重写的方法之一未返回 DBG_CONTINUE。

线程与对话框之间的沟通机制很简单:当某个被调试事件发生时,调试器线程将

Figure 6

中所列的消息发送到对话框。其中第一个消息是在加载了所有静态链接的 DLLs 时发送;也就是说,当 Windows

触发第一个(伪)断点时,便发信号给调试器,然后调试器调用可重写的 OnProcessRunning

将消息发送给对话框。第二个消息是当被调试程序卸载某个 DLL 时,由可重写的 OnUnloadDLLDebugEvent 调试事件处理例程发送

第三个消息需要所解释几句,为了创建 CModuleInfo,需要 DLL 的全路径名。而在本文第一部分中,我们没有提供任何方法直接从其

hModule 或加载地址获取 DLL 文件名。即便是当调试器接收到此事件时(因为它可能浏览到了它的 PE

头),DLL已经被映射到被调试程序的地址空间,这时,Windows 还没有初始化 PSAPI 所需的数据结构。

事实上,LoadDll.lpImageName 域是一个 LOAD_DLL_DEBUG_INFO

结构成员,LOAD_DLL_DEBUG_INFO 来自 DEBUG_EVENT 结构中的联合 u(参见

Figure 2),LoadDll.lpImageName

总是指向被调试程序地址空间中一块具备读/写/执行权限的奇怪的内存区域,LOAD_DLL_DEBUG_INFO 结构定义如下:

typedef struct _LOAD_DLL_DEBUG_INFO {

HANDLE hFile;

LPVOID lpBaseOfDll;

DWORD dwDebugInfoFileOffset;

DWORD nDebugInfoSize;

LPVOID lpImageName;

WORD fUnicode;

} LOAD_DLL_DEBUG_INFO, *LPLOAD_DLL_DEBUG_INFO;

被加载的DLL的路径名就包含在此内存块中。MSDN 在线帮助文档是这样描述 IpImageName 的:

“...与 hFile 关联的文件名指针。该成员可能为

NULL,也可能包含被调试进程地址空间中的串指针地址。这个地址可能为 NULL 或者指向实际的文件名。

如果 fUnicode 是一个非零值,则名字串是 Unicode,否则是 ANSI 串。该成员是可选项。调试器必须考虑处理

lpImageName 为 NULL 或 *lpImageName(在被调试进程的地址空间中)为 NULL

的情况。很显然,系统决不会为某个创建进程事件提供映像名,同时它也不可能为第一个 DLL 事件传递映像名。系统也决不会在源于

DebugActiveProcess 函数调用的调试事件中提供这个信息。”

OnLoadDLLDebugEvent 可重写方法将上述解释翻译为在 99% 的情况下可工作的纯 C++ 代码。其余 1%

不工作的情况是指加载 ntdll.dll:这种情况既是文档中所说的第一个 DLL 事件。即使延迟到下一个被调试程序事件发生时(参见 CLoadLibraryDebugger

的 OnDebugEvent)才获取路径名。在文档的描述中,可以调用 SearchPath 从模块名获得全路径名,“system32”对于

ntdll.dll 并不感到惊讶。这个 API 函数使用与 LoadLibrary 同样的算法在文件系统中查找某个

DLL。从理论上讲,因为它是由调试器调用的,有可能返回的文件并不是被调试程序加载的那个文件——例如,在调试器文件夹中存在另外一个版本的

ntdll.dll。在实际应用中,ntdll.dll 得不到打补丁的机会,并且被拷贝到了某个与 system32 不同的目录。

防止泄漏

文档中关于 Win32 调试 API 的另一方面的描述是必须释放不同的 XXX_DEBUG_EVENT 结构返回的句柄。Matt Pietrek

在其 November 1995 MSJ“Under the Hood”专栏文章中指出:在 XXX_DEBUG_EVENT

结构中返回到调试器的句柄应该被关闭。事实上,几乎每个句柄都必须用 CloseHandle 关闭。只有一个例外,就是存储在 CREATE_THREAD_DEBUG_EVENT

中的线程句柄,它应该在进程终止时由系统来关闭。其它的句柄如果不关闭,便会造成增长速度非常快的系统资源泄漏,有关的句柄如

Figure 7

所示。这类垃圾的收集由 CApplicationDebugger::HandleDebugEvent 自动处理。

不论你使用哪种清除方法,每次你调试某个进程时,系统不可避免地要泄漏两个句柄:信号机(semaphore

)和端口(port),两者都没有命名。为了让你确信 CApplicationDebugger 不负责处理这种泄漏,请允许我指出:用

sysinternals 的 ProcessExplorer

或 Windows Resource Kit 中的 DH.EXE 可以观察到 Visual Studio 6.0 和 Visual Studio

.NET 中同样的泄漏行为。

现在你已经看到了如何用 Win32 调试 API 来获取某个进程执行期间在其地址空间中加载和卸载的 DLLs 确切列表。Windows

本身提供了另外一个途径来获取有关 DLLs 的其它详细信息。

参考资料

The Win32 Debugging Application Programming Interface

Bugslayer: Windows 2000 and LDR Messages, A COM Symbol Engine, Finding Bloated Functions, and MoreDEB Sample: Debug Event Browser

Spawn Console Processes with Redirected Standard Handles

GetWindowModuleFileName & GetModuleFileName Work Only with the Calling Process

在后续文章中,我将介绍 Windows Loader,它知道一切。

(待续)

作者简介

Christophe Nasarre 是法国 Business Objects 公司的技术经理(technical

manager)。他在 Windows 平台上(3.0 以后的版本)编写了若干个低级工具。他的联系方式:cnasarre@montataire.net.

.

本文出自 MSDN Magazine

August 2002 期刊,可通过当地报摊获得,或者最好是

订阅

本文由 VCKBASE MTT 翻译

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有