今天原本打算在谷歌上搜索处理SEH的文章,以使我不需要在每一个线程中使用__try{}__except()代码块包裹代码的情况下,就能在任意线程抛出SEH时生成MiniDump文件。不过最后的结果是处理SEH的文章没有搜索出几篇,却幸运的搜索出了满足我需要的工具crashrpt。
crashrpt是一个包含能够在程序出现各种类型未处理异常时生成程序错误报告,然后将该报告按照指定的方式(例如HTTP或者SMTP)发送给开发者,最后分析这些信息的工具。crashrpt由3个部分组成,错误报告生成库CrashRpt,我们需要在自己的程序中使用该库捕获我们的程序没有处理的异常,在该库捕获到这些未处理的异常后,CrashRpt会生成MiniDump文件,并将和你使用该库指定的信息(例如日志文件和屏幕截图等)一起打包成错误报告。CrashRpt库支持处理我所知道的所有Windows C/C++程序抛出的各类异常,例如我前面提到过的SEH,它还能捕获C++异常、信号和调用各类CRT库中的函数出现的错误。异常信息发送工具CrashSender,该工具能够按照我们使用CrashRpt设置的方式,将生成的错误报告按照我们指定的方式(HTTP、SMTP或者MAPI)发送给我们。自动异常信息处理工具crprober,该工具能够在后台接收CrashSender发送给我们的错误报告,通过分析错误报告后以文本的形式输出程序的异常信息。关于crashrpt更详细的介绍,可以参考面https://code.google.com/p/crashrpt/以及http://crashrpt.sourceforge.net/docs/html/getting_started.html。
为了使用crashrpt,我们首先需要在https://code.google.com/p/crashrpt上下载crashrpt的最新版本,在我写这篇文章时的最新版本是1.3.0。下载解压后的目录如下图所示:
其中bin目录中包含使用vc10编译出来的所有crashrpt相关库和程序,include和lib目录中包含了开发所需要的头文件以及lib文件。如果你不介意程序在发布时带上vc10的运行库,或者你的程序就是用vc10开发的,你可以直接使用这些编译好的二进制文件。如果你想crashrpt和你的程序依赖相同的vc运行库,那么你需要使用你的vc重新编译crashrpt。对于使用除vc10之外的其它vc版本的朋友,如果要编译crashrpt,则需要使用开源交叉编译工具cmake,我们需要使用cmake生成和你vc版本相同的解决方案以及工程文件。首先在http://cmake.org/cmake/resources/software.html中下载并安装适合你系统的最新版本cmake。安装完成后,运行cmake-gui,在where is the source code文本框以及where to build the binaries文本框中输入crashrpt的顶层目录,也就是包含文件CMakeLists.txt的目录,然后点击Configure按钮,然后在弹出的对话框中选择你的vc版本并点击finish,则出现如图所示的输出:
在列表框中选择和你的vc版本匹配的选项,最后点击Gnerate按钮,就可以生成与你vc版本相匹配的解决方案和工程文件了。使用vc打开生成的解决方案文件,在这里我使用的vc版本是vc9,该解决方案中有下图所示的工程:
在解决方案处点击右键菜单中的build,即可生成crashrpt。
下面让我们来看一下如何使用crashrpt库来生成错误报告。首先我们需要声明一个CR_INSTALgL_INFO结构体,然后按照自己的需要对其进行设置之后,即可以使用crInstall函数向程序中安装crashrpt中的异常处理函数。调用该函数之后,如果程序中出现了未捕获的异常,则crashrpt会捕获该异常并生成MiniDump文件,关于CR_INSTALgL_INFO结构体的详细描述,请参考http://crashrpt.sourceforge.net/docs/html/struct_c_r___i_n_s_t_a_l_l___i_n_f_o_a.html。除了MiniDump文件之外,我们还可以通过调用 crAddFile2函数够将指定的文件加入到错误报告中,例如我们可以将程序相关的日志文件加入到错误报告中,以便我们更好的分析程序的内部状态,然后通过这些信息更快的找到程序出错的原因;除了能够添加指定的文件以外,我们还能够通过调用 crAddScreenshot函数添加屏幕截图,这样在程序崩溃的时候,我们能够将当时的屏幕截图包含到错误报告中;有时候运行程序的硬件也可能是导致程序崩溃的原因,我们可以通过调用 crAddProperty函数,将自定义信息添加到crashrpt生成的错误报告里包含的xml描述文件中;最后,我们还可以调用crAddRegKey函数将注册表的相关信息包含到错误报告中;请记住在程序结束之前调用crUninstall函数清理crashrpt所使用的相关资源。关于crashrpt使用的更详细介绍,请参考http://crashrpt.sourceforge.net/docs/html/using_crashrpt_api.html
现在让我们来看一个使用crashrpt库的示例MyApp,程序MyApp拥有2个线程,主线程负责与用户进行交互,另外一个工作线程负责处理一些需要花费大量时间才能完成的操作。程序还将创建一个日志文件,我们可以使用crashrpt库在程序崩溃的时候将该文件和MiniDump文件一起打包发送给我们,以便于我们更好地分析出程序崩溃的原因。程序的代码如下所示:
#include <windows.h> #include <stdio.h> #include <tchar.h> #include "CrashRpt.h" <span style="font-family: monospace, fixed;">// 包含crashrpt库使用所需要的头文件 </span> FILE* g_hLog = NULL; // 日志文件句柄 // 程序崩溃时由crashrpt调用的回调函数 BOOL WINAPI CrashCallback(LPVOID /*lpvState*/) { // 需要在这里关闭日志文件句柄,否则crashrpt无法对处于占用状态的文件进行操作 if(g_hLog!=NULL) { fclose(g_hLog); g_hLog = NULL; } // 返回TRUE, 由crashrpt生成错误报告 return TRUE; }
// 日志函数 void log_write(LPCTSTR szFormat, ...) { if (g_hLog == NULL) return; va_list args; va_start(args); _vftprintf_s(g_hLog, szFormat, args); fflush(g_hLog); } // 线程处理函数 DWORD WINAPI ThreadProc(LPVOID lpParam) { // 在该线程中安装crashrpt库对未处理异常的处理
crInstallToCurrentThread2(0); log_write(_T("Entering the thread proc\n")); for(;;) { // 在这里模拟一处内存越界 int* p = NULL; *p = 13; } log_write(_T("Leaving the thread proc\n")); // 清理crashrpt资源 crUninstallFromCurrentThread(); return 0; } int _tmain(int argc, _TCHAR* argv[]) { // 设置crashrpt的各项参数 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("[email protected]"); info.pszUrl = _T("http://myapp.com/tools/crashrpt.php"); info.pfnCrashCallback = CrashCallback; info.uPriorities[CR_HTTP] = 3; // 首先使用HTTP的方式发送错误报告 info.uPriorities[CR_SMTP] = 2; // 然后使用SMTP的方式发送错误报告 info.uPriorities[CR_SMAPI] = 1; //最后尝试使用SMAPI的方式发送错误报告 // 捕获所有能够捕获的异常, 使用HTTP二进制编码的方式传输 info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS; info.dwFlags |= CR_INST_HTTP_BINARY_ENCODING; info.dwFlags |= CR_INST_APP_RESTART; info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS; info.pszRestartCmdLine = _T("/restart"); // 隐私策略URL info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html"); int nResult = crInstall(&info); if(nResult!=0) { TCHAR szErrorMsg[512] = _T(""); crGetLastErrorMsg(szErrorMsg, 512); _tprintf_s(_T("%s\n"), szErrorMsg); return 1; } // 添加日志文件到错误报告中 crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY); // 添加程序崩溃时的截屏到错误报告中 crAddScreenshot(CR_AS_VIRTUAL_SCREEN); // 添加任意的信息到错误报告中,这里以显卡信息作为示例 crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS")); 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")); HANDLE hWorkingThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)NULL, 0, NULL); log_write(_T("Created working thread\n")); TCHAR* szFormatString = NULL; _tprintf_s(szFormatString); WaitForSingleObject(hWorkingThread, INFINITE); log_write(_T("Working thread has exited\n")); if(g_hLog!=NULL) { fclose(g_hLog); g_hLog = NULL; } crUninstall(); return 0; }
crInstallToCurrentThread2(0); log_write(_T("Entering the thread proc\n")); for(;;) { // 在这里模拟一处内存越界 int* p = NULL; *p = 13; } log_write(_T("Leaving the thread proc\n")); // 清理crashrpt资源 crUninstallFromCurrentThread(); return 0; } int _tmain(int argc, _TCHAR* argv[]) { // 设置crashrpt的各项参数 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("[email protected]"); info.pszUrl = _T("http://myapp.com/tools/crashrpt.php"); info.pfnCrashCallback = CrashCallback; info.uPriorities[CR_HTTP] = 3; // 首先使用HTTP的方式发送错误报告 info.uPriorities[CR_SMTP] = 2; // 然后使用SMTP的方式发送错误报告 info.uPriorities[CR_SMAPI] = 1; //最后尝试使用SMAPI的方式发送错误报告 // 捕获所有能够捕获的异常, 使用HTTP二进制编码的方式传输 info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS; info.dwFlags |= CR_INST_HTTP_BINARY_ENCODING; info.dwFlags |= CR_INST_APP_RESTART; info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS; info.pszRestartCmdLine = _T("/restart"); // 隐私策略URL info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html"); int nResult = crInstall(&info); if(nResult!=0) { TCHAR szErrorMsg[512] = _T(""); crGetLastErrorMsg(szErrorMsg, 512); _tprintf_s(_T("%s\n"), szErrorMsg); return 1; } // 添加日志文件到错误报告中 crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY); // 添加程序崩溃时的截屏到错误报告中 crAddScreenshot(CR_AS_VIRTUAL_SCREEN); // 添加任意的信息到错误报告中,这里以显卡信息作为示例 crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS")); 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")); HANDLE hWorkingThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)NULL, 0, NULL); log_write(_T("Created working thread\n")); TCHAR* szFormatString = NULL; _tprintf_s(szFormatString); WaitForSingleObject(hWorkingThread, INFINITE); log_write(_T("Working thread has exited\n")); if(g_hLog!=NULL) { fclose(g_hLog); g_hLog = NULL; } crUninstall(); return 0; }
该示例程序中有几点需要注意的地方:
1.如果想要在错误报告中包含日志文件,请记住一定要使用类似于示例中的CrashCallBack函数来设置CR_INSTALL_INFO中的pfnCrashCallback域,并在函数中关闭该日志文件的句柄。
2.根据我的使用经验,其实不需要在线程中使用crInstallToCurrentThread2/crUninstallFromCurrentThread这一对函数来安装异常处理过程,只要在主线程中调用了crInstall。就能够捕获到程序中所有线程中未处理的异常。
3.调用crInstall出错的原因一般是没有将CrashRptXXXX.dll、CrashSenderXXXX.exe以及crashrpt_lang.ini放到正确的路径中,在默认情况下,该路径即是和应用程序相同的路径。其中的XXXX指的是crashrpt的版本号,这篇文章中的版本号为1300。
关于该示例的原文介绍,请参考http://crashrpt.sourceforge.net/docs/html/simple_example.html
最后发一段我使用crashrpt的代码块,我使用的目的是将程序交给测试人员进行测试时,如果程序崩溃后,crashrpt将程序的错误报告保存到本地,测试人员发现程序崩溃后,将该报告发给我进行调试。代码如下所示:
int main(int argc, char **argv) { #if defined(WIN32) && defined(USE_CRASHRPT) CR_INSTALL_INFO info = {0}; info.cb = sizeof(CR_INSTALL_INFO); info.pszAppName = TEXT("xxx"); info.pszAppVersion = TEXT("0.1.0"); info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS; info.dwFlags |= CR_INST_DONT_SEND_REPORT; info.pszErrorReportSaveDir = TEXT("./xxx"); if (EXIT_SUCCESS != crInstall(&info)) { TCHAR errorMsg[512]; crGetLastErrorMsg(errorMsg, 512); std::cerr << errorMsg; return EXIT_FAILURE; } #endif int ret = mainImpl(argc, argv); #if defined(WIN32) && defined(USE_CRASHRPT) crUninstall(); #endif return ret; }
crashrpt是一个功能很强大的错误报告生成、发送以及分析工具。由于我的使用比较简单,所以我这里介绍的只是crashrpt功能的一小部分,按照crashrpt文档中的描述,crashrpt完全可以在使用http发送错误报告时,与我们所使用的BUG管理系统进行联动,我认为这样可以极大的提升BUG的修改效率,如果对crashrpt有兴趣的朋友,可以参考http://crashrpt.sourceforge.net/docs/html/index.html进行更深入的学习。