crashpad是一個支持mac和windows的崩潰報告庫,google還有一個breakpad,已經不建議使用了。編譯 crashpad 只能用 gn 來生成 ninja 文件,gn 的下載方法: git clone https://gn.googlesource.com/gn
因此,編譯crashpad同時需要gn和ninja。 ninja的下載地址: https://github.com/ninja-build/ninja
crashpad編譯麻煩,直接使用breakpad吧。
breakpad的代碼見:
https://github.com/google/breakpad
Breakpad是谷歌開源的一個跨平台崩潰處理框架,內含崩潰轉儲、上報、分析一套工作流程框架。
主要的工作流程為:client以library的方式嵌入自己的程序,並設置handler,將會在程序崩潰時將會把一系列的線程列表、調用堆棧和一些系統信息寫入minidump文件。 得到minidump文件后,分析minidump文件可以使用dump_syms將編譯器生成的含調試信息的可執行文件生成符號文件,然后再使用minidump_walker生成可以閱讀的stack trace。
編譯:
1、下載breakpad的代碼;
2、克隆 https://chromium.googlesource.com/linux-syscall-support
3、將 linux-syscall-support 里面的 linux_syscall_support.h 頭文件放到 breakpad/src/third_party/lss/ 目錄下。
4、必須用支持 c++11 的編譯器來編譯breakpad,生成 libbreakpad.a libbreakpad_client.a 庫及一些分析用的 tool,主要使用 dump_syms 和 minidump_stackwalk 兩個工具來分析崩潰報告文件。
崩潰分析過程:
參考: https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/linux_starter_guide.md
示例代碼如下(a.cpp):
#include <unistd.h> #include <thread> #include <iostream> #include "client/linux/handler/exception_handler.h" static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) { printf("Dump path: %s\n", descriptor.path()); return succeeded; } void crash() { volatile int* a = (int*)(NULL); *a = 1; } void task1(string msg) { std::cout << "task1 says: " << msg << std::endl; crash(); } int main(int argc, char* argv[]) { google_breakpad::MinidumpDescriptor descriptor("/tmp/hzh"); google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1); std::thread t1(task1, "Hello"); t1.detach(); sleep(2); return 0; }
編譯,必須用 -g:
$ g++ -g a.cpp -I/home/hzh/soft/softy/breakpad/include/breakpad -L/home/hzh/soft/softy/breakpad/lib -lbreakpad -lbreakpad_client -pthread -o test
1,運行test,會崩潰並產生 179cac63-2e41-4de0-09e8b58c-56069f80.dmp 文件。
2,從可執行程序生成符號表:
$ /home/hzh/soft/softy/breakpad/bin/dump_syms test >> test.sym
3,建立一個目錄結構,目錄名必須為“可執行程序的名字”,然后再該目錄里面建立一個目錄,名字為 test.sym 的第一行的某個數據,具體如下:
$ head -n1 test.sym
得到: MODULE Linux x86_64 A35260606902350047A2A3559926FE410 test ,我們就要 A35260606902350047A2A3559926FE410 作為目錄名。
$ mkdir -p ./symbols/test/A35260606902350047A2A3559926FE410
4,將 test.sym 移動到目錄里:
$ mv test.sym symbols/test/A35260606902350047A2A3559926FE410/
5,開始分析:
$ /home/hzh/soft/softy/breakpad/bin/minidump_stackwalk 179cac63-2e41-4de0-09e8b58c-56069f80.dmp ./symbols
在qt中直接使用 breakpad 原則上可行的,但是這樣breakpad的調用在每個平台上代碼會有些差異,因此可以使用打包過的 QBreakpad 來實現在每個平台上代碼都一樣,代碼見:
https://github.com/buzzySmile/qBreakpad
-------------------------------------------------------------
windows 下怎么使用:
什么是gyp
GYP(Generate Your Projects)是由 Chromium 團隊開發的跨平台自動化項目構建工具,Chromium 便是通過 GYP 進行項目構建管理。
獲取gyp
git clone https://chromium.googlesource.com/external/gyp
安裝gyp
cd gyp python setup.py install
然后,拷貝gyp文件夾到breakpad\src\tools文件夾下
然后,生成Breakpad的sln文件,步驟 :
進入剛剛拷貝的gyp目錄,然后執行:
gyp.bat --no-circular-check "../../client/windows/breakpad_client.gyp"
程序輸出為:
$ gyp.bat --no-circular-check "../../client/windows/breakpad_client.gyp" Warning: Missing input files: ..\..\client\windows\unittests\..\..\..\testing\src\gmock-all.cc ..\..\client\windows\unittests\..\..\..\testing\gtest\src\gtest-all.cc ..\..\client\windows\unittests\..\..\..\testing\src\gmock_main.cc
這里要注意,一定不能使用絕對路徑,要使用相對路徑,所以為什么要拷貝gyp文件夾到tools文件夾下面。
使用vs2015編譯
剛才我看看到了提示,missing幾個文件,所以我們這里不能編譯unittest下的兩個工程,暫時不理會
編譯后,在debug文件夾下會生成:
.
├── common.lib
├── crash_generation_client.lib
├── crash_generation_server.lib
├── crash_report_sender.lib
├── exception_handler.lib
└── processor_bits.lib
然后,使用Breakpad生成dump文件得步驟:
把之前生成的幾個lib,包含進來
common.lib
exception_handler.lib
crash_generation_server.lib
crash_generation_client.lib
頭文件目錄導進來:
breakpad/src/client breakpad/src/client/windows/common breakpad/src/client/windows/crash_generation breakpad/src/client/windows/handler
編寫測試代碼:
#include <cstdio> #include "client/windows/handler/exception_handler.h" namespace { static bool callback(const wchar_t *dump_path, const wchar_t *id, void *context, EXCEPTION_POINTERS *exinfo, MDRawAssertionInfo *assertion, bool succeeded) { if (succeeded) { printf("dump guid is %ws\n", id); } else { printf("dump failed\n"); } fflush(stdout); return succeeded; } static void CrashFunction() { int *i = reinterpret_cast<int*>(0x45); *i = 5; // crash! } } // namespace int main(int argc, char **argv) { google_breakpad::ExceptionHandler eh( L".", NULL, callback, NULL, google_breakpad::ExceptionHandler::HANDLER_ALL); CrashFunction(); printf("did not crash?\n"); return 0; }
編譯可能出現的錯誤:
common.lib(guid_string.obj) : error LNK2038: 檢測到“RuntimeLibrary”的不匹配項: 值“MTd_StaticDebug”不匹配值“MDd_DynamicDebug”(main.obj 中)
解決方法:
就是編譯庫的時候 和現在使用庫的工程 選擇的代碼生成方式不一致:
在工程屬性頁里,選擇: 代碼生成 -> 運行庫, 將運行庫改成“多線程調試DLL(/MDd)
如何根據生成的dump定位錯誤代碼?
文件->打開->文件,找到剛生成的dump文件,然后點擊“使用僅限本機進行調試”
windows 下使用 git 和 breakpad 將可執行文件對應到代碼版本庫以及使用breakpad 將崩潰日志和現場保存起來的比較好的方法:
首先使用git的hook將每次commit和merge的hash版本自動給代碼打上版本,方法如下:
添加兩個git的hook,hook的文件內容一模一樣,名字分別為 post-commit 和 post-merge: (注意,這里沒有考慮 git reset 和 git revert 回退版本帶來的影響,這兩個命令不常用,因此沒考慮它們的解決方案)
#!/bin/bash commit_short_hash=$(git log -1 --pretty=%h) # c7618bf branch_simple=$(git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3) # master echo "--------" echo "${branch_simple}_${commit_short_hash}" echo "--------\n" versionfilename=$(git config hooks.versionfilename) if [[ -z $versionfilename ]] then versionfilename="./YCAISecurity/version_git.h" fi echo -n "static std::string version_hash=\"${branch_simple}_${commit_short_hash}\";" > $versionfilename
就是在commit和merge之后(pull之后如果代碼沒沖突,則一定有個merge;如果pull之后有沖突則沒有merge,但是一定有個commit)自動調用hook產生一個版本文件version_git.h,將這個文件包含在代碼種,可執行文件就可以和代碼版本關聯起來了。以后可執行文件奔潰了之后,就可以通過該hash調出該可執行文件在版本庫里對應的代碼。
然后將version_git.h加入到 .gitignore里,不用跟蹤它,因為每次都會自動生成。
然后在代碼里使用 version_git.h 和 breakpad 將崩潰的日志dump文件對應到該hash。我自己的使用示例如下:
#include "client\windows\handler\exception_handler.h" #include "version_git.h" static bool crash_callback(const wchar_t *dump_path, const wchar_t *id, void *context, EXCEPTION_POINTERS *exinfo, MDRawAssertionInfo *assertion, bool succeeded) { if (succeeded) { QString exeFilePath = QCoreApplication::applicationDirPath(); QString exeFilePathName = QCoreApplication::applicationFilePath(); QString exeName = QFileInfo(exeFilePathName).fileName(); QString exeNameWithoutExtension = QFileInfo(exeFilePathName).completeBaseName(); QString pdbFilePathName = exeFilePath + "/" + exeNameWithoutExtension + ".pdb"; QString dumpPath(QString::fromUtf16(reinterpret_cast<const unsigned short *>(dump_path))); QString exeFileInDumpPath = dumpPath + "/" + exeName; QString pdbFileInDumpPath = dumpPath + "/" + exeNameWithoutExtension + ".pdb"; QFileInfo exeFileInDumpPathExist(exeFileInDumpPath); QFileInfo pdbFileInDumpPathExist(pdbFileInDumpPath); if (!exeFileInDumpPathExist.exists()) QFile::copy(exeFilePathName, exeFileInDumpPath); if (!pdbFileInDumpPathExist.exists()) QFile::copy(pdbFilePathName, pdbFileInDumpPath); } else { qDebug() << "dump failed" << endl; } fflush(stdout); return succeeded; } QString createCrashLogsDir(const wchar_t *dump_path_parent, std::string dump_path_subdir) { QString dumpPathParent(QString::fromUtf16(reinterpret_cast<const unsigned short *>(dump_path_parent))); QString dumpPathSubdir = QString::fromLocal8Bit(dump_path_subdir.c_str()); QString dumpDir = dumpPathParent + "/" + dumpPathSubdir; bool success = QDir().mkpath(dumpDir); return (success ? dumpDir : "."); } int main(int argc, char *argv[]) { const wchar_t *dump_path_parent = L"./crash_logs"; QString dump_path = createCrashLogsDir(dump_path_parent, version_hash); wchar_t wchar_array[128]; dump_path.toWCharArray(wchar_array); google_breakpad::ExceptionHandler eh(wchar_array, NULL, crash_callback, NULL, google_breakpad::ExceptionHandler::HANDLER_ALL); ... }
注意,每次生成可執行文件時,必須提交你所有更改的代碼(不然以后版本check出來的代碼就和你編譯的代碼不一樣),然后再執行生成。
必須將可以行文件和起pdb文件一起拷貝到目標機器,如果不拷貝pdb文件,以后崩潰的時候你都不知道到哪里去找這個文件。
如果可執行文件在客戶那里崩潰,則會在可執行文件目錄里創建 crash_logs/version_hash 目錄,然后將可執行文件和"可執行文件.pdb"一起拷貝到這個目錄,這個目錄還有崩潰時的dmp文件。
