符号与故障转储---VS studio 和 WinDbg

符号服务器

在 您的应用程序和操作系统之间排列正确的符号是进行快速调试的秘密所在。您知道当您不对它们进行协调时会发生什么;您得到了恰好只含有一个项的漂亮的调用堆 栈。符号如此重要的原因是框架指针省略 (FPO) 数据作为 PDB 文件的一部分包含在其中。尽管您可能想摆弄符号会非常麻烦,但请想象一下 Windows® 操作系统开发人员的工作有多么困难。尽管您具有您可能认为非常大的应用程序,但操作系统开发人员具有世界上最大的商业应用程序。我认识 Microsoft 操作系统小组的人,并且我曾经问他们是否从调试他们的应用程序的用户那里获得过任何帮助。他们全都笑了,并且告诉我,他们就操作系统获得的帮助与我在为谋 生而编写开发人员工具时获得的帮助一样多。换句话说,就是没有获得任何帮助。

当然,在任何给定时间,他们都具有比您能够想象到的更多个版本的操作系统正在运行。在开发周期中,他们可能具有多达 10,000 个不同的版本在世界各地运行。如果您认为您在使符号匹配方面有麻烦,那么和他们相比根本不值得一提。


Microsoft 的开发人员意识到他们必须做点什么,以便为他们自己以及他们的客户提供一些方便。由此诞生了“符号服务器”。其概念非常简单:将所有公共版本的所有符号存 储在一个已知的位置,并且使调试器变得更加智能,以便它们可以在没有任何用户交互的情况下加载正确的符号。美妙之处在于事实几乎是同样简单!虽然存在一些 小问题(我将在本专栏中加以指明),但通过正确地设置符号服务器,您永远都不再需要符号了。

通往符号天堂的第一步是从 http://www.microsoft.com/ddk/debugging 下载最新版本的 WinDBG,因为符号服务器二进制文件是由 WinDBG 小组开发的。您将需要检查是否有 WinDBG 更新版本,因为该小组似乎具有相当紧凑的发布日程安排,并且每隔几个月就会发布更新版本。在安装 WinDBG 以后,请将安装目录添加到主 PATH 环境变量中。必须可以访问两个关键的二进制文件 SYMSRV.DLL 和 SYMSTORE.EXE,以便从您的符号服务器读取这些文件或者将它们写入您的符号服务器。



bugslayer0206fig01

1 SYMSRV




符号存储区本身只是一个恰好使用文件系统来查找这些文件的数据库。 1 显 示了我的一台计算机上的符号服务器树资源管理器中的部分列表。根目录是 WebSymbols,并且各个符号文件(例如,ADVAPI32.PDB)列在第一级。在各个符号文件名下面是一个目录,该目录与完全识别该符号文件的 特定版本所需的日期/时间戳、签名和其他信息相对应。请记住,如果对于不同的操作系统版本,一个文件(例如,ADVAPI32.PDB)有多个版本,则对 于您已经访问的每个唯一版本,在相应的文件(例如,ADVAPI32.PDB)下面都有多个目录。在签名目录中,您很可能具有该版本的特定符号文件。还有 一些措施,即让特殊的文本文件指向符号存储区中的其他位置,但通过遵循我的建议,您将具有实际的符号文件。

实际创建符号服务器需要完成两个 非常困难的步骤。首先,在服务器上创建一个文件夹,给予开发小组中的每个人读写权限,并且确保您具有足够的可用磁盘空间。其次,为所有开发人员共享该文件 夹。您很可能希望服务器和共享名类似于 //Symbols/Symbols 或者是其他易于记忆的名称。

当您用操作系统符号填充符号服务 器时,它的真正魅力才会显现出来。如果您多年以来一直是一个好的 bugslayer,那么您很可能已经在自己的计算机上安装了操作系统符号。这总是有一点儿令人沮丧,因为您很可能安装了一些热修复程序,而某些操作系统 符号从来不会包括这些热修复程序符号。符号服务器带来的好消息是它可以确保您总是获得正确的操作系统符号,而无需进行任何操作。这是一个巨大的方便。

这 里的奥妙在于 Microsoft 已经使所有已发布的操作系统(从 Windows NT® 4.0 到 Windows .NET Server 的最新测试版本,包括所有操作系统热修复程序)的符号可供下载。要体验这一奥妙,您需要将 _NT_SYMBOL_PATH 环境变量设置为 SRV*//Symbols/Symbols* http://msdl.microsoft.com/download/symbols。请注意,我假设您的符号存储区位于名为 //Symbols 的服务器上的名为 Symbols 的共享文件夹中。如果您的符号存储区位置与此不同,只须替换您自己的值。

当您下次开始调试时,调试器将 看到设置了 _NT_SYMBOL_PATH,如果尚未下载符号文件,则自动开始通过 HTTP 从 Microsoft 下载操作符号,并且将它们放入您的符号存储区。请记住,符号服务器将只下载它需要的符号,而非每个操作系统符号。这就是为什么将符号存储区放入共享目录是 如此重要的原因;如果您的某位队友已经下载了符号,则您就可以避免执行可能很漫长的下载。

这样就可以很好地处理适当的操作系统符号,因此让我们致力于将您的产品符号放入符号服务器。SYMSTORE.EXE 是一个命令行实用工具,通过该工具可以将包含符号的整个目录树添加到您的符号存储区。SYMSTORE.EXE 具有许多命令行开关(请参见 2)。

使 用 SYMSTORE.EXE 的最佳方式是让其在日常版本或里程碑版本的结尾自动添加您的完整版本树。您大概不希望让开发人员添加他们的本地版本,除非您确实要消耗大量磁盘空间。例 如,下面的命令将在您的符号存储区中为 D:/BUILD/RELEASE 下的所有目录(包括该目录)存储所有 PDB 和二进制文件:

symstore add /r /f d:/build/release/*.* /s //Symbols/Symbols
/t "MyApp" /v "Build 632"

将二进制文件存储在您的符号存储区中,以便故障转储可以自动排列这些二进制文件,这是一种很不错的做法,但这样做可能会消耗大量磁盘空间。如果您只希望包括 PDB 文件,则可以使用以下命令:

symstore add /r /f d:/build/release/*.PDB /s //Symbols/Symbols
/t "MyApp" /v "Build 632"

在 WinDBG 文档资料中的 Symbols 下面,有大量关于 SYMSTORE.EXE 和符号服务器的内容可供阅读;我在这里所讨论的是我发现对我非常适用的步骤。令我感到惊奇的是,符号服务器能够如此完美地工作并且总是能够以快得多的速度 进行调试,因为我几乎总是可以得到完美的调用堆栈。

故障转储

有 关符号服务器的真正令人难以置信的事实是,如果您也读取故障转储,则 WinDBG 和 Visual Studio .NET 都将使用它们。假如您使用的是其他操作系统之一,则故障转储就是 Microsoft 所谓的进程用户模式转储(当进程崩溃时)。如果您选中了 3 中显示的 Create Crash Dump 按钮,则 Dr. Watson(默认的调试器)将写入故障转储。正如您可以猜到的那样,故障转储几乎是用于监视应用程序崩溃的一项接近于完美的技术。



bugslayer0206fig03

3Create Crash Dump




正如大多数人所意识到的那样,WinDBG 很长时间以来就能够读取和处理故障转储了。尽管如此,人们可能并不知道 Visual Studio .NET 也可以很好地处理故障转储。这很了不起,因为 WinDBG 的 UI 将极少主义发挥到了一个新的水平。

在 Visual Studio .NET 中处理故障转储非常容易,但打开一个故障转储会令人感到有点困惑。从一个全新的 Visual Studio .NET 实例开始,然后从 File 菜单中选择 Open Solution。在 File Open 对话框中,选择 Files of Type 组合框中的上数第五个项,即 Dump Files (*.dmp; *.mdmp)。导航到包含故障转储文件的目录,并且打开该目录。这将创建一个新的、需要您保存的解决方案。要开始查看故障转储,只须按调试键之一,例 如,F5 (Go) 或 F10 (Step)。您将看到消息框弹出并报告错误,而且,如果您具有所有适当的符号和源代码,则将恰好为您指出发生崩溃的代码行。就这么简单。

这两个调试器都可以在调试过程中的任何时刻写出故障转储。当我跟踪困难的问题时,我经常这样做,因为我可以快速查看我在调试时看到的不同阶段。这可以节省大量时间。

要 从 Visual Studio .NET 中写转储,只须在调试过程中单击 Debug 菜单,然后选择该菜单上的最后一项 Save Dump As。Visual Studio .NET 可以写出两种类型的故障转储。小型转储包含模块信息,例如,名称和日期/时间戳以及所有线程的调用堆栈。小型转储非常小,介于 3 到10 KB 之间。另一方面,带有堆的小型转储写出相同的信息,但是还写出所有被标记为“已分配内存”的内存。这样,您可以查看指针变量指向哪些内容。带有堆的小型转 储要大得多;对于简单的“Hello World!”程序,它们的大小一般为 2.5MB。

在 WinDBG 中,通过 .dump 命令创建故障转储。WinDBG 的故障转储的一项附加功能是:使您可以用 .dump/mh 命令获得故障转储中存储的进程的所有句柄数据。通过 !handle 命令,您可以随后从故障转储中看到句柄的确切状态。这对于跟踪死锁而言是非常有用的。

您甚至可以通过调用 DBGHELP.DLL 中的 MiniDumpWriteDump API 函数,随时写出您自己的故障转储。请记住,您必须使用 WinDBG 安装中的最新版本的 DBGHELP.DLL 才能使该函数正常工作。唯一的问题是如果您自己调用 MiniDumpWriteDump,则故障转储将在 MiniDumpWriteDump 的执行过程中启动,这可能意味着您无法沿着堆栈向后审核到您自己的代码。因此,BugslayerUtil.DLL 包含一个名为 CreateCurrentProcessMiniDump 的函数,该函数将适当地包装对 MiniDumpWriteDump 的调用,以便您可以在需要时获得可能最好的故障转储。

调试引擎

尽 管拥有故障转储是非常奇妙的,但您在加载故障转储时总是在做同样的事情;您将枚举线程以便可以了解每个线程的位置。因为我很懒,所以我需要一个工具为我提 供我总是要寻找的信息,以便我甚至不必启动调试器。我开始浏览文档资料,以便寻找读取转储文件的方法,并且最终遇到了有关 WinDBG 实际上是调试引擎上层的外壳程序的叙述。我断定如果我能够获得该调试引擎的接口,就可以容易地编写一个工具来转储非常有用的资料。在 WinDBG 安装中藏有一个名为 SDK 的节点,但该节点没有设置为在默认情况下安装。我将它设置为“将安装在本地硬盘上”,并且得到了 DBGENG.DLL 的头文件和库,即调试器引擎。



bugslayer0206fig04

4 安装 WinDBG SDK




如 果您观察图 4(该图显示了您为安装 WinDBG SDK 而需要完成的工作),则您将注意到没有 Documentation 的安装节点。使用 DBGENG.DLL 之所以有趣,是因为唯一的文档资料是头文件 DBGENG.H 中的注释节。在很大程度上,这些注释使您可以开始工作,但在提供完整的文档资料之前,您将必须花费一些时间来试用参数,以便了解某些 API 期待什么内容(请参见 5)。奇怪的是,该接口看起来好像完全基于 COM。当它使用接口时,它根本不使用 OLE32.DLL。请将 API 视为伪 COM。从以下意义上说来,它也是伪 COM:您将承受引用计数的所有辛苦,却得不到枚举数及类似功能的任何好处。

该 接口的另一个问题是它实质上是 WinDBG 的内部接口。一些接口和方法用显然是内部 WinDBG 格式的格式返回项。此外,该引擎输出大量文本消息,可能使您的应用程序看起来就像 WinDBG Command 窗口一样(如果您不加以取消)。总之,存在调试引擎这一事实充分弥补了接口中的怪异功能。在 5 中,我只列出了派生程度最大的接口,因为似乎“2”接口是最新的且最完整的接口。因为您无法调用调试引擎接口上的 CoCreateXxx,所以 DBGENG.DLL 导出了两个函数:DebugConnect 和 DebugCreate,以便为您创建特定的接口。

开始使用调试引擎的最佳方式是编译 SDK 安装随附的 DUMPSTK 示例并认真地逐句通过该示例。唯一的问题是它无法正常工作。DUMPSTK 应该为转储文件转储调用堆栈。我不知道为什么代码无法按预期方式工作,以至于几乎要发疯了。

让 调试引擎启动的关键方法是 IDebugControl::WaitForEvent。每当 DUMPSTK 调用它时,它总是返回 E_INVALIDARG。因为它只采用两个无符号的 long 类型参数(指示您正在等待的诸如初始断点之类的内容以及要等待的时间的标志),所以我被完全搞糊涂了。慢慢我终于明白了,DBGENG.DLL 是在抱怨没有同时设置映像路径和符号路径。我设置了环境变量 _NT_IMAGE_PATH,感觉它可能被获得,突然之间 IDebugControl::WaitForEvent 便开始工作了。再也没有像返回与实际错误没有任何关系的值一样的事情了。

一旦我让 DUMPSTK 缓慢运行之后,证明了它是有用的。它足够小以至于您完全可以接受它,但它实际上完成了非常方便的工作。而且,我建议您花一点儿时间阅读完整的 DBGENG.H 头文件。正如您可以从 5 中的列表看到的那样,您在通过调试引擎解决问题时可能需要的信息分布在多个接口中。

当 我首次开始考察调试引擎时,我可以看到所有种类的非常棒的调试和分析实用工具,我将在我的商业程序在客户站点崩溃时编写这些工具。好消息是, DBGENG.DLL 是 Windows XP 和 Windows .NET Server 操作系统的一部分。要在 Windows 2000 上合法地使用它,您的客户必须下载完整的 WinDBG 软件包,并且将其安装在他们的计算机上。

故障转储信息转储器

既 然我已经讨论了调试引擎的接口,我希望介绍一下我编写的 DMPINFO 程序。我一直希望有一个能够将用户模式故障转储中的重要信息告诉我的程序。当我在 Visual Studio .NET 和 WinDBG 中打开用户模式故障转储时,我总是执行相同的操作,因此我希望自动执行这些操作。DMPINFO 还是一个有关如何使用调试引擎的接口的完整得多的示例。

使 用 DMPINFO 非常简单;只须在命令提示窗口中键入 DMPINFO,后面跟您要转储的用户模式故障转储文件。DMPINFO 输出用户模式故障转储中的系统信息、故障转储中的已加载模块、崩溃线程的寄存器、崩溃线程的反汇编以及带有所有本地变量的调用堆栈。如果您希望看到所有线 程,请在命令行上传递 -a。您还可以传入特定的源路径、符号路径以及映像路径。在观察 DMPINFO 输出时,您可能会注意到,即使您具有 PDB 文件,模块符号类型也将是 Document Interchange Architecture (DIA)。DBGENG.H 定义了 DIA 符号类型,而 DIA 似乎是 Visual Studio .NET 的新符号格式。然而,所有 PDB 符号都被报告为 DIA。

我用 DBGENG.DLL 版本 4.00.0018.0 编写了 DMPINFO。在 DBGENG.DLL 中有两个错误,您可以从 DMPINFO 中看到。系统信息值看起来不正确,并且有时不显示堆栈作用域的本地符号。如果您要运行 DMPINFO 的调试版本,则您将看到一个断言消息框。出于某种原因,DBGENG.DLL 停止调用 IDebugOutputCallbacks 接口,因此 DMPINFO 无法显示本地符号。稍后我将详细讨论该问题。

实际上,编写 DMPINFO 花费了我大量的时间,因为我必须花费如此之多的时间来进行试用版和错误开发。DBGENG.H 中的文档资料还不错;它只是不够完整。因此,我必须总是尝试传入不同的参数,以便获得我需要的结果。您将在 DMPINFO.CPP 中看到的断言比您曾经看到的任何程序都要多,因为我需要在某个部分失败后立即知道。

我遇到的第一个问题是调试引擎输出了太多的内容,以至于 带来了麻烦。我设置了我自己的接口 IBetterDebugOutputCallbacks(它派生自 IDebugOutputCallbacks),以便我可以筛选掉我不希望看到的调试引擎输出。您可以看到我的可下载源示例中提供的 OUTPUT.H 和 OUTPUT.CPP 中的工作。值得庆幸的是,输出似乎全部发生在加载故障转储的时候,因此我可以关闭输出,直至我加载完所有内容为止。可以在 DMPINFO 的命令行上使用 -v 命令行开关,以便看到所有输出。

我遇到的下一个问题是,从编程角度而言,似乎没有方法可以确定所加 载的符号是否与二进制文件不匹配。当您加载故障转储时,调试引擎将输出不匹配,以便引擎了解该不匹配。我希望 Microsoft 将向 IDebugSymbols 中添加一个方法,或者向 DEBUG_MODULE_PARAMETERS 结构中添加一个新字段,以便您可以找到不匹配。

我 的 DMPINFO 的目标是:说明如何在不使用某些接口的某些简单方法的情况下完整所有工作。这样,您将具有更为强大的示例,并且将了解自己如何应用上述技术。当我需要完成 DMPINFO 的反汇编部分时,我必须承认我无能为力。要向后反汇编到 IA32 汇编语言是不可能的(因为指令是变长的),所以我并未盼望能够琢磨出一种算法以将一切排列起来,以便我可以在指令指针前面显示 15 条指令。IDebugControl->OutputDisassemblyLines 的输出不是我所需要的,因为我无法添加一个小的指针前缀以指示指令指针。输出只是一块文本而已。OutputDisassemblyLines 将完成所有工作以查找反汇编中的指令起始位置,并且将它们作为数组返回。当我看到 OutputDisassemblyLines 能够为我完成这一工作时,我便孤注一掷了。我关闭了输出,调用了 OutputDisassemblyLines 以便我可以获得所有指令起始位置的偏移量,然后调用了 IDebugControl->Disassemble 以便我可以将代码行格式化为我需要的格式。

我花费了无数时间来努力完成 DMPINFO 的最后部分:获得本地符号。第一个问题是:我无法想出如何获得在我设置作用域之后加载的本地符号。在调用 IDebugSymbols->SetScope 之后,我可以看出我需要调用 IDebugSymbols->GetScopeSymbolGroup。当我调用 IDebugSymbolGroup->GetNumberSymbols 时,我总是回到有零个符号的情况。在几乎要放弃之后,我最后询问 Microsoft 如何获得本地符号。您必须将 "*" 字符串传递给 IDebugSymbolGroup->AddSymbols,才能获得加载到 IDebugSymbolsGroup 中的本地符号。您可以在 DMPINFO.CPP 的 OutputScopeSymbols 函数中观察所有这些功能的工作方式。

在我加载了本地符号之后,我想我 终于走上正轨了。这时,我遇到了当前 IDebugSymbolGroup 接口的最大问题:无法枚举本地符号值。您可以调用 IDebugSymbolGroup->GetSymbolName 来获取符号索引的名称。但缺少两个方法:GetSymbolType 和 GetSymbolValue。您可以通过间接方式获得类型,方法是调用 IDebugSymbolGroup->GetSymbolParameters 以获取符号的 DEBUG_SYMBOL_PARAMETERS 结构。该结构中有一个 TypeId 字段,您可以对其调用 IDebugSymbols->GetTypeName(注意,它是一个不同的接口)。这样可以获得信息的三分之二,但它不含关键值。我调用了 IDebugSymbolGroup->OutputSymbols,而这的确输出了所有符号信息,但采用的是下面的这种非常奇怪的格式:

**NAME****VALUE****OFF****TYPE**

调试引擎以这种格式输出所有符号,这些符号首尾相连填塞到一个巨大的字符串中。我尤其喜欢使用常见值“*”(请想一想指针)作为分隔符 这一事实。因为没有其他获得值的方法,所以我必须捕获该字符串,并对其进行分析以显示这些符号。我当然希望将来版本的调试引擎能够弥补这一疏忽。

小结

安 装符号服务器是如此重要,因此我敦促您立即停止阅读,并且为您的组织安装一个符号服务器。它将使您的调试工作变得容易得多。而且,借助于 Visual Studio .NET 和 WinDBG 中的新的故障转储处理,消除错误也会变得更加容易。最后,我希望我能够帮助您克服一些我在开始使用 DBGENG.DLL 时遇到的相同的障碍。尽管它可能有一些奇特之处,但它仍然是一个不断进步的作品,并且随着时间的推移将会变得更好。我建议您考虑一下可能性,并且开始创建 一些您一直在盼望的调试工具。

另外,如果你想得到更详细的信息和示例代码可以参看以下链接:

http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/WBS0206bugslayer.mspx?mfr=true

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值