linux, windows, mac 的c/c++程序使用 breakpad 来进行崩溃汇报
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文件。
支付宝扫一扫捐赠
微信公众号: 共鸣圈
欢迎讨论,邮件: 924948$qq.com 请把$改成@
QQ群:263132197
QQ: 924948