C++ 崩溃收集和分析解决方案 (二):CrashRpt API 使用
下面的示例展示如何使用 CrashRpt API 函数和结构在控制台 C++ 应用程序中启用崩溃报告支持。为了简单起见,我们使用控制台应用程序,但通常应用程序也可以是基于 WinAPI/MFC/ATL/WTL 的。
1.配置项目的生成设置
编译完 CrashRpt 后,需要修改你的项目设置。
1.1.包含目录和库目录
要让 Visual C++ 编译器和链接器知道 CrashRpt include 和 lib 文件的位置,请执行以下操作。
打开项目属性管理器窗口,多选解决方案中的所有项目,然后右键单击选择项。在出现的对话框中,打开配置属性 -> VC++ 目录。
- 在 “Show directory for” 组合框中,选择 “Include files”,然后将 <CRASHRPT_HOME>/include 目录的路径添加到列表中。
- 在 “Show directory for” 组合框中,选择 “Library files”,然后将 <CRASHRPT_HOME>/lib 目录的路径添加到列表中。
在这里,<CRASHRPT_HOME> 应该替换为你解压缩 CrashRpt 发行版到的目录的实际路径。
在开始在程序中使用 CrashRpt API 函数之前,不要忘记在代码开头包含 CrashRpt.h 头文件:
// Include CrashRpt header
#include "CrashRpt.h"
还需要将 CrashRpt 库文件添加到项目的输入库列表中。在“解决方案资源管理器”窗口中,右键单击项目并从上下文菜单中选择“属性项”。然后打开配置属性->链接器->输入->附加依赖项,然后添加 CrashRptLIB.lib。
1.2. 使用 CRT 作为 Multi-Threaded DLL (/MD) 在 Release 配置
注意:MT 不需要配置此项。
重要的是,配置你的项目使用 C 运行时库(CRT)作为 Multi-threaded DLL (/MD) 进行 Release 配置。这是MSDN中推荐的方式。有关更多信息,请参见异常处理和CRT链接部分。
在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。然后选择配置属性->C/C++ ->代码生成。在 Runtime Library 字段中,选择 Multi-threaded DLL (/MD)。这应该对解决方案中的所有项目执行,以共享单个 CRT DLL。
1.3.对所有应用程序模块使用相同版本的 CRT
确保应用程序中存在的所有模块使用相同版本的 CRT。如果某些依赖模块是使用较旧版本的 CRT 编译的,则应该重新编译它们,以确保使用单一版本或 CRT DLL。
1.4.在 Release 配置中启用 Program Database (/Zi, /DEBUG)
为了能够从 crash minidump 中恢复堆栈跟踪,调试器需要应用程序的调试符号(PDB文件)。
启用 PDB 文件的生成的步骤是:
- 在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。然后选择 Configuration Properties->C/C++->General. 在 Debug Information Format 字段,选择 Program Database (/Zi)。
- 选择配置属性- >链接器- >调试。在 Generate Debug Info 字段中,选择 Yes (/DEBUG)。
应该对解决方案中支持程序数据库 (EXE, DLL)的所有项目执行步骤1和步骤2。
1.5.禁用帧指针遗漏优化
我们建议在发布版本配置中关闭省略帧指针(FPO)优化,因为这种优化并没有真正带来可观的收益,但是极大地使转储的分析复杂化:/Oy 编译器选项使使用调试器更加困难,因为编译器抑制了帧指针信息。此外,在 Visual Studio 2010 中,默认情况下该优化是禁用的。
禁用 省略帧指针(FPO )优化的步骤是:
- 在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。
- 选择配置属性 -> C/C++ ->优化。
- 在 Omit Frame Pointers 字段,选择 No (/Oy-)。
2.CrashRpt API 使用示例
首先创建一个控制台 Win32 应用程序并将其命名为 MyApp。然后按照上面配置项目的构建设置中所述配置 MyApp 应用程序。
让我们假设 MyApp 应用程序有两个线程。第一个线程,即应用程序线程,将是主线程。main()
函数将在这个线程中执行,与用户的交互也将在这个线程中执行。第二个线程是工作线程,通常用于在不阻塞应用程序线程的情况下完成一些耗时的计算工作。
MyApp 应用程序将创建一个日志文件,该文件将包含在崩溃时的错误报告中。在调试崩溃时,日志文件通常很有帮助。
让我们创建代码模板。
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
FILE* g_hLog = NULL; // Global handle to the application log file
// The following function writes an entry to the log file
void log_write(LPCTSTR szFormat, ...)
{
if (g_hLog == NULL)
return; // Log file seems to be closed
va_list args;
va_start(args);
_vftprintf_s(g_hLog, szFormat, args);
fflush(g_hLog);
}
// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
log_write(_T("Entering the thread proc\n"));
// Define the infinite loop where some processing will be done
for(;;)
{
// There is a hidden error somewhere inside of the loop...
int* p = NULL;
*p = 13; // This results in Access Violation
}
log_write(_T("Leaving the thread proc\n"));
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
// Open log file
errno_t err = _tfopen_s(&g_hLog, _T("log.txt"), _T("wt"));
if(err!=0 || g_hLog==NULL)
{
_tprintf_s(_T("Error opening log.txt\n"));
return 1; // Couldn't open log file
}
log_write(_T("Started successfully\n"));
// Create the worker thread
HANDLE hWorkingThread = CreateThread(NULL, 0,
ThreadProc, (LPVOID)NULL, 0, NULL);
log_write(_T("Created working thread\n"));
// There is a hidden error in the main() function
// Call of _tprintf_s with NULL parameter
TCHAR* szFormatString = NULL;
_tprintf_s(szFormatString);
// Wait until the worker thread is exited
WaitForSingleObject(hWorkingThread, INFINITE);
log_write(_T("Working thread has exited\n"));
// Close the log file
if(g_hLog!=NULL)
{
fclose(g_hLog);
g_hLog = NULL;// Clean up handle
}
// Exit
return 0;
}
我们故意插入了会在两个线程中引起异常的代码。在实际的程序中,这样的代码总是存在的,即使在非常仔细地测试应用程序时也是如此。
为了在应用程序中启用崩溃报告支持,我们需要在代码的开头包含 CrashRpt.h 头文件:
// Include CrashRpt Header
#include "CrashRpt.h"
接下来,我们定义崩溃回调函数并将其命名为 CrashCallback()
。崩溃发生时,CrashRpt 将调用崩溃回调函数,因此我们将能够关闭日志文件的句柄。有关崩溃回调的更多信息,请参见 PFNCRASHCALLBACK() 原型。
// Define the callback function that will be called on crash
int CALLBACK CrashCallback(CR_CRASH_CALLBACK_INFO* pInfo)
{
// The application has crashed!
// Close the log file here
// to ensure CrashRpt is able to include it into error report
if(g_hLog!=NULL)
{
fclose(g_hLog);
g_hLog = NULL;// Clean up handle
}
// Return CR_CB_DODEFAULT to generate error report
return CR_CB_DODEFAULT;
}
因为我们这是一个多线程应用程序,所以需要使用一些 CrashRpt 函数来为工作线程设置异常处理程序。在本例中,我们分别使用 crInstallToCurrentThread2()
和 crUninstallFromCurrentThread()
函数在线程过程开始时设置异常处理程序,并在线程过程结束时取消设置。
// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// Install exception handlers for this thread
crInstallToCurrentThread2(0);
...
// Unset exception handlers before exiting the thread
crUninstallFromCurrentThread();
return 0;
}
接下来,在 main()
函数开始时,我们初始化 CrashRpt 库,并在 crInstall()
函数的帮助下为整个过程安装异常处理程序,并通过 CR_INSTALL_INFO
结构体将配置信息传递给它。如果出现错误,我们可以使用 crGetLastErrorMsg()
函数检查最后一条错误消息。
int _tmain(int argc, _TCHAR* argv[])
{
// Define CrashRpt configuration parameters
CR_INSTALL_INFO info;
memset(&info, 0, sizeof(CR_INSTALL_INFO));
info.cb = sizeof(CR_INSTALL_INFO);
info.pszAppName = _T("MyApp");
info.pszAppVersion = _T("1.0.0");
info.pszEmailSubject = _T("MyApp 1.0.0 Error Report");
info.pszEmailTo = _T("myapp_support@hotmail.com");
info.pszUrl = _T("http://myapp.com/tools/crashrpt.php");
info.uPriorities[CR_HTTP] = 3; // First try send report over HTTP
info.uPriorities[CR_SMTP] = 2; // Second try send report over SMTP
info.uPriorities[CR_SMAPI] = 1; // Third try send report over Simple MAPI
// Install all available exception handlers
info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS;
// Restart the app on crash
info.dwFlags |= CR_INST_APP_RESTART;
info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS;
info.pszRestartCmdLine = _T("/restart");
// Define the Privacy Policy URL
info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html");
// Install crash reporting
int nResult = crInstall(&info);
if(nResult!=0)
{
// Something goes wrong. Get error message.
TCHAR szErrorMsg[512] = _T("");
crGetLastErrorMsg(szErrorMsg, 512);
_tprintf_s(_T("%s\n"), szErrorMsg);
return 1;
}
接下来,我们想通过调用 crSetCrashCallback()
来设置崩溃回调函数。
// Set crash callback function
crSetCrashCallback(CrashCallback, NULL);
接下来,我们想要将错误日志文件添加到崩溃报告中。我们使用 crAddFile2()
函数来实现这一点。
// Add our log file to the error report
crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY);
当应用程序崩溃时,我们可以在崩溃报告中包含屏幕截图。我们使用 crAddScreenshot2()
函数来实现这一点。
// We want the screenshot of the entire desktop is to be added on crash
crAddScreenshot2(CR_AS_VIRTUAL_SCREEN, 0);
下面的代码将一个命名属性添加到崩溃描述 XML 文件中(请参阅 crAddProperty()
函数)。这个属性告诉终端用户的计算机上安装了什么图形适配器(为了简单起见,它是硬编码的,但是你通常使用 Windows 管理工具动态地确定适配器的模型)。
// Add a named property that means what graphics adapter is
// installed on end user's machine
crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS"));
在 main()
函数的最后,我们使用 crUninstall()
函数来初始化 CrashRpt 并解除异常处理程序的设置。
// Uninitialize CrashRpt before exiting the main function
crUninstall();
下面是我们的完整代码:
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
// Include CrashRpt Header
#include "CrashRpt.h"
FILE* g_hLog = NULL; // Global handle to the application log file
// Define the callback function that will be called on crash
int CALLBACK CrashCallback(CR_CRASH_CALLBACK_INFO* pInfo)
{
// The application has crashed!
// Close the log file here
// to ensure CrashRpt is able to include it into error report
if(g_hLog!=NULL)
{
fclose(g_hLog);
g_hLog = NULL;// Clean up handle
}
// Return CR_CB_DODEFAULT to generate error report
return CR_CB_DODEFAULT;
}
// The following function writes an entry to the log file
void log_write(LPCTSTR szFormat, ...)
{
if (g_hLog == NULL)
return; // Log file seems to be closed
va_list args;
va_start(args);
_vftprintf_s(g_hLog, szFormat, args);
fflush(g_hLog);
}
// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// Install exception handlers for this thread
crInstallToCurrentThread2(0);
log_write(_T("Entering the thread proc\n"));
// Define the infinite loop where some processing will be done
for(;;)
{
// There is a hidden error somewhere inside of the loop...
int* p = NULL;
*p = 13; // This results in Access Violation
}
log_write(_T("Leaving the thread proc\n"));
// Unset exception handlers before exiting the thread
crUninstallFromCurrentThread();
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
// Define CrashRpt configuration parameters
CR_INSTALL_INFO info;
memset(&info, 0, sizeof(CR_INSTALL_INFO));
info.cb = sizeof(CR_INSTALL_INFO);
info.pszAppName = _T("MyApp");
info.pszAppVersion = _T("1.0.0");
info.pszEmailSubject = _T("MyApp 1.0.0 Error Report");
info.pszEmailTo = _T("myapp_support@hotmail.com");
info.pszUrl = _T("http://myapp.com/tools/crashrpt.php");
info.uPriorities[CR_HTTP] = 3; // First try send report over HTTP
info.uPriorities[CR_SMTP] = 2; // Second try send report over SMTP
info.uPriorities[CR_SMAPI] = 1; // Third try send report over Simple MAPI
// Install all available exception handlers
info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS;
// Restart the app on crash
info.dwFlags |= CR_INST_APP_RESTART;
info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS;
info.pszRestartCmdLine = _T("/restart");
// Define the Privacy Policy URL
info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html");
// Install crash reporting
int nResult = crInstall(&info);
if(nResult!=0)
{
// Something goes wrong. Get error message.
TCHAR szErrorMsg[512] = _T("");
crGetLastErrorMsg(szErrorMsg, 512);
_tprintf_s(_T("%s\n"), szErrorMsg);
return 1;
}
// Set crash callback function
crSetCrashCallback(CrashCallback, NULL);
// Add our log file to the error report
crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY);
// We want the screenshot of the entire desktop is to be added on crash
crAddScreenshot2(CR_AS_VIRTUAL_SCREEN, 0);
// Add a named property that means what graphics adapter is
// installed on user's machine
crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS"));
// The main code follows...
// Open log file
errno_t err = _tfopen_s(&g_hLog, _T("log.txt"), _T("wt"));
if(err!=0 || g_hLog==NULL)
{
_tprintf_s(_T("Error opening log.txt\n"));
return 1; // Couldn't open log file
}
log_write(_T("Started successfully\n"));
// Create the worker thread
HANDLE hWorkingThread = CreateThread(NULL, 0,
ThreadProc, (LPVOID)NULL, 0, NULL);
log_write(_T("Created working thread\n"));
// There is a hidden error in the main() function
// Call of _tprintf_s with NULL parameter
TCHAR* szFormatString = NULL;
_tprintf_s(szFormatString);
// Wait until the worker thread is exited
WaitForSingleObject(hWorkingThread, INFINITE);
log_write(_T("Working thread has exited\n"));
// Close the log file
if(g_hLog!=NULL)
{
fclose(g_hLog);
g_hLog = NULL;// Clean up handle
}
// Uninitialize CrashRpt before exiting the main function
crUninstall();
// Exit
return 0;
}
不要忘记添加 CrashRptXXXX.lib 文件到项目的输入库列表中(XXXX 是 CrashRpt 版本的占位符)。有关更多信息,请参见上篇文章,配置项目的生成设置。
在运行应用程序之前,你应该将以下文件放置到你的应用程序可执行文件所在的目录中:
- CrashRptXXXX.dll (这里和下面的XXXX应该替换为CrashRpt的实际版本)
- CrashSenderXXXX.exe
- dbghelp.dll
- crashrpt_lang.ini
复制文件后,运行应用程序。当错误发生时,你应该能够看到一个错误报告窗口。
3.国际化支持
当程序崩溃时,你看到的错误报告窗口默认语言应是英文的。CrashRpt 支持多语言用户界面。为了使 CrashRpt 正常工作,你应该将一个名为 crashrpt_lang.ini的有效语言文件放置到 CrashSender.exe 文件所在的目录中。
还可以选择使用 CR_INSTALL_INFO::pszLangFilePath
结构成员指定定制语言文件名。该语言文件是 UNICODE 格式的文本文档,具有 INI 扩展名。语言文件包含 CrashRpt 对话框使用的本地化字符串。
最简单的快速的办法就是复制 <CRASHRPT_HOME>/lang_files 目录下的 crashrpt_lang_ZH-CN.ini 文件中的内容到 crashrpt_lang.in 中即可。