定制调试诊断工具和实用程序
——摆脱DLL"地狱"(DLL Hell)的困扰(五)
原著:Christophe Nasarre
编译:NorthTibet
下载源代码:Debugsrc0206.exe (583KB)
原文出处:Windows XP:Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities
本文假设你熟悉 Win32,DLL
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(一)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(二)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(三)
定制调试诊断工具和实用程序——摆脱DLL"地狱"(DLL Hell)的困扰(四)
列举加载的模块
任何时候通过 PSAPI 或 TOOLHELP32 都可以列出某个进程加载的 DLLs 列表。在写此文前的调研过程中,我研究了 Matt Pietrek 以前在
MSJ Under The Hood 专栏中的一篇文章,其内容是讨论如何使用 TOOLHELP32 来实现前述的功能,我发现在 Windows 2000 和 Windows
XP 环境中是有问题的,代码不能正常工作,现将其代码摘录如下:
用TOOLHELP32遍历模块
//
//通过取得ToolHelp32 进程快照,枚举此进程的模块列表
//
HANDLE hSnapshotModule;
hSnapshotModule = pfnCreateToolhelp32Snapshot( TH32CS_SNAPMODULE,
procEntry.th32ProcessID );
if ( !hSnapshotModule )
continue;
// 迭代快照中每一个模块
MODULEENTRY32 modEntry = { sizeof(MODULEENTRY32) };
BOOL fModWalkContinue;
for (fModWalkContinue = pfnModule32First(hSnapshotModule,&modEntry);
fModWalkContinue;
fModWalkContinue = pfnModule32Next(hSnapshotModule,&modEntry) )
{
// 确定是否为EXE文件本身,如果是,则不将它加入模块列表
if ( 0 == stricmp( modEntry.szExePath, procEntry.szExeFile ) )
continue;
// 确定是否为我们已有的DLL
PModuleInstance pModInst = modList.Lookup(modEntry.hModule,
modEntry.szExePath );
// 如果以前没有见过,则将它加入列表
if ( !pModInst )
pModInst = modList.Add( modEntry.hModule, modEntry.szExePath );
// 将此进程加入到使用此DLL的进程列表
pModInst-AddProcessReference( procEntry.th32ProcessID );
}
CloseHandle( hSnapshotModule ); // 完成模块列表快照
其实并不是程序有什么瑕疵,主要是时过境迁,导致代码中一个if语句的使用无效,毕竟 Matt Pietrek 写那篇文章的时候(其代码是1998.9 在 MSJ 上发布的),Windows
2000 还不知道在哪里呢!
那个无效的 if 语句是这样的:由于 CreateToolhelp32Snapshot 调用失败时不会返回
NULL,所以下面的错误处理代码是无效的:
if ( !hSnapshotModule )
continue;
实际上,如果失败,hSnapshotModule的值为INVALID_HANDLE_VALUE或-1,并且这个if语句是捕获不到它的,这到没什么,关键是如何发现这个bug。当我在Windows
2000上测试ProcessSpy时,一切运行正常,只是当列表框即便为空的时候,程序也没有返回某些进程的出错信息。由于错误处理代码本身是错的,执行跳过了循环,Module32First调用失败,但没有任何实质性的错误。如果你在Windows
2000环境用Matt Pietrek的这篇文章提供的ModuleList工具,你将得到不正确的结果。
为了搞清楚代码运行中发生的事情,用本文实例代码包含的Helpers.cpp 文件中提供的GetLastErrorMessage辅助函数可以有助于你看得更清楚。他调用GetLastError
和 FormatMessage以纯文本形式获取相应的失败原因。失败原因都一样:Access Denied,也就是拒绝存取。但是使用PSAPI函数时,当获得相同进程的模块列表时不存在存取问题。
之所以发生存取问题,是由于缺乏优先级。使用TOOLHELP32 的代码要正常工作必须得有 SE_DEBUG_NAME 优先级。有关这个问题的详细信息,请参考 1998.3
MSJ 的
Q&A Win32 专栏以及 1999.8 的
关于 DLL 的方方面面
用 PSAPI 和 TOOLHELP32
两种途径获得的某个进程所加载的模块列表只反映地址,在这个地址处,DLL被映射到地址空间。下一步便是尽可能完整地获取关于DLL的描述。我的实现并不象在CProcess中所做的那样提供单独的
AttachModule 方法。因为要获取某些细节信息代价实在太高,因此我选择将它们分割成不同的函数。最不值钱的信息从 CModule 的构造函数获得,其它信息的获取要到相应的存取器方法被调用(通过
Refresh 函数)。实现细节请参考 Module.cpp 文件。其 Refresh 方法模仿了 Matt Pietrek 的
CModuleList 中的Refresh/RefreshTOOLHELP32 方法。表三列出了 CModule 的存取器方法:
存取器
说明
HMODULE GetModuleHandle
DLL被映射的地址
CString& GetFullPathName
源自TOOLHELP32::Module32xxx
或PSAPI::GetModuleFilenameEx
CString& GetPathName
同GetFullPathName
CString& GetModuleName
同GetFullPathName
我前面提到过,想要获取模块的全路径名需要一点诀窍。由于一些原因,GetModuleFilenameEx 或 TOOLHELP32
模块函数返回的模块名很奇怪,它们不遵循 Win32 的命名标准。例如以smss为例,返回的名字是"\SystemRoot\System32\smss.exe",这里"\SystemRoot"必须用Windows文件夹的实际名字来替换。又如
wonlogon.程序,返回的名字是"\??\C:\WINNT\system32\winlogon.exe",应该转换成"C:\WINNT\system32\winlogon.exe"。"\??\"前缀是
Windows NT 名字空间的残留物,是 kernel 模式中的东西,即便是Win32编程也很少用到它。我写了一个辅助函数
TranslateFilename 用于将这些文件名转换成更标准的形式。此函数的细节请参考下载源代码中的Helpers.cpp 文件。
我用 Refresh 方法采集其余的模块描述,具体实现请参考 Module.cpp 文件,下面是对它的一个概述,详细的存取函数见表四:
存取器
说明
DWORD
GetBaseAddress
使用PE_EXE::GetImageBase
来获得首选的加载地址
void
GetFileTime(FILETIME& ft)
用KERNEL32.DLL
输出的API
GetFileTime来获悉何时被创建、修改和最后一次存取
CString&
GetFileTime
获得与上一个函数相同的信息,但这里是文本格式,使用GetFileDateAsString/GetFileTimeAsString
辅助函数
DWORD
GetFileSize
用PE_EXE::GetFileSize
获取文件的大小,以字节为单位
CString&
GetSubSystem
用PE_EXE::GetSubSystem
获悉IMAGE_SUBSYSTEM_xxx模块子系统之一,在winnt.h
文件中定义,在这个文件的最新版本中可以找到IMAGE_SUBSYSTEM_XBOX
void
GetLinkTime(FILETIME& ft)
用PE_EXE::GetTimeDateStamp
获取模块的链接时间
CString&
GetLinkTime
获得与上一个函数相同的信息,但这里是文本格式,使用GetFileDateAsString/GetFileTimeAsString辅助函数
WORD
GetLinkVersion
用PE_EXE::GetLinkerVersion
获取用于构造此模块的链接器版本
大部分的描述信息都是从文件本身吸取出来的,同时借助了 Matt Pietrek 所写的几篇文章中有关PE格式的知识。
如果你想了解更多有关 PE 文件的细节,请阅读 Matt Pietrek 的这些文章,其中重点是 PE_EXE 类和 PEDUMP
实现。其代码对于诸位具有很高的参考价值。
GetBaseAddress 一个有趣的使用方法是将它的返回值与 GetModuleHandle
的返回值进行比较。后者是实际的地址,正是在这个地址,模块被加载到进程地址空间里,而前者的地址是模块希望被加载的地址。这正好用来发现加载是否冲突。
当一个进程启动时,Windows 加载程序自动加载静态DLLs。这些静态链接的东西很容易用PE格式和
MODULE_DEPENDENCY_LIST 类通过编程获得。没有哪个API能扫描到这些模块与那些用 LoadLibrary 或
CoCreateInstance 动态加载的模块之间的差别。如果一个DLL被某个进程使用,但它又不在静态链接之列,那么它就应该是动态加载的。
在 ProcessSpy
的输出画面中,如图四,底下的窗格中每一个模块都有一个前缀图符,圆形的D表示动态加载的,方形的S表示静态加载的。它们的颜色也有不同的意思,红色表示这个模块的基地址与其加载地址是不同的,反之则为浅蓝色。
除了从文件本身吸取描述信息外,还可以从它的资源版本中获取其它描述信息。Paul DiLascia 在他的
C++Q&A 专栏(MSJ
1998.4)文章中为我们提供了一个很帅的打包类 CModuleVersion,用这个类可以方便地获得资源版本中对模块的描述信息。对于每一项VS_VERSION_INFO
细节都有存取函数,这些函数返回 CString 引用,都是由 CModuleVersion::GetFileVersion 用相应的串填写。GetCompanyName
就是一个很好的例子。
为了满足我的需要,我对 Paul DiLascia 的代码进行了修改。GetFileVersionInfo
方法应该得到模块的名字,而不是真正的文件名。为了获取相应的文件名,调用 GetModuleHandle。如果在当前的进程空间中查找模块失败(这种情况罕见)。为了解决这个问题,当给定的模块名就是实际的执行文件名时(用
GetFileAttributes 可以判断出来),则直接使用它即可。
Windows
提供的资源信息不仅仅限于公司名这么简单,通常还有更多的东西,例如,从中可以很容易知道应用程序是否为Debug版本,是否是私隐或特别版。你必须看一下
VS_FIXEDFILEINFO 结构中的 dwFileFlags
标志。MSDN文档对它的描述是包含一个位码(bitmask)值,这些位码值的含义请参考表五:按照版本信息对文件进行分类:
(表五)
标志
描述
VS_FF_DEBUG
包含调试信息或者编译时是按可调试方式编译的
VS_FF_INFOINFERRED
动态创建版本结构,因此这个结构中的某些成员可能为空或不正确。在文件的VS_VERSIONINFO数据中决不能设置此标志
VS_FF_PATCHED
已经被修改并且与原来同一版本号的文件不相同了
VS_FF_PRERELEASE
开发版本,非商业发布产品
VS_FF_PRIVATEBUILD
没有用标准的发布过程构造,如果设置了此标志,则StringFileInfo
结构应该包含PrivateBuild
项
VS_FF_SPECIALBUILD
由原公司用标准的发布过程构造,但是相同版本号的标准文件的变种。如果设置此标志,则StringFileInfo
结构应该包含SpecialBuild
项
在相同版本的结构中,dwFileType域定义了文件类型,参见表六:dwFileType域中的标志
(表六)
标志
描述
VFT_UNKNOWN
系统未知
VFT_APP
包含一个应用程序
VFT_DLL
包含一个动态链接库(DLL)
VFT_DRV
包含一个设备驱动程序,如果dwFileType
是VFT_DRV,则dwFileSubtype
包含进一步的关于此驱动程序的描述
VFT_FONT
包含一种字体,如果dwFileType
是VFT_FONT,则dwFileSubtype
包含进一步的字体文件描述
VFT_VXD
包含一个虚拟设备
VFT_STATIC_LIB
包含一个静态链接库
ProcessSpy 使用这些标志来表示版本栏(Version),用D表示 Debug,用P表示补丁,参见图四。
下一回内容预告
本文以后的内容将讨论几种用非常规方式来获取一些附加的信息源。也就是说如果在没有可借助的 API 的情况下,你就可以用这几种非常规方式。其中包括我至今未曾提到的一个主要信息源,那就是 Windows 的外壳(Shell)。在模块文件中隐藏一个文件的时候,
关于某个文件的信息,没有人比 Windows 资源管理器知道的更多。如图十八所示:
图十八 用资源管理器查看文件信息
那么如何从自己的程序中打开或者调用 Windows 资源管理器文件属性对话框呢?关于这个请参考精华区的一小段代码。其关键是先填写 SHELLEXECUTEINFO 结构,注意结构中的 fMask 成员一定要用 SEE_MASK_INVOKEIDLIST 赋值,然后调用
ShellExecuteEx API 函数,如:
SHELLEXECUTEINFO sei;
ZeroMemory(&sei,sizeof(sei));
sei.cbSize = sizeof(sei);
sei.lpFile = szFilename;
sei.lpVerb = _T("properties");
sei.fMask = SEE_MASK_INVOKEIDLIST;
ShellExecuteEx(&sei);
在 ProcessSpy 程序界面底部窗格中任何一个模块记录上双击鼠标便可以调出文件的属性对话框,相应模块文件的描述信息一目了然。注意
Windows XP 中不支持多个 ShellExecuteEx 调用,当你调用第二次时,线程冻结,也不会有任何提示。
正如你所看到的,有许多方法都可以获得加载 DLLs
以及活动进程的信息。我在本文中提供的几个工具可以作为一个很好的学习开端,你完全可以借鉴文本描述的方法以及所提供的 C++
类来定制满足自己需要的调试工具。
参考资料
如何在 Windows NT、Windows 2000 和 Windows XP 中使用VDMDBG函数?
Windows NT 系统中如何启动和终止 16 位 Windows 应用程序?
在后续文章中,我将介绍从系统获取信息的新方法。
(待续)
作者简介
Christophe Nasarre 是法国 Business Objects 公司的技术经理(technical
manager)。他在 Windows 平台上(3.0 以后的版本)编写了若干个低级工具。他的联系方式:cnasarre@montataire.net.
.
本文出自 MSDN Magazine 的
June 2002 期刊,可通过当地报摊获得,或者最好是
本文由 VCKBASE MTT 翻译