今天本來打算在谷歌上搜索處理SEH的文章,以使我不須要在每個線程中使用__try{}__except()代碼塊包裹代碼的狀況下,就能在任意線程拋出SEH時生成MiniDump文件。不過最後的結果是處理SEH的文章沒有搜索出幾篇,卻幸運的搜索出了知足我須要的工具crashrpt。php
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。html
爲了使用crashrpt,咱們首先須要在https://code.google.com/p/crashrpt上下載crashrpt的最新版本,在我寫這篇文章時的最新版本是1.3.0。下載解壓後的目錄以下圖所示:windows
其中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,則出現如圖所示的輸出:api
在列表框中選擇和你的vc版本匹配的選項,最後點擊Gnerate按鈕,就能夠生成與你vc版本相匹配的解決方案和工程文件了。使用vc打開生成的解決方案文件,在這裏我使用的vc版本是vc9,該解決方案中有下圖所示的工程:app
在解決方案處點擊右鍵菜單中的build,便可生成crashrpt。ide
下面讓咱們來看一下如何使用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("myapp_support@hotmail.com"); 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("myapp_support@hotmail.com"); 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進行更深刻的學習。