65.9K
CodeProject 正在变化。 阅读更多。
Home

使用CrashRpt库为您的应用程序添加崩溃报告

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (83投票s)

2003 年 1 月 12 日

BSD

11分钟阅读

viewsIcon

773499

downloadIcon

9362

本文介绍如何使用 CrashRpt 库为您的应用程序生成崩溃报告,以便使用 WinDbg 或 VS.NET 进行调试。

CrashRpt 项目托管在 http://code.google.com/p/crashrpt/

图 1 - 主对话框

目录

概述

如果您曾经负责调试致命异常,您可能知道仅凭用户重现问题的步骤来调试有多困难。许多因素,例如应用程序的版本、用户的操作系统以及依赖的模块,都可能导致最终崩溃。这使得复制用户的环境,进而复制崩溃,对于除最明显的错误之外的所有错误来说,几乎是不可能的。

CrashRpt 库是一个轻量级的错误处理框架。该模块将拦截您的应用程序生成的任何未处理异常,构建完整的调试报告,并可选择地将报告邮寄给您。

CrashRpt 项目托管在 http://code.google.com/p/crashrpt/

用法

在本文中,崩溃报告是指一系列旨在帮助开发人员快速诊断崩溃原因的文件。具体来说,崩溃报告包括一个小型转储文件、一个崩溃日志以及由应用程序通过崩溃回调提供的最多十个额外的特定于应用程序的文件。其中最有用的大可能是应用程序的小型转储文件。小型转储文件包含调用堆栈、局部变量、所有应用程序模块的详细信息,甚至可以帮助精确定位生成异常的源代码行号。

CrashRpt DLL 的工作方式类似于 XP 中附带的新 Dr. Watson 工具。它会拦截未处理的异常,创建小型转储文件,构建崩溃日志,提供一个界面允许用户查看崩溃报告,最后它会压缩并可选择地将崩溃报告通过电子邮件发送给您。

检测到未处理异常时,CrashRpt 会通知用户并允许他们查看报告内容。首先显示上面所示的主对话框。从这里,用户可以输入他们的评论和电子邮件地址,或者通过单击超链接查看报告内容。这将带他们到详细信息对话框,在那里他们会看到构成报告的文件。双击文件名将以其关联的程序打开该文件,前提是存在该文件类型的关联。

图 2 - 崩溃详细信息对话框

一旦用户满意,他就可以关闭详细信息对话框,并在主对话框上单击“发送”按钮,这将通过电子邮件将完整的崩溃报告发送给您。

在您的应用程序中使用 CrashRpt 库

构建库

下载并解压本文附加的源代码。打开位于顶层目录中的 `complete.dsw` 工作区。此工作区包含两个示例应用程序和 CrashRpt 库的源代码。您只需要构建 CrashRpt 项目 - *crashrpt.dsp*。

注释

  1. CrashRpt 库在其 GUI 组件中使用 WTL,因此为了构建 CrashRpt 库,必须安装并正确配置 WTL。您可以在 此处 下载 WTL。如果您是 WTL 新手,CodeProject 有一些文章 在此处,并且有一个很好的入门指南 在此处
  2. CrashRpt 库使用 Microsoft 的 Debug Help 库 (*dbghelp.dll*)。为了构建 CrashRpt 库,必须安装并正确配置正确的头文件和库文件。这些文件可在 Debugging SDK 中找到,您可以 此处 下载(请务必选择 SDK 安装选项)。

构建完成后,您应该得到以下结果:

crashrpt\include
crashrpt.h 

crashrpt\bin\debug or crashrpt\bin\release
crashrpt.dll
 
crashrpt\lib
crashrpt.lib 

crashrpt\src
[all source files...]

链接到库

要隐式链接到库,您需要包含 *crashrpt.h* 文件并链接 *crashrpt.lib* 文件。我发现最简单的方法是将以下两行添加到我的主应用程序文件中。

#include "[whateveryourpath]/crashrpt/include/crashrpt.h"
#pragma comment(lib, "[whateveryourpath]/crashrpt/lib/crashrpt")
图 3 - 将 CrashRpt 与您的应用程序集成

初始化库

库需要在捕获任何异常之前进行初始化。您可以通过调用 `Install` 方法来完成此操作,通常是在您的主函数中。`Install` 方法的详细信息如下。

//-----------------------------------------------------------------------------
// Install
//    Initializes the library and optionally set the client crash callback and
//    set up the email details.
//
// Parameters
//    pfn         Client crash callback
//    lpTo        Email address to send crash report
//    lpSubject   Subject line to be used with email
//
// Return Values
//    If the function succeeds, the return value is a pointer to the underlying
//    crash object created.  This state information is required as the first
//    parameter to all other crash report functions.
//
// Remarks
//    Passing NULL for lpTo will disable the email feature and cause the crash 
//    report to be saved to disk.
//
CRASHRPTAPI 
LPVOID 
Install(
   IN LPGETLOGFILE pfn OPTIONAL,                // client crash callback
   IN LPCTSTR lpTo OPTIONAL,                    // Email:to
   IN LPCTSTR lpSubject OPTIONAL                // Email:subject
   );
图 4 - Install() 函数

所有参数都是可选的。第一个参数是指向崩溃回调函数的指针,该函数定义为

// Client crash callback
typedef BOOL (CALLBACK *LPGETLOGFILE) (LPVOID lpvState);
图 5 - 客户端崩溃回调

仅当您想收到应用程序失败的通知以便执行一些基本清理(例如,关闭数据库连接,尝试保存等)时,才需要定义此回调。否则,您可以简单地为此参数传递 `NULL`。

第二个参数定义了您希望将崩溃报告邮寄到的电子邮件地址,如果希望将报告保存到用户的工作站,则为 `NULL`。

第三个参数是生成的邮件消息中使用的主题行。

`Install` 函数返回一个指向实现该库实际功能的底层对象的指针。此值是库后续所有调用的必需参数。

如果您在调用 `Install` 后决定取消挂钩 CrashRpt 库,则可以调用 `Uninstall`。

//-----------------------------------------------------------------------------
// Uninstall
//    Uninstalls the unhandled exception filter set up in Install().
//
// Parameters
//    lpState     State information returned from Install()
//
// Return Values
//    void
//
// Remarks
//    This call is optional.  The crash report library will automatically 
//    deinitialize when the library is unloaded.  Call this function to
//    unhook the exception filter manually.
//
CRASHRPTAPI 
void 
Uninstall(
   IN LPVOID lpState                            // State from Install()
   );
图 6 - Uninstall 函数

您仅在调用 `Install` 后决定不希望 CrashRpt 库拦截异常时才会调用 `Uninstall`。所以,基本上您可能永远不会直接调用此方法。

将自定义文件添加到报告

客户端应用程序可以随时通过调用 `AddFile` 函数将文件提供给崩溃报告,要求将其包含在内。

//-----------------------------------------------------------------------------
// AddFile
//    Adds a file to the crash report.
//
// Parameters
//    lpState     State information returned from Install()
//    lpFile      Fully qualified file name
//    lpDesc      Description of file, used by details dialog
//
// Return Values
//    void
//
// Remarks
//    This function can be called anytime after Install() to add one or more
//    files to the generated crash report.
//
CRASHRPTAPI 
void 
AddFile(
   IN LPVOID lpState,                           // State from Install()
   IN LPCTSTR lpFile,                           // File name
   IN LPCTSTR lpDesc                            // File desc
   );
图 7 - AddFile 函数

当您的应用程序使用或生成外部文件(例如初始化文件或日志文件)时,这非常有用。生成报告时,它将包含这些附加文件。

手动生成报告

您可以通过调用 `GenerateErrorReport` 来强制生成报告。如果您想提供一种简单的方法来收集应用程序的调试信息以帮助调试非致命错误,这会很有用。

//-----------------------------------------------------------------------------
// GenerateErrorReport
//    Generates the crash report.
//
// Parameters
//    lpState     State information returned from Install()
//    pExInfo     Pointer to an EXCEPTION_POINTERS structure
//
// Return Values
//    void
//
// Remarks
//    Call this function to manually generate a crash report.
//
CRASHRPTAPI 
void 
GenerateErrorReport(
   IN LPVOID lpState,
   IN PEXCEPTION_POINTERS pExInfo OPTIONAL
   );
图 8 - GenerateErrorReport 函数

如果您未提供有效的 `EXCEPTION_POINTERS`,则小型转储的调用堆栈可能会不完整。

生成调试符号

为了充分利用小型转储,调试器需要您的应用程序的调试符号。默认情况下,发布版本不生成调试符号。您可以通过更改几个项目设置来配置 VC 为发布版本生成调试符号。

在选择了发布版本配置后,在“C/C++”选项卡下的“常规”类别中,在“调试信息”下选择“程序数据库”。

图 9 - C++ 项目设置

在“链接器”选项卡下的“常规”类别中,选中“生成调试信息”选项。

图 9 - 链接器项目设置

现在,发布版本将在 PDB 文件中生成调试符号。为每个发送给客户的发布版本保留所有可执行文件和 PDB 文件。您将需要这些文件才能在调试器中读取小型转储文件。

使用崩溃报告

使用崩溃日志

崩溃日志是一个 XML 文件,它描述了崩溃的详细信息,包括异常类型、发生异常的模块和偏移量,以及一些粗略的操作系统和硬件信息。我编写崩溃日志是为了更容易地编目崩溃。可以通过模块、偏移量和异常代码唯一地标识崩溃。开发人员或自动化过程可以检查此信息,并与先前报告的问题进行比较。如果找到匹配项,开发人员或自动化过程可以通知用户解决方案,而无需再次调试错误。

该日志分为四个不同的部分或节点。第一个节点是我们之前讨论过的 `ExceptionRecord`。

图 10 - ExceptionRecord 节点

接下来是 `Processor` 节点,其中包含有关用户 CPU 的一些信息。

图 11 - Process 节点

接下来是 `OperatingSystem` 节点,其中包含用户的操作系统版本信息。

图 12 - OperatingSystem 节点

最后是 `Modules` 节点。此节点包含已终止应用程序加载的每个模块的路径、版本、基地址、大小和时间戳。

图 13 - Modules 节点

使用崩溃转储文件

崩溃转储文件是一个小型转储文件,在 `DbgHelp` DLL 的 `MiniDumpWriteDump` 函数的帮助下创建。小型转储包含错误发生时应用程序状态的各种信息,包括调用堆栈、局部变量和已加载的模块。有关创建小型转储文件的更多信息,请参阅 Andy Pennell 的 文章

您可以在 VS.NET 或 WinDbg 调试器中查看小型转储文件。由于 WinDbg 是免费的,我将在以下示例中使用它。我在示例中使用的是版本 6.1.0017.0。

本文随附的示例应用程序所做的就是生成一个空指针异常。我将使用该示例生成一个崩溃并演示如何使用生成的小型转储文件。

运行示例应用程序时,单击炸弹按钮以生成空指针异常,并保存生成崩溃报告。然后从崩溃报告中提取 *crash.dmp* 文件,启动 WinDbg,然后按 CTRL+D 打开崩溃转储文件。

接下来,您需要使用 `.sympath` 命令设置 WinDbg 的符号路径。切换到命令窗口 (ALT+1),然后输入 `.sympath`,后跟一个空格,然后是分号分隔的搜索目录列表。

.sympath c:\downloads\CrashRptTest
图 14 - 设置符号路径

同样,您需要使用 `.exepath` 和 `.srcpath` 命令设置可执行文件和源文件搜索路径。

.exepath c:\downloads\CrashRptTest
.srcpath c:\downloads\CrashRptTest
图 15 - 设置源文件和可执行文件路径

最后一步是通过输入 `.ecxr` 命令将调试器上下文更改为与异常相关的上下文记录。

.ecxr
图 15 - 设置异常上下文记录

如果一切配置正确,您现在应该能够遍历调用堆栈,查看局部变量和已加载的模块。您甚至可以通过在“调用堆栈”窗口 (ALT+6) 中双击 `CrashRptTest` 帧来让 WinDbg 突出显示出错的代码行。注意:由于链接器优化,确切的行号可能会有点偏差。

图 16 - 应许之地:使用 WinDbg 定位空指针异常的根源

部署

CrashRpt 库依赖于几个可再发行库。为了确保库可以访问所需文件,您可以分发我包含的 ZLib 和 DbgHelp。

文件版本 描述
CrashRpt.DLL 3.0.2.1 崩溃报告库
ZLib.DLL 1.1.3.0 ZLib 压缩库
DbgHlp.DLL 6.1.17.1 Microsoft 调试帮助库
图 17 - 可再发行库

要分发什么以及要保存什么

正如我之前提到的,要调试崩溃,您不仅需要小型转储文件,还需要构成您应用程序的符号和可执行文件。在准备要发布给客户的版本时,您应始终保存您分发给客户的确切可执行模块以及相应的调试符号。这样,当收到崩溃报告时,您将拥有调试器需要正确解释小型转储的模块和调试符号。

注意:我收到了关于分发调试版本或调试符号的几条评论/问询。您永远不应分发调试版本或调试符号,因为它们不仅会占用 CD/下载/客户工作站上的更多空间,而且还会使逆向工程您的代码变得轻而易举。需要明确的是,我建议的是修改您的发布版本配置,使其生成调试符号,将您的模块的发布版本及其相应的调试符号保存在您的源代码控制系统中,并且仅将您的模块的发布版本分发给客户(就像您今天一样)。当收到崩溃报告时,您将使用您存档的发布版本和调试符号,以及崩溃报告中包含的小型转储文件来调试崩溃。

注意:CrashRpt 使用 Microsoft 的 Debug Help 库 (*dbghelp.dll*)。该库随 Windows XP 一起发布,但某些版本是可再发行的。我建议您将 *crashrpt.dll* 与包含在源代码/演示附件中的 *dbghelp.dll* 文件一起安装到应用程序目录中,以避免可能的冲突或缺失的依赖项问题。

关于首选基加载地址的说明

每个可执行模块(EXE、DLL、OCX 等)都有一个首选基加载地址。这是加载器将尝试映射该模块的应用程序进程空间中的地址。如果两个或多个模块列出了相同的基加载地址,则加载器将被迫重新定位模块,直到每个模块在唯一的地址加载。这不仅会减慢应用程序的启动时间,而且还会使调试致命异常成为不可能。为了使用小型转储文件,您必须确保应用程序模块不会发生冲突。您可以使用 *rebase.exe* 或手动覆盖每个冲突模块的首选基加载地址。无论哪种方式,您都需要确保应用程序模块始终在相同的地址加载,这样小型转储文件才有用。有关这方面的信息,您可以在 John Robbin 于 1998 年 4 月的 MSJ 文章中找到。

参考资料

有关与本文直接相关的主题的更多信息,请参阅以下链接

更改历史

  • 03/17/2003

    重大更改。

    • 用 WTL 替换 MFC。
    • 更改了 crashrpt 接口。
    • 大规模重构。
    • 更新了文章。

    次要更改。

    • 详细信息对话框预览窗口现在使用系统定义的窗口颜色而不是白色。
    • ZIP 文件未保存目录结构。

    已修复的错误

    • 支持多个应用程序使用。
    • 预览文件大于 32k 时发生缓冲区溢出错误。
    • 主对话框现在显示应用程序图标。
  • 01/12/2003
    • 初始发布。
© . All rights reserved.