使用 Minidumps 和 Visual Studio .NET 进行崩溃后调试

本文转自http://vicchina.51.net/research/other/seh/minidumps/intro.htm

 

本文讲述了 minidumps 是怎样工作的、当你的程序崩溃的时候应该如何生成它们、以及如何在 Visual Studio .NET 中将它们重新读入。

原文作者:Andy Pennell
中文翻译:Victor
原文链接:http://www.codeproject.com/debug/postmortemdebug_standalone1.asp

如果你的程序在客户的机器上崩溃了,那么你现在可以使用 minidumps 和 Microsoft Visual Studio .NET 调试器在事后进行调试。本文讲述了 minidumps 是怎样工作的、当你的程序崩溃的时候应该如何生成它们、以及如何在 Visual Studio .NET 中将它们重新读入。微软的错误报告程序之所以能够改进 Windows 操作系统和诸如 Visual Studio .NET 这样的应用程序的稳定性,其关键就在于 minidumps。本文也讲述了如何使用 Microsoft 符号服务器自动地为系统组件查找符号。你在阅读本文前应该已经熟悉 Win32 和 C++ 编程了。

目录
  • 什么是 Minidump?
  • 创建一个 Minidump
  • 与 Build 相关的问题
  • 使用 MiniDumpWriteDump 写一个 Minidump
  • 使用 Visual Studio .NET 读入一个 Minidump
  • 微软是如何使用 Minidumps 的
  • 进一步改进
  • 总结
  • 附:版权及免责声明
什么是 Minidump?

一个 minidump 就是一个文件,它包括了崩溃的应用程序中最重要的部分。它在用户的机器上生成,然后用户就可以将其提交给开发人员。开发人员可以装入这个 dump,以便查找崩溃的原因,并发布一个补丁。

从早期的 Windows NT 开始,Dr.Watson 程序就能够生成以 .dmp 为扩展名的崩溃 dump 文件。但是它们却没有想像中那样有用,因为有两个问题:

  1. 它们太大了。一个应用程序的 dump 包含了整个进程空间中的每一个字节,所以即使是 Notepad 这么简单的程序崩溃后也会产生几兆的 dump 文件,如果是 Word 这样的应用程序崩溃了的话,可能会产生上百兆的 dump 文件。
  2. 它们所包含的内容并不都是有用的。事实上,Dr.Watson 是一个 just-in-time(JIT)调试器,但调试器很难得到一个已加载模块的完整路径。完整的调试器,例如 Visual Studio 调试器,为了获得这些路径进行了很多额外的工作,但是 Dr.Watson 却没有。这通常就会导致毫无意义的模块名,如 MOD0000 等等。

Minidumps 在设计时就使用了几个方法用来解决上述问题:

  • 只有几个区域被保存下来,而不是整个进程空间。保存诸如 Kernel32.dll 这样的模块简直毫无意义;只要给出了版本号,就可以很容易地从一张 Windows CD 上拿到这个文件。默认情况下,程序的内存堆是不保存的;不需要调试一个崩溃比率高得令人乍舌的崩溃点。当然,如果你想的话,还是可以把堆保存下来的。
  • Minidump 生成代码可以得到精确、完整的模块信息,包括它们的名字、路径、版本信息和内部时间戳。
  • Minidump 生成代码还可以得到线程列表,包括它们的上下文(也就是寄存器集合)和堆栈中的内容。
  • 整个文件是压缩的,进一步减小了大小。在 Windows XP 上,一个 Notepad 的 minidump 大概在 6K 左右,比上面提到的同一进程的崩溃 dump 小了将近 300 倍。

注意:Windows XP 在计算机停止响应后也会生成一种内核态的 minidumps,但是本文讨论的是更常见的用户态 minidumps。

创建一个 Minidump

有三种方法可以创建一个 minidump:

  • 在你自己的应用程序中添加代码,在遇到一个未捕获的异常时写 minidump。
  • 在 Visual Studio .NET 的集成开发环境中调试程序,单击“调试”菜单上的“将转储另存为”。
  • 什么也不做。

方法一将在下文中详细讨论。

方法二仅适用于一个已经装了调试器的工作站,很有可能只在开发团队内才有用(例如:在另一个开发者或者测试人员的机器上)。如果你使用 Visual Studio .NET 对崩溃进行调试,那么你就可以这么做了。你可以保存一个 Minidump 或者一个包含堆的 Minidump。你不需要任何符号或者 PDB 文件就可以保存一个 dump 文件;但是在将它们重新读入时你需要。

方法三只在 Windows XP 下有效,如果程序遇到了一个未捕获的异常,并且没有启动 JIT 调试器,系统就会自动创建一个 minidump。然后,这个 minidump 将直接提交给 Microsoft,你就没有机会查找崩溃的原因了。

与 Build 相关的问题

为了使你的程序在崩溃时创建 dumps,你必须配置你的生成选项使其生成完整的调试信息,特别是在生成最终版的时候。生成 PDB 文件之后,你必须将你要发布给用户的每一个二进制文件及其对应的 PDB 文件进行归档;之后在调试那些用户提交的 minidumps 时你将需要这些 PDB 文件。

此外,确保你的二进制文件中包含正确的版本信息。你所发布的每一个组件的每一个版本都应该有一个不同的版本号,以便你能够与 minidump 对应起来。版本字段(二进制资源中的版本信息——译者注)可以帮你匹配这两者。但调试器本身并不使用版本信息,它是基于 PE 文件头内包含的内部时间戳来匹配二进制文件的。

输出时,为最终版生成调试信息会有一些小的影响。会生成一个 PDB 文件,在生成所使用的机器上占用一些空间,而且最终生成的二进制文件会增大几百个字节以便在 PE 文件中记录调试目录下的 PDB 文件名。你不应该向用户发布 PDB 文件;这会使你的程序更容易地被用户反向工程。

使用 MiniDumpWriteDump 写一个 Minidump

用来保存一个 minidump 的关键 API 是 MiniDumpWriteDump,它从 DbgHelp.dll 中导出,这是一个 Platform SDK 中可以再分发的 DLL。确保你使用的是 Windows XP 版本 5.1.2600;更早期的 beta 和 release candidate 版中的这个 API 有问题,并且 Windows 2000 中包含的 5.0.x 版中没有导出这个函数。如果你有比 5.0 更早的版本,那么它一定是来自于 System Debugger package(包括 WinDbg 等工具),并且它是不可再分发的。

在调用这个 API 之前,你需要用 SetUnhandledExceptionFilter API 设置一个未捕获异常的处理器(Unhandled Exception Handler,也就是 Top-level Exception Filter,最后一个异常过滤器——译者注),以便能够捕获到崩溃点。这样就可以使这个 Filter 函数在程序遇到未捕获的异常的时候被调用。在某些未捕获的异常中,比方说两次堆栈溢出(double stack fault,即在处理 stack fault 的时候又发生了 stack fault——译者注),操作系统会立即结束应用程序,既不会调用 filter、也不会启动 JIT 调试器(据查,网上资料说如果发生了 triple fault,CPU 将会关闭,到时候除了 Reset 信号,就没有什么能救得了这台机器的了。所以可以认为 double fault 是非常严重的情况——译者注)。

在你的 filter 函数中,你需要加载 DbgHelp.dll。这并不是调用 LoadLibrary 这么简单;在 Windows 2000 系统中,你将会访问到 System32 目录下的那个不正确的版本。演示代码试着从 EXE 文件所在的位置装入这个库。将正确版本的 DbgHelp.dll 放在 EXE 文件所在的目录下;否则的话,代码只好进行一次普通的 LoadLibrary 调用,这也就使得程序只能在 Windows XP 下工作。

装入 DLL 后,它接着检查导出函数名;如果正确的话,它会用合适的名字创建一个文件,比方说用程序名加 .dmp 后缀保存在临时目录中。这个文件句柄接着会传递给 API,并附上一些其它的信息,诸如进程 ID 和所需的 dump 文件类型等。例子中使用的是 MiniDumpNormal。也许你想使用 MiniDumpWithDataSegs 标志位,也就是相当于 Visual Studio 调试器中的“附带堆信息的小型转储”选项,这显然会使 dump 文件变得更大。

在 .dmp 文件创建完成后,程序会询问用户将其保存在什么位置。然后用户就可以通过 e-mail 或者使用 FTP 将文件发送过来供你分析。

如果你想使用本文所提供的演示代码的话,就在你的工程中加入 mdump.h 和 mdump.cpp 文件,并声明一个全局的 MiniDumper 对象。这个对象的构造函数需要一个参数,也就是 minidump 文件的基础文件名。为了能够正常运行,你还需要把正确的 DbgHelp.dll 放在 EXE 所在的目录下。

不能使用调试器调试写 minidump 的那段代码(在演示代码中,也就是 Minidumper::TopLevelFilter)。如果进程附加了一个调试器,那么未捕获异常的处理函数将永远不会被调用(关于这一点,MSDN 中关于结构化异常处理的文档说得很清楚:调试器有两次机会处理一个异常,一次是在异常刚刚发生时,也就是 VC6 经常报出来的“First-chance exception”;还有一次是在执行了所有的 filter,发现没有一个能够处理之后,通知调试器发生了一个“Second-chance exception”,此时调试器会中断程序并进入调试模式,因为绝大多数情况下这就属于未捕获的异常,是程序 BUG,在非调试环境下是要崩溃的——译者注)。如果你遇到了问题需要调试的话,你需要使用 MessageBox 调试。

使用 Visual Studio .NET 读入一个 Minidump

文章的这一部分使用了一个例子,在 Windows 2000 系统下手工创建了 Notepad 的一个 minidump,然后在 Windows XP 系统下调试。

启动 Visual Studio .NET,单击“文件”菜单上的“打开解决方案”,在“文件类型”下拉列表框中选择“转储文件(*.dmp; *.mdmp)”(555~~~为什么我在 Visual Studio .NET 2003 中找不到这一项……不过还好,可以直接双击 .dmp 文件打开——译者注),找到 minidump 文件,然后点击“打开”创建一个缺省工程。

按 F5 在调试器中启动这个 dump,这一步将为你开始调试提供一些信息。调试器将创建一个假的进程;在输出窗口中显示了很多模块加载的消息。此时调试器仅仅是在重建崩溃时的进程状态。在显示了一条 EXE 不包含调试信息的警告消息之后,调试器停在了用户崩溃的地方,诸如一个非法访问什么的。这时候如果你查看调用栈窗口,你会发现缺少符号和很多有用的信息。

图 1:起初没有符号文件的堆栈窗口

为了读取一个 minidump,你通常需要相关的二进制拷贝。为了找到正确的二进制文件,打开模块窗口。

图 2:起初没有二进制文件的模块窗口

 

图 2 展示了 Notepad 的例子,并且说明了两个情况。首先,二进制所在的路径名前标上了一个星号,这表示这些是在用户机器上的模块路径,但是在本地却找不到对应的二进制文件。其次,在“Information”一列中全都写着“No matching binary found”。找到对应的二进制文件的关键是注意“Version”字段和文件名。在这个例子中,大多数系统文件的版本号都是 2195,也就是 Windows 2000。虽然无法从这些信息上立即得知确切的 service pack (SP) 或者 quality fix engineering (QFE),但这些信息可以从微软的 DLL 帮助数据库中查询到:http://support.microsoft.com/servicedesks/fileversion/dllinfo.asp。

现在,你需要找到一张 Windows 操作系统的 CD 或者已经安装了正确版本的机器,然后将所需要的文件复制到一个目录。通常情况下没有必要把进程中每个模块的二进制文件都找出来,但是找出那些在每个调用栈上的关键模块是很重要的。这通常包括操作系统的二进制文件(例如 Kernel32.dll)和你自己的二进制模块(在这个例子中就是 Notepad.exe)。

在你找到这些二进制文件、并把它们拷贝到一个本地目录之后,单击“调试”菜单中的“停止调试”命令。然后在解决方案资源管理器中,右键单击工程图标,在快捷菜单上单击“属性”,你将看到“调试”属性页。在“命令参数”中填入“MODPATH”,跟上一个等号,然后输入二进制文件所在的位置,如果有多个位置,可以用分号分隔。在这个例子中,它是:

MODPATH=m:/sysbits

在设置好了路径之后,按 F5 重新装入 minidump,MODPATH 的值将通过命令行参数传递给调试器;在 Visual Studio .NET 的后续版本中应该会有更方便的方法设置这个参数,或许可以作为一个选项出现在属性对话框中。

尽管找到二进制文件并不太可能改善调用栈窗口的情况,但是它却能解决模块窗口中的问题,如图 3 所示:

图 3:找到二进制文件后的模块窗口

 

它现在显示的不再是“No matching binary found”,而是“Cannot find or open a required DBG file”(我怎么觉得这个地方的英文语法应该用“nor”而不是“or”……呵呵,不管它了,反正微软程序中无伤大雅的语法错误已经不是第一次被发现了——译者注)和“No symbols loaded”。前一条消息出现在那些使用 DBG 文件存储调试信息的系统 DLL 上,后一条消息则出现在使用 PDB 文件的 DLL 上。找到对应的二进制文件并不能使你看到调用栈;你还需要找到它们对应的调试信息。

方法 A:坎坷之路

为了完整地分析一个 minidump,你需要找到所有的调试信息。但为了节省时间,你可以只找那些你需要的信息。本例中的调用栈列表包含了 User32.dll 和 Kernel32.dll,所以需要它们的调试信息。

对应的调试信息
操作系统 所需的文件
Windows NT 4 DBGs
Windows 2000 DBGs, PDBs
Windows XP PDBs

一个找系统符号的好地方在 http://www.microsoft.com/ddk/debugging,你也可以在 Windows NT Server 和 Windows 2000 Server 操作系统的 Support CD 上找到系统符号。在本例中,它们被拷贝到了二进制代码所在的位置。实际情况中你可能会遇到非微软发布的二进制模块,这时候你就需要它们的 PDB 文件了。同样在本例中,Notepad 的 DBG 和 PDB 文件也被拷贝了出来,因为它是我们使用的样本应用程序。

在单击“调试”菜单上的“停止调试”命令后再按 F5 就会看到调用栈列表,如图 4 所示。你也许发现了,由于添加了新的二进制文件和调试信息,调用栈发生了变化。这就是我们要的结果;只有在具有调试信息的情况下才能准确地回溯一个调用栈,提供的信息越详细,你得到的堆栈就越精确,通常能够把那些原来没有显示出来的栈帧信息暴露出来。

在本例中并没有崩溃。在实际情况中,这些信息应该已经能够帮助你查找出大概 70% 的崩溃原因。另外,本例中的调用栈列表是使用微软随系统组件提供的、经过删减的符号文件所生成的,所以没有行号信息。如果你使用自己生成的、完整的 PDB 文件,你可以看到一个更详尽的调用栈。

图 4:找到符号和二进制文件后的调用栈窗口

符号服务器

如果你需要处理大量 minidumps,进行大范围调试,那么存储和访问所有的二进制文件以及 PDB/DBG 文件将变得很困难。Windows NT 中包含了一种叫做符号服务器的技术,起初只是用来存储符号文件的,后来扩展到也支持二进制文件的查找。Windows NT 调试器是第一个支持它的工具,但实际上 Visual Studio .NET 也支持它,虽然没有文档提及(事实上,在 Visual Studio .NET 2003 的文档中和 MSDN 的 Knowledge Base (KB) 中都有讲到如何使用符号服务器,KB 中的 Q319037 和 Q311503 分别讲述了如何在 Visual Studio .NET 2002 和 Visual Studio 6.0 中使用符号服务器,甚至还包括了一个详细无比的、傻瓜式的教学视频。在 MSDN 中搜索关键字“symsrv.dll”可以找到这部分内容——译者注)。关于符号服务器,可以参考 http://www.microsoft.com/ddk/debugging/symbols.asp。

方法 B:康庄大道——使用符号服务器

首先,去 http://www.microsoft.com/ddk/debugging 下载调试工具。你需要安装 Symsrv.dll 文件,你可以将它拷贝到 devenv.exe 所在的目录下,或者放到你的 System32 目录下,以便 Visual Studio .NET 可以访问到它。在复制了 Symsrv.dll 文件之后,你就可以安全地卸载调试工具了。你还需要创建一个本地目录,在本例中,创建了一个本地目录 C:/localstore。

在工程属性对话框中的“调试”属性页上,填写“符号路径”:

SRV*c:/localstore*http://msdl.microsoft.com/download/symbols

这个字符串会告诉调试器使用符号服务器来获取符号文件,并在本地创建一个符号服务器,用来存放符号文件。现在,当你在 minidump 工程中按下 F5 后,符号文件将从微软的网站上拷贝到本地服务器。在第一次下载后,之后的加载速度就会快很多,因为符号文件将从本地服务器中直接加载,不再需要通过 Web 下载了。

在调试微软之外的程序时,你应该将方法 A 和方法 B 结合起来。用方法 A 获得系统组件的符号文件(反了吧?好像应该用方法 B 吧……不过原文确实是“Use A to get the system components”——译者注),然后附上你自己的符号文件路径,用分号将它们隔开,例如:

c:/drop/build/myapp;SRV*c:/localstore*http://msdl.microsoft.com/download/symbols

由于符号服务器是 Visual Studio .NET 中的一个没有文档说明的特性,所以没有错误报告。如果表达式错误或者 Symsrv.dll 文件的位置不正确,符号文件就不能被加载,只能在模块窗口中显示“No symbols loaded”的错误信息。你也可以使用符号服务器存储和下载二进制文件,但是 MODPATH 表达式需要使用“symsrv*symsrv.dll*”而不是“SRV*”(MSDN 中对于“srv”的解释是:“This is shorthand for symsrv*symsrv.dll.”——译者注)。

注意:微软的符号服务器不包含二进制文件,但是你自己创建的符号服务器却可以。

符号服务器不仅仅是用来调试 minidumps 的,它提供了一种“在线”调试的方法。在使用符号服务器之前,别忘了正确地配置“调试”属性页上的“符号路径”选项。

微软是如何使用 Minidumps 的

微软使用 minidumps 改进其程序的历史已经有一年多了(本文发布于 2002 年 3 月 7 日——译者注)。Microsoft Internet Explorer 5.5 和 Microsoft Office XP 是第一批与新版的 Dr.Watson 同时发布的产品。这个新版的 Dr.Watson 可以在程序停止响应时捕获程序中未处理的异常,创建一个 minidump,然后询问用户是否愿意将信息提交给微软。

进一步改进

在服务器端,可以根据发生崩溃的组件和崩溃点对 minidumps 进行分析和归类,这样就可以使产品组得到程序的崩溃频率、以及某个崩溃情况的发生频率,小组也可以得到崩溃时的 minidumps 以便日后分析。在某些情况下,如果崩溃的原因已经完全查明,则用户可以被引导到一个网页,这个页面上有已知的、可以暂时避免这种崩溃的方法,或者一个解决该问题的补丁。在 Internet Explorer 5.5 和 Office XP 发布后,有很多其他产品组已经开始使用类似的技术收集崩溃信息了。它同时也是 Windows XP 的一个标准部分。

本文中讨论的例子主要是用来理解 minidumps 的,没有涉及到如何从用户机器上返回 dump 文件(在 CodeProject 还有一篇文章是专门讲这个处理过程的,也就是著名的 BT 客户端软件 BitComet 所使用的崩溃信息报告程序 XCrashReport——译者注)。在最简单的情况下,可以提示用户使用 e-mail 发送 minidump 文件。但是要注意,这可能会带来隐私方面的问题,因为用户数据也许就存储在于堆栈中,对于一个包含完整堆信息的 minidump 来说,则一定会有用户数据在其中,这一点要让用户清楚。微软的 Data Collection Policy 中有一些额外的信息用来说明 minidumps 所包含的所有详细内容,它位于:http://watson.microsoft.com/dw/1033/dcp.asp。

另外,如果在一个 Windows 服务中遇到了未处理的异常,那么为它创建 minidumps 将是另一个挑战。你需要处理桌面访问(例如,如果没有人在使用控制台,那么你就无法提示他们)以及安全上下文。

总结

Minidumps 是一种新技术,它使得程序在用户的机器上崩溃后也可以进行事后调试。向已有的程序中加入代码、使其在遇到未捕获的异常时自动创建 minidumps 也是一件很容易的事。Visual Studio .NET 可以很容易地载入它们,从而重现崩溃现场、使得开发人员可以调试程序。符号服务器可以很轻松地找到系统符号文件,帮助分析。

你可能感兴趣的:(调试,MFC,.net,windows,microsoft,服务器,exception,微软)