简介
调试应用程序时,调试器必须加载可执行模块的符号,以便能够显示有意义的调用堆栈、当前源代码行、变量值等。如果您曾经调试过在另一个系统上创建的小型转储,那么您已经知道除了符号之外,调试器还需要访问创建转储时由应用程序加载的相同版本的模块。如果调试器找不到与模块完全相同的版本(即匹配的模块),则无法加载模块的符号,从而严重限制了成功调试的可能性。在本文中,我们将讨论VS.NET和WinDbg调试器用来识别和定位匹配模块的规则。我们将看到如何告诉调试器在哪里查找匹配的模块。我们还将讨论无法找到匹配模块的情况,并尝试找到解决方法。
模块匹配
调试器如何决定模块是否匹配?他们如何知道需要什么版本的模块?让我们看看Minidump里面。每个小型转储都包含转储进程加载的模块列表。此列表存储为小型转储模块结构的数组(如DbgHelp文档中所述)。此结构声明如下:
typedef struct _MINIDUMP_MODULE { ULONG64 BaseOfImage; ULONG32 SizeOfImage; ULONG32 CheckSum; ULONG32 TimeDateStamp; RVA ModuleNameRva; VS_FIXEDFILEINFO VersionInfo; MINIDUMP_LOCATION_DESCRIPTOR CvRecord; MINIDUMP_LOCATION_DESCRIPTOR MiscRecord; ULONG64 Reserved0; // Reserved for future use. ULONG64 Reserved1; // Reserved for future use. } MINIDUMP_MODULE, *PMINIDUMP_MODULE;
下表描述了结构的成员:
Member | Description |
---|---|
BaseOfImage | Base address of the module in memory. |
SizeOfImage | Size of the module (in bytes) equal to the value stored in the module’s optional header (IMAGE_OPTIONAL_HEADER.SizeOfImage). |
CheckSum | Checksum of the module, equal to the value stored in the module’s optional header (IMAGE_OPTIONAL_HEADER.CheckSum). This value can be 0, unless the module was linked with /release option. |
TimeDateStamp | Time/date stamp of the module, equal to the value in the module’s file header (IMAGE_FILE_HEADER.TimeDateStamp). |
ModuleNameRva | This is a relative address (from the beginning of the minidump) of MINIDUMP_STRING structure that contains the file name (and path) of the module. |
VersionInfo | This structure contains the version information of the module (including file version and product version). More information about the contents of this structure can be found in Platform SDK documentation. |
CvRecord | This is a structure that contains the so-called debug record. You can find more information about its contents in this article (see CV_INFO_PDB20 and CV_INFO_PDB70 structures). This structure is present only if debug information for the module is stored in .PDB file. |
MiscRecord | This structure also contains the debug record, but in another format (IMAGE_DEBUG_MISC). This structure is present only if debug information for the module is stored in .DBG file. |
如果要显示小型转储的模块信息,可以使用小型转储视图工具。例如,以下是kernel32.dll的模块信息:
Module: C:WINNTsystem32KERNEL32.DLL Address: 7c4e0000 Size: 000b9000 CheckSum: 000bdbb3 TimeDateStamp: 3ef274dc File version: 5.0.2195.6688 Product version: 5.0.2195.6688 MISC record: Available
如我们所见,小型转储允许非常精确地标识模块。我们可以确定模块的文件和产品版本,它在文件系统中的位置,并有足够的信息来加载匹配的调试信息文件。但是调试器真的使用所有这些信息来识别模块吗?结果是,它们没有。VS.NET调试器只使用以下数据来检查模块是否匹配:
- File name (MINIDUMP_MODULE.ModuleNameRva)
- Module size (MINIDUMP_MODULE.SizeOfImage)
- Module timestamp (MINIDUMP_MODULE.TimeDateStamp)
WinDbg更精确一些,因为它还使用模块的校验和。以下是WinDbg用来检查模块是否匹配的数据列表:
- File name (MINIDUMP_MODULE.ModuleNameRva)
- Module size (MINIDUMP_MODULE.SizeOfImage)
- Module timestamp (MINIDUMP_MODULE.TimeDateStamp)
- Module checksum (MINIDUMP_MODULE.CheckSum)
有趣的是,模块的版本信息(MINIDUMP_module.VersionInfo)根本没有考虑在内。
模块搜索路径
VS.NET debugger
调试器在哪里寻找匹配的模块?它们搜索预定义的一组目录。这里记录了VS.NET调试器的模块搜索路径。值得注意的是,调试器检查了两个最重要的位置:
- 存储小型转储的目录(及其一些子目录)
- 在创建转储的系统上模块所在的目录
如果我们想指定其他目录在哪里搜索模块,我们可以这样做。有两种可能的方法-在注册表和项目设置中。以下注册表设置可用于指定系统范围内的目录:HKLMSoftwareMicrosoftVisualStudioxxx.xxxNativeDEDumps GlobalModPath = REG_SZ。此设置可以包含用分号分隔的目录列表。类似的设置允许指定用户的模块搜索路径。
在“项目设置”中,我们必须将目录列表输入以下设置:Project properties | Configuration Properties | Debugging | Command Arguments。目录列表的前缀应为“MODPATH=”。例如:MODPATH=c:modules estapp;d:in estapp。如下图
如果同时使用两种指定模块搜索路径的方法(注册表和项目设置),则生成的模块搜索路径是这两种方法的组合。首先检查项目设置中的目录。
WinDbg
WinDbg有更自然的方法来指定模块搜索路径-.exepath命令、-i命令行选项和_NT_EXECUTABLE_IMAGE_PATH环境变量。假如我不记得WinDbg默认使用什么路径,但可以通过以下命令轻松检查:
!sym noisy .reload /f yourmodule.dl
如果我们没有匹配的模块,怎么办?
要确保自己构建的所有模块都存储在一个安全的位置,并且在需要调试崩溃转储时可以找到这些模块并不困难。但是系统dll呢?我们如何确保能够找到安装在客户系统上的相同版本的系统dll?尤其是考虑到热修复之类的东西?
最简单的方法是使用完整的内存内容来生成Minidumps, 如果使用MiniDumpWithFullMemory函数,使用MiniDumpWithFullMemory内存选项,使用NTSD调试器“.dump /ma”,或者检查Watson配置配置中的“崩溃转储类型”设置(这个设置只存在于WindowsXP之后,旧系统总是产生完全崩溃转储)。如果模块的完整内容存储在minidump中,那么理论上,调试器不必在其他地方查找匹配的模块。实际上,似乎只有WinDbg能够在小型转储中利用模块的内容.NET调试器有时只能从小型转储加载模块,但在大多数情况下,它拒绝这样做,并坚持从磁盘加载匹配的模块(如果找不到匹配的模块,则拒绝加载符号)。
当然,具有完整内存内容的小型转储有一个严重的缺点——它们是巨大的。如果我们负担不起自己从客户向开发人员传输许多兆字节的小型转储数据,我们就必须寻找更好的选择。幸运的是,Windows XP和Windows Server 2003操作系统存在一个简单的解决方案——它们的模块在符号服务器上可用,还有符号,调试器(带有符号服务器支持)可以下载它们。我们应该如何配置调试器从symbol服务器下载模块?如果符号服务器的路径是在_NT_SYMBOL_PATH环境变量中指定的,或使用.sympath/.symfix命令指定的,则WinDbg不需要任何其他步骤。VS.NET调试器无法使用_NT_SYMBOL_PATH或符号路径设置进行模块下载。相反,我们必须使用指定模块搜索路径的注册表或项目设置。尽管可以(而且工作正常)在命令参数| MODPATH设置中指定符号服务器的路径,但我发现使用系统范围的注册表设置更方便: HKLMSoftwareMicrosoftVisualStudioxxx.xxxNativeDEDumps GlobalModPath = "srv*c:symbols*http://msdl.microsoft.com/download/symbols"。
建议再执行一个步骤,以确保VS.NET的模块下载可靠–升级symsrv.dll。这个负责符号服务器访问的小DLL的最新版本可以通过Windows的调试工具获得。我们应该将symsrv.dll从调试工具的安装目录复制到<VSInstallDir>Common7IDE目录中,覆盖旧版本的dll。
不幸的是,符号服务器上没有旧操作系统(Windows 2000和更早版本)的系统dll。看来调试器在这种情况下几乎无能为力。即使系统dll的符号在符号服务器上,调试器也无法下载它们,因为它们找不到匹配的模块。WinDbg通过在符号路径上查找任何不匹配的.DBG和.PDB文件提供了一点帮助,但无论如何都没有太大帮助。VS.NET调试器甚至不能做到这一点。
模块化
幸运的是,小型转储中的信息通常足以手动生成一个模块,该模块允许调试器从符号服务器下载符号,即使找不到真正匹配的模块。