第一章 Windows 2000对调试技术的支持
翻译:Kendiv ( fcczj@263.net )
更新:Tuesday, May 03, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
枚举符号
毫不留情的批判完psapi.dll后,现在是说些好话的时候了。psapi.dll可能比较失败,但另一方面,imagehlp.dll却十分完美! 我曾深入研究这个设计精巧的软件,以寻找有关Windows 2000符号文件的内部结构的更多信息。最终,世界著名的“Windows外科专家” Matt Pietrek在三年前发表的一篇文章让我确信:至少到现在还绝对没有必要了解符号文件的格式,因为imagehlp.dll可以很容易的分析它们。但现在这个神话被SymEnumerateSymbols()打破了。该函数的原形在列表1-9中列出。就目前来说,我已经学到了Windows NT 4.0和Windows 2000的符号文件的大多数基本的内部细节,因此我不再需要imagehlp.dll了。我会在下一节覆盖这部分内容。
hProcess参数通常是调用进程的句柄,因此可以使用GetCurrentProcess()的返回值。注意:GetCurrentProcess()不会返回实际的进程句柄。它实际返回一个常量—0xFFFFFFFF,我们称之为伪句柄,它可以被所有期望一个进程句柄的函数使用。0xFFFFFFFE是另一个用来代表当前线程的伪句柄,由GetCurrentThread()返回。
BaseOfDll被定义为DWORD,尽管其实际类型为HMODULE或HINSTANCE。我猜测微软选择这个数据类型是为了表明该值并不需要必须是一个有效的HMODULE,尽管它大多数情况下都是。SymEnumeraterSymbols()计算所有与该值相关的所有可列举符号的基地址。该函数完全可以查询一个没有被加载到任何进程地址空间中的DLL的符号,所以BaseOfDll可以指定为任意值。
BOOL IMAGEAPI SymEnumerateSymbols(
HANDLE hProcess,
DWORD BaseOfDll,
PSYM_ENUMSYMBOLS_CALLBACK CallBack,
PVOID UserContext);
typedef BOOL (CALLBACK *PSYM_ENUMSYMBOLS_CALLBACK)
(PTSTR SymbolName,
DWORD SymbolAddress,
DWORD SymbolSize,
PVOID UserContext);
列表1-9 SymEnumerateSymbols()和它的CallBack函数
CallBack参数是一个指向用户定义的callback函数的指针,该指针函数针对每个符号来调用。列表1-9给出了该参数的相关信息。这个CallBack函数接受一个以0结尾的符号名字符串,符号的基地址将作为SymEnumerateSymbols的BaseOfDll参数,the estimated size of the item tagged by the symbol.SymbolName定义为PTSTR,这意味着其实际类型依赖于调用的是SymEnumerateSymbols的ANSI版还是Unicode版。SDK文档明确规定SymbolSize是一个最佳猜测值(best-guess value)或者为0。我发现SymbolAddress最好为0,
UserContext是一个指针,调用者用它来跟踪记录枚举顺序。例如,它可能指向一块用来存放符号信息的内存块。该指针也被传递给CallBack函数,作为该函数的UserContext参数。CallBack函数可在任何时候取消枚举操作,并返回FALSE。该操作发生的典型情况是发生了无法恢复的错误或者调用者收到他消息,表示其需要等待的时。
PDBG_LIST WINAPI dbgSymbolList( PWORD pwPath, PVOID pBase)
{
PLOADED_IMAGE pli;
HANDLE hProcess = GetCurrentProcess();
PDBG_LIST pdl = NULL;
if ( NULL != pwPath && SymInitialize(hProcess,NULL,FALSE) )
{
if ( (pli = dbgSymbolLoad(pwPath,pBase,hProcess)) != NULL )
{
if ( (pdl = dbgListCreate()) != NULL )
{
SymEnumerateSymbols(hProcess,(DWORD_PTR)pBase,
dbgSymbolCallback,fcpdl);
}
dbgSymbolUnload(pli,pBase,hProcess);
}
SymCleanup(hProcess);
}
return dbgListFinish(pdl);
}
列表1-10 创建一个符号链表
列表1-10 示范了一个使用SymEnumerateSymbols()的典型程序,其源代码来自w2k_dbg.dll。该程序枚举指定模块的符号,在此之前必须完成如下步骤:
1. 在做任何事之前,必须先调用SymInitialize()来初始化符号句柄。列表1-11给出了该函数的原型和另一个在此讨论的函数。hProcess参数可以是系统中任何活动进程的句柄。调试器使用该参数来标识目标进程。SymInitialize()分配的资源必须调用SymCleanup()进行释放。
2. 要获取有关模块的精确信息,最好调用ImageLoad()。注意,该函数由imagehlp.dll导出,dbghelp.dll没有此函数。ImageLoad()返回一个LOADED_IMAGE结构的指针,该结构包含被加载模块的详细信息(参见列表1-11)。该结构必须使用ImageUnload()进行释放。
3. 在调用SymEnumerateSymbols()之前的最后一步就是执行SymLoadModule()。如果ImageLoad()在此之前已被调用,那么将其返回的LOADED_IMAGE结构中的SizeOfImage和hFile作为参数传递给SymLoadModule()。否则,你就必须将hFile设置为NULL,并将SizeOfImage设为0。在此种情况下,SymLoadModule()将试图从符号文件中获取映像的大小,但这不能保证精确。稍后须调用SymUnloadModule()来释放符号表。
BOOL IMAGEAPI SymInitialize(HANDLE hProcess,
PSTR UserSearchPath,
BOOL fInvadeProcess);
BOOL IMAGEAPI SymCleanup(HANDLE hProcess);
DWORD IMAGEAPI SymLoadModule(HANDLE hProcess,
HANDLE hFile,
PSTR ImageName,
PSTR ModuleName,
DWORD BaseOfDll,
DWORD SizeOfDll);
BOOL IMAGEAPI SymUnloadModule(HANDLE hProcess,DWORD BaseOfDll);
BOOL IMAGEAPI ImageUnload(PLOADED_IMAGE LoadedImage);
typedef struct _LOADED_IMAGE
{
PSTR ModuleName;
HANDLE hFile;
PUCHAR MappedAddress;
PIMAGE_NT_HEADERS FileHeader;
PIMAGE_SECTION_HEADER LastRvaSection;
ULONG NumberOfSections;
PIMAGE_SECTION_HEADER Sections;
ULONG Characteristices;
BOOLEAN fSystemImage;
BOOLEAN fDOSImage;
LIST_ENTRY Links;
ULONG SizeOfImage;
} LOADED_IAMGE, *PLOADED_IMAGE;
列表1-11 imagehlp.dll的一些函数原型
在列表1-10中,对SymInitialize()、SymEnumerateSymbols()和SymCleanup()的调用很清晰。这里请先忽略dbgListCreate()和dbgListFinish()调用,这两个函数涉及到w2k_dbg.dll,主要是用来帮助在内存中建立对象链表。前面提到的imagehlp.dll中的函数,隐含于w2k_dbg.dll中的dbgSymbolLoad()和dbgSymbolUnload()中,请参考列表1-12中。注意,dbgSymbolLoad()使用dbgStringAnsi()将模块路径字符串从Unicode转化为ANSI,这是因为imagehlp.dll没有导出一个支持Unicode的ImageLoad()。
PLOADED_IMAGE WINAPI dbgSymbolLoad (PWORD pwPath,
PVOID pBase,
HANDLE hProcess)
{
WORD awPath [MAX_PATH];
PBYTE pbPath;
DWORD dPath;
PLOADED_IMAGE pli = NULL;
if ((pbPath = dbgStringAnsi (pwPath, NULL)) != NULL)
{
if (((pli = ImageLoad (pbPath, NULL)) == NULL) &&
(dPath = dbgPathDriver (pwPath, awPath, MAX_PATH)) &&
(dPath < MAX_PATH))
{
dbgMemoryDestroy (pbPath);
if ((pbPath = dbgStringAnsi (awPath, NULL)) != NULL)
{
pli = ImageLoad (pbPath, NULL);
}
}
if ((pli != NULL)
&&
(!SymLoadModule (hProcess, pli->hFile, pbPath, NULL,
(DWORD_PTR) pBase, pli->SizeOfImage)))
{
ImageUnload (pli);
pli = NULL;
}
dbgMemoryDestroy (pbPath);
}
return pli;
}
// -----------------------------------------------------------------
PLOADED_IMAGE WINAPI dbgSymbolUnload (PLOADED_IMAGE pli,
PVOID pBase,
HANDLE hProcess)
{
if (pli != NULL)
{
SymUnloadModule (hProcess, (DWORD_PTR) pBase);
ImageUnload (pli);
}
return NULL;
}
// -----------------------------------------------------------------
PDBG_LIST WINAPI dbgSymbolList (PWORD pwPath,
PVOID pBase)
{
PLOADED_IMAGE pli;
HANDLE hProcess = GetCurrentProcess ();
PDBG_LIST pdl = NULL;
if ((pwPath != NULL) &&
SymInitialize (hProcess, NULL, FALSE))
{
if ((pli = dbgSymbolLoad (pwPath, pBase, hProcess)) != NULL)
{
if ((pdl = dbgListCreate ()) != NULL)
{
SymEnumerateSymbols (hProcess, (DWORD_PTR) pBase,
dbgSymbolCallback, &pdl);
}
dbgSymbolUnload (pli, pBase, hProcess);
}
SymCleanup (hProcess);
}
return dbgListFinish (pdl);
}
列表1-12. 加载/卸载符号信息
即使仅提供模块名,不提供任何路径信息,ImageLoad()也可定位指定的模块。不过,对于/winnt/system32/drivers目录下内核驱动程序,该函数将调用失败。因为它该目录并不是系统搜索路径的一部份。此时,dbgSymbolLoad()将调用dbgPathDriver()函数,然后再尝试调用LoadImage()。如果路径中仅包含一个文件名,那么dbgPathDriver()将简单的在指定路径前增加一个”driver/”前缀。如果这两次ImageLoad()调用中有任意一个能返回一个有效的LOADED_IMAGE指针,则dbgSymbolLoad()将加载模块的符号表(使用SymLoadModule()函数),并返回LOADED_IMAGE结构(如果成功的话),至此,dbgSymbolLoad()的任务就完成了。与之很相似的是dbgSymbolUnload(),这个函数的价值不大,它完成卸载符号表,和销毁LOADED_IMAGE结构的工作。
在列表1-10中,SymEnumerateSymbols()被指示使用dbgSymbolCallback()函数来进行回调(callback),dbgSymbolCallback()是w2k_dbg.dll中的一个导出函数。我没有给出SymEnumerateSymbols()的源代码,因为它是imagehlp.dll中的一个函数。该函数仅使用它接收到的符号信息(参见列表1-9中的PSYM_ENUMSYMBOLS_CALLBACK的定义)并将此符号信息保存到UserContext指向的内存块中,UserContext指向的内存块由调用者分配。尽管w2k_dbg.dll提供的链表、索引和排序函数等特性仅对其自己有用,但它们也属于本书的范畴。如果你需要更多的信息,请参考w2k_dbg.dll和w2k_sym.exe的源代码。
Windows 2000符号浏览器
w2k_sym.exe是w2k_dbg.dll的一个客户端程序,它运行于Win32控制台下。如果不使用任何参数调用它,它将显示如示例1-5所示的内容。w2k_sym.exe可识别多个命令行选项,它根据这些选项来确定它该执行什么样的操作。四个基本的选项是:/p(列出进程)、/m(列出进程模块)、/d(列出驱动和系统模块)、或者你要查看符号信息的模块的全路径。通过使用排序、过滤等显示模式选项可以修改程序的默认显示方式。例如,如果你打算查看ntoskrnl.exe的所有符号,并按照名称进行排序,可使用命令:w2k_sym /n /v ntoskrnl.exe。/n选项表示按名称进行排序,/v选项表示显示详细的信息,否则,将只显示一些汇总信息。
// w2k_sym.exe
// SBS Windows 2000 Symbol Browser V1.00
// 08-27-2000 Sven B. Schreiber
// sbs@orgon.com
Usage: w2k_sym { <mode> [ /f | /F <filter> ] <operation> }
<mode> is a series of options for the next <operation>:
/a : sort by address
/s : sort by size
/i : sort by ID (process/module lists only)
/n : sort by name
/c : sort by name (case-sensitive)
/r : reverse order
/l : load checkpoint file (see below)
/w : write checkpoint file (see below)
/e : display end address instead of size
/v : verbose mode
/f <filter> applies a case-insensitive search pattern.
/F <filter> works analogous, but case-sensitive.
In <filter>, the wildcards * and ? are allowed.
<operation> is one of the following:
/p : display processes - checkpoint: processes.dbgl
/m : display modules - checkpoint: modules.dbgl
/d : display drivers - checkpoint: drivers.dbgl
<file> : display <file> symbols - checkpoint: symbols.dbgl
<file> is a file name, a relative path, or a fully qualified path.
Checkpoint files are loaded from and written to the current directory.
A checkpoint is an on-disk image of a DBG_LIST structure (see w2k_dbg.h).
示例1-5. w2k_sym.exe的帮助信息
作为一个附加功能,w2k_sym.exe允许读取/写入检查点文件。一个检查点文件只是一对一的将对象列表写入到磁盘文件中。你可以使用检查点来保存系统的当前状态以便以后进行比对。检查点文件中包含一个CRC32字段,在加载检查点文件时将使用该字段检查文件内容的有效性。w2k_sym.exe在当前目录下维护4个检查点,分别对应前面提到的四个选项,即进程、模块、驱动和符号列表。
深入微软符号文件
微软提供了一个标准的接口来访问Windows 2000的符号文件(Symbol file),该接口隐藏了这些文件的内部格式。有时,你可能想直接读取这些符号文件,以获取进一步的控制权。在这一节里,我将向你展示符号文件.pdb和.dbg中的数据的结构形式,并提供了一个DLL和一个示例性的客户端程序来使你可以查找和浏览符号文件的内部信息。是的,这将是另一个符号查看程序,不过不要担心,这将是一个全新的程序,和我们先前讨论的将完全不同。
符号的编码方式
符号名称在微软符号文件中的存储格式叫做编码格式(原始的符号都使用前缀/后缀进行了一定的修饰),这意味着这些增加了前缀和后缀的符号名将包含更多有关类型和如何使用符号的信息。表1-4列出了最常见的编码方式。由C代码生成的符号通常有一个下划线或者一个@符号,这与采用的调用约定有关。@表示这是一个__fastcall函数,下划线表示__stdcall和__cdecl函数。__fastcall和__stdcall约定将参数堆栈的清理工作留给了被调用函数,同时这种类型的函数还表示参数所占字节数将由调用者放入堆栈中。这些信息被增加到符号化名称上,由@符号分隔开来。此种情况下,全局符号将按照__cdecl函数对待,这意味着它们的符号将由一个下划线开始但结尾处没有参数堆栈的相关信息。
表1-4. 符号编码方式的分类
示 例 | 描 述 |
symbol | 未修饰的符号(可能定义于ASM模块中) |
_symbol | __cdecl函数或全局变量 |
_symbol@N | __stdcall函数(其参数占用N字节) |
@ symbol@N | __fastcall函数(其参数占用N字节) |
_imp_symbol | __cdecl函数或变量的import thunk |
_imp_symbol@N | __stdcall函数(其参数占用N字节)的import thunk |
_imp_@symbol@N | __fastcall函数(其参数占用N字节)的import thunk |
?symbol | 内嵌参数类型信息的C++符号 |
__@@_PchSym_symbol | PCH符号 |
符号名称中的_imp__或_imp_@前缀表示这是一个import thunk,import thunk是指一个函数指针或位于其他模块中的变量。Import thunk可以在运行时更容易的动态链接到其他模块的导出符号上,而不需要关心目标模块的实际加载地址。当一个模块加载到内存之后,载入机制将修改Thunk指针使其指向实际的入口点地址。Import thunk的优势在于对导入的函数或者变量只需修改一次,所有对此外部符号的引用都将导向该符号的Thunk。应该注意的是Import thunk并不是必须的。这取决于编译器是想使用thunks来使所需的修改最小化,还是不使用thunks来最小化内存使用率(使用thunks将占用一定的内存空间)。如表1-4所示,相同的前缀/后缀规则也可应用于本地的导入符号,但前缀为__imp_的import thunks除外(注意,该前缀有两个下划线)。
通过先前示范的w2k_sym.exe可很容易得发现Imagehlp.dll的在对符号进行解码(undecoration)时存在问题,只所以w2k_sym.exe可以证明这一点是因为它实际上使用的是imagehlp.dll中的API(通过w2k_dbg.dll)。如果你使用命令:w2k_sym /v /n /f __* ntoskrnl.exe,来要求w2k_sym.exe显示有两个下划线的符号(按名称排序),你会发现有些地方和示例1-6并不相同。在列表顶部堆积的大量字符让人非常困惑。在内核调试器中使用ln 8047F798得到结果却是:ntoskrnl!__,这更让人气愤。地址 8047F798处的符号的原始修饰名实际居然是__@@_PchSym_@00@UmgUkirezgvUmlhUlyUfkUlyqUrDIGUlykOlyq@ob,看起来似乎是imagehlp.dll简单的丢弃了除两个下划线外的所有符号。
示例1-6. w2k_sym /v /n /f __* ntoskrnl.exe命令的输出结果
更好一些的示例命令是:w2k_sym /v /n /f _imp_* ntoskrnl.exe,该命令显示所有以_imp_开头的符号,这些符号就是ntoskrnl.exe的import thunks。示例1-7给出部分符号。这里再次出现了与示例1-6类似的情况,列表开始都是一大段非常晦涩难懂的名字,而内核调试器也无能为力(内核调试器给出的这些地址对应的符号名与列表中的相同)。如果我告诉你地址0x 804005A4处的原始符号名其实是__imp_@ExReleaseFastMutex@4,你会怎么想呢?很显然,有一个下划线迷路了,第一个@之后的整个字符串都丢失了。看起来imagehlp.dll中的解码算法在处理@符号时存在着问题。这种奇怪行为的原因是:@并不只是__fastcall函数名的前缀,它也是__fastcall和__stdcall函数末尾用来分隔堆栈大小的分隔符,显然,解码算法应该能查找第一个下划线和@符号,错误的假定末尾的剩余部分指定了参数在调用者堆栈中所占的字节数。因此,较长的PCH符号被从两个下划线之后截断了,__fastcall的import thunk被简化为_imp_。在这两种情况下,第一个下划线将被移除并且第一个@以及其后的字符都将被丢掉。
示例1-7. w2k_sym /v /n /f _imp_* ntoskrnl.exe命令的输出结果
译注:
在Windows XP SP2上,我测试的结果是ntoskrnl.exe并没有导出以双下划线为前缀的符号,ntdll.dll倒是导出了一些。( 2005-5-16)
如示例1-6、1-7所示的问题,在Windows XP SP2上并未发现。
上面两个例子所示的问题可能会让你失去耐心,你可能会喊道:“Hey,我还是以自己的方式来干吧!”,但存在的问题是几乎没有文档记录过有关微软符号文件格式的内部信息,而某些符号信息----特别是PDB文件的结构—则一点文档也没有。甚至在微软基本知识库中居然包含如下内容:
“Program Database文件的格式(即.PDB文件的格式)没有提供相应的文档。这些信息是微软私有的。”(微软 2000d)
这么看来开发自己的符号信息分析器似乎必定要失败了。别担心,我将告诉你PDB文件的结构到底是什么样的。但在开始之前,我们需要深入研究一下.dbg文件,因为它是我们整个故事的开始。