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
", 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文件夹到breakpadsrc ools文件夹下

然后,生成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:
....clientwindowsunittests......	estingsrcgmock-all.cc
....clientwindowsunittests......	estinggtestsrcgtest-all.cc
....clientwindowsunittests......	estingsrcgmock_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
", id);
    }
    else {
      printf("dump failed
");
    }
    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?
");
  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 "--------
"

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 "clientwindowshandlerexception_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

良辰美景补天漏,风雨雷电洗地尘
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原文地址:https://www.cnblogs.com/welhzh/p/14821817.html