XCrashReport:异常处理和崩溃报告——第一部分






4.97/5 (64投票s)
为您的应用程序添加基本的异常处理和崩溃报告
引言
对于开发者来说,调试已投入生产或已交付给客户的程序是一项巨大的挑战。在开发者的工作站上,程序运行良好。但在客户的系统上,却会出现随机崩溃。由于距离遥远,通常无法直接访问客户的系统。写入事件日志或其他日志文件可能有所帮助,但只能指示一个方向,而不能给出精确的位置。我当时的情况就是这样,直到我读了 Bruce Dawson 关于发布模式调试的论文。Dawson 的论文讨论了几种我以前从未遇到过的技术,包括如何捕获崩溃时的指令指针 (ip),以及如何将 ip 输入 VC++ 并直接定位到崩溃的源代码行。(我这里说的是 VC++ 6.0,不是 .Net)。
这些新技术引导我走向了开发者们梦寐以求的圣杯:能够看到导致崩溃的每个函数的堆栈跟踪。在过程中,我曾好几次想:“嗯,这已经很完善了,没什么可以再添加的了。” 但随后我又发现另一种方法,另一种我忽略的 API,于是我继续探索。
Bruce Dawson 的技术
Dawson 方法的关键在于为发布版本生成调试符号 (我稍后会讨论如何做到)。然后,每当您发布新版本时,都会将 pdb 文件与 exe 文件一起存档。有一点我之前不知道:您可以在 DevStudio 中打开一个 exe 文件,单步进入它,输入一个 ip,您将立即看到源代码行。当然,前提是您拥有与该 exe 对应的 pdb 文件。哦,对了,您还需要指令指针 (ip)。这是我的第二个启示。在 Dawson 的文章中,有一个链接指向他曾在 Game Developer Magazine 上发表过的一些源代码。代码包含一个异常处理器,可以在崩溃时捕获 ip、系统信息和堆栈。最棒的是,还有一个可以包含在任何 MFC 应用程序中的代码,它会自动调用 Dawson 的异常处理器。以下是 Dawson 添加异常处理器到任何 MFC 应用程序的详细步骤:
准备工作
- 设置您的发布版本以生成调试符号 (pdb)
- 将这些文件包含在您的项目中
- 重新编译整个项目。
- 在您的 VC++ 项目中,转到 **Project | Settings**。确保左侧的 **Settings For** 组合框中选择了 **Release** 配置。转到 **C/C++** 选项卡,选择 **General** 类别,然后在 **Debug Info** 组合框中选择 **Program Database**。这会告诉编译器生成调试信息。
- 转到 **Link** 选项卡,勾选 **Generate debug info**。这会告诉链接器将调试信息汇编到 .pdb 文件中。链接器还会将 .pdb 文件的名称放入可执行文件中,以便调试器可以找到它。
- 在同一个 **Link** 选项卡上,在 **Project Options** 列表的末尾输入 **/OPT:REF**。这会告诉链接器删除未被引用的函数和/或数据。这通常是发布版本的默认设置,但当您告诉链接器生成调试信息时,它会被关闭。如果不指定 **/OPT:REF**,您的可执行文件和 DLL 会增大 10-20%。
- ExceptionAttacher.cpp
- ExceptionHandler.cpp - 应在 **C/C++** 选项卡 (Precompiled Headers) 下设置为 **Not using precompiled headers**。
- ExceptionHandler.h
- GetWinVer.cpp
- GetWinVer.h
- MiniVersion.cpp
- MiniVersion.h
- CrashFileNames.h
将 exe 和 pdb 文件妥善保存。**不要**将 pdb 文件分发给客户 - 这既不必要,也可能对想要逆向工程您的程序的人有所帮助。
理论付诸实践
当您的应用程序崩溃时,它现在将调用一个异常处理器,该处理器将 ip、系统信息和堆栈写入名为 ERRORLOG.TXT 的文件。在下载文件中有一个名为 **Test1** 的示例项目,演示了如何使用 Dawson 的异常处理器。以下是 ERRORLOG.TXT 开头部分的内容:
Test1 caused an Access Violation (0xc0000005)
in module Test1.exe at 001b:00402cc0. <=== HERE IS THE IP
Exception handler called in ExceptionAttacher.cpp - AfxWinMain.
Error occurred at 10/18/2003 19:05:08.
D:\temp1\XCrashReportTest\1.1\Test1\Release\Test1.exe, run by hdietrich.
Operating system: Windows XP (5.1.2600).
1 processor(s), type 586.
32% memory in use.
1024 MBytes physical memory.
687 MBytes physical memory free.
2462 MBytes paging file.
2253 MBytes paging file free.
2048 MBytes user address space.
2033 MBytes user address space free.
Write to location 00000000 caused an access violation.
Context:
EDI: 0x0012fe70 ESI: 0x004043c0 EAX: 0x00000000
EBX: 0x00000001 ECX: 0x0012fe70 EDX: 0x00000000
EIP: 0x00402cc0 EBP: 0x0012f82c SegCs: 0x0000001b
EFlags: 0x00010246 ESP: 0x0012f820 SegSs: 0x00000023
Bytes at CS:EIP:
c7 05 00 00 00 00 00 00 00 00 c3 90 90 90 90 90
Stack:
0x0012f820: 73dd23d8 004043c0 00000111 0012f85c .#.s.C@.....\...
0x0012f830: 73dd22ae 0012fe70 000003e8 00000000 .".sp...........
0x0012f840: 00402cc0 00000000 0000000c 00000000 .,@.............
0x0012f850: 00000000 0012fe70 000003e8 0012f880 ....p...........
0x0012f860: 73dd8fc5 000003e8 00000000 00000000 ...s............
0x0012f870: 00000000 000003e8 0012fe70 00000000 ........p.......
0x0012f880: 0012f8d0 73dd2976 000003e8 00000000 ....v).s........
0x0012f890: 00000000 00000000 0012fe70 0012fe70 ........p...p...
.
.
.
好的,现在我们有了 ip,以及 exe 和它的 pdb 文件。下一步是启动 DevStudio,然后转到 **File | Open** 并浏览到您 exe 的发布版本 (在本例中是 ..\Test1\Release\Test1.exe)。接下来单击 **Step Into (F11)**。您现在应该会看到这个:
转到 **View | Debug Windows | Registers**。您将看到 Registers 窗口。
现在点击 EIP 寄存器十六进制值的前面,输入崩溃的 ip (从 ERRORLOG.TXT 中,我们知道这是 **00402cc0**)。您不能复制粘贴 - 必须手动输入。输入时,更改后的地址将以红色显示。
输入完成后,按 Enter 键,您将看到这个:
摘要
我们仅凭一个信息——崩溃的 **指令指针**——就从崩溃的应用程序跳转到了 (近似的) 源代码行。我们相当简单地获得了这个崩溃 ip——无需修改任何现有源代码。成本:发布应用程序 (Test1.exe) 的大小从 21 KB 增加到 29 KB。对于当今大多数商业应用程序来说,这种大小差异微不足道。知道应用程序崩溃在哪里很重要,但有时您也需要知道它是如何到达崩溃位置的。这将在 第二部分 中讨论。
非 MFC 应用程序
Dawson 的 **ExceptionHandler.cpp** 也可以在非 MFC 应用程序中使用。这是一个示例,展示了如何在 Win32 应用程序中使用它。int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { int nResult = -1; __try { nResult = DoSomeStuff(hInstance, hPrevInstance, lpCmdLine, nCmdShow); } __except(RecordExceptionInfo(GetExceptionInformation(), "WinMain")) { // Do nothing here - RecordExceptionInfo() has already done // everything that is needed. Actually this code won't even // get called unless you return EXCEPTION_EXECUTE_HANDLER from // the __except clause. } return nResult; }
修订历史
版本 1.1 - 2003 年 10 月 19 日
- 首次公开发布
用法
本软件已进入公共领域。您可以随意使用它,但不得销售本源代码。如果您修改或扩展它,请考虑将新代码发布在此处供大家分享。本软件按“原样”提供,不附带任何明示或暗示的保证。对于本软件可能造成的任何损害或业务损失,本人概不负责。