Breakpad 是一个库和工具套件,它允许你将应用程序分发给用户,并将编译器提供的调试信息删除,在紧凑的 “minidump” 文件中记录崩溃信息,把它们发送回你的服务器,并从这些 “minidump” 文件中生成C和C++堆栈跟踪。Breakpad 也可以在没有崩溃的项目的请求下生成 “minidump” 文件。

Breakpad 是跨平台的,支持 Windows、Linux、Mac 等操作系统,目前被 Google Chrome、火狐、Google Picasa、Camino、Google Earth 和其他项目所使用。

breakpad

如上图所示是 Breakpad 的工作机制,它参考 Windows 上的溃处理机制。Windows 平台上编译器把代码编译成可执行文件,同时生成包含调试信息的 PDB 符号文件。程序运行发生崩溃时,将崩溃的堆栈等信息存储成一个 dump 文件。调试器打开 dump 文件并读取 PDB 里面的调试信息,就可以看到崩溃的堆栈。因为 Breakpad 是跨平台的,所以在 Linux 等其他平台也实现了 Windows 上的生成调试符号、生成 dump、处理 dump 等功能。

Breakpad 有三个主要组成部分:

  • client 是你的应用程序中包含的一个库。它可以写入 minidump 文件,捕获当前线程的状态和当前加载的可执行文件和共享库的特征。你可以配置客户端,以便在发生崩溃时写入 minidump,或者在显式请求时写入 minidump。
  • symbol dumper 是一个程序,它读取编译器产生的调试信息并生成一个 symbol file,详见下节。
  • processor 是一个读取 minidump 文件的程序,为可执行文件的版本和共享库找到合适的符号文件,并生成易读的 C/C++ 堆栈跟踪。

Symbols 文件格式

给定一个 minidump 文件,Breakpad 处理器会生成包含函数名和源位置的堆栈跟踪。然而,minidump 文件只包含线程寄存器和堆栈的字节数,没有函数名或机器码到源的映射数据。处理器查询 Breakpad symbol 文件,以获取从二进制仅存的 minidump 文件中生成人类可读的堆栈跟踪信息所需的信息。

Breakpad symbol 文件是 ASCII 文本文件,每一行都是一个 record,由单个空格分割成字段。在某些情况下,记录的最后一个字段可以包含空格。第一个字段是一个字符串,表示这一行是什么类型的记录。有些字段保存十进制或十六进制数字;十六进制数字没有 “0x” 前缀,并使用小写字母。

Breakpad symbol 文件包含以下记录类型:

  • MODULE 记录描述了可执行文件或共享库,从中得出数据,供 symbol suppliers 使用。MODULE 记录应该是文件中的第一个记录。
  • FILE 记录给出了一个源文件名称,并为它分配一个号码,以供其他记录(records)引用它。
  • FUNC 记录描述源代码中存在的一个函数。
  • line 记录指示应将哪一个源文件和一个给定范围的机器代码行标记。这行是由最近的 FUNC 记录定义的函数。
  • PUBLIC 记录给出了链接符号的地址。
  • STACK 记录提供了产生堆栈跟踪所需的信息。

关于 Symbols 文件更详细的说明请参见:symbol_files.md

minidump

minidump 文件格式由微软开发,用于其崩溃上传功能。minidump文件包含:

  • 在 dump 创建时,在流程中加载的可执行文件和共享库的列表。这个列表包含了加载的那些文件的特定版本的文件名和标识符。
  • 进程中出现的线程列表。对于每一个线程,minidump 包括处理器寄存器的状态,以及线程的堆栈内存的内容。这些数据是未解释的字节流,Breakpad 客户端通常没有可用来生成函数名或行号的调试信息,甚至无法识别堆栈框架边界。
  • dump 收集的系统的其他信息:处理器和操作系统版本,dump 的原因,等等。

Breakpad 在所有的平台上都使用 Windows minidump 文件替代核心文件(core files)文件。

一个 minidump 是通过调用到 Breakpad 库生成的。默认情况下,初始化 Breakpad 安装一个异常/信号处理器,在异常的时候将 minidump 写到磁盘上。

  • 在 Windows 平台上使用 SetUnhandledExceptionFilter() 函数;
  • 在 OS X 平台上,通过创建一个在 Mach exception port 上等待的线程来完成的;
  • 在 Linux 平台,通过安装一个信号处理器来监听 SIGILL SIGSEGV 等异常信号。

一旦生成 minidump,每个平台都有一种稍微不同的上传 crash dump 的方式。在 Windows 和 Linux 上,提供了一个单独的可以调用它来完成上传的功能库。在OS X上,一个单独的进程会提示用户获得上传许可,如果许可,将会发送文件。

Breakpad 源码

Breakpad 所有客户端代码都是通过访问 Google Project 获得,也可以访问 Github 上的镜像。

src 目录中包含:

  • processor 包含在服务端使用的 minidump-processing 代码。
  • client 包含为所有平台使用的客户端 minidump-generation 库。
  • tools 包含在每个平台上构建各种工具的源代码和项目。

Windows 集成

Windows 客户端代码位于 src/client/windows 目录中。

编译过程

编译需求:

  • Breakpad 源码。
  • gyp 工具,gyp工具在 https://chromium.googlesource.com/external/gyp/ 可获得,不过我用的 Visual Studio 2017 中自带。
  • Python 2.7,需加入到环境变量中,不要使用 Python 3.x。
  • googletest(可选)。

在命令行工具中依次执行下列命令:

cd Breakpad\src
tools\gyp\gyp.bat --no-circular-check client\windows\breakpad_client.gyp

成功后在 breakpad\src\client\windows\ 目录下有生成的 breakpad_client.sln 工程文件。如果没有安装 googletest 会出现 Warning: Missing input files: *\testing\googletest\src\* 警告,可以直接忽略。

打开 breakpad_client.sln 工程文件进行编译,编译成功后生成 一个演示程序 crash_generation_app.exe 和五个库文件 common.libcrash_generation_client.libcrash_generation_server.libcrash_report_sender.libexception_handler.lib

minidump-generation 集成

编译 src/client/windows 目录中的源代码,可以生成 exception_handler.lib 等库文件。你也可以将源代码导入到你的项目的目录中,直接使用其源代码构建项目。

在你的应用程序中启用 Breakpad 需要 #include "exception_handler.h"(在 src\client\windows\handler 目录下) 并且使用如下方式实例化 ExceptionHandler 对象:

handler = new ExceptionHandler(const wstring& dump_path,
                               FilterCallback filter,
                               MinidumpCallback callback,
                               void* callback_context,
                               int handler_types,
                               MINIDUMP_TYPE dump_type,
                               const wchar_t* pipe_name,
                               const CustomClientInfo* custom_info);

这些参数依次为:

  • pathname for minidumps to be written to - this is ignored if OOP dump generation is used
  • A callback that is called when the exception is first handled - you can return true/false here to continue/stop exception processing
  • A callback that is called after minidumps have been written
  • Context for the callbacks
  • Which exceptions to handle - see HandlerType enumeration in exception_handler.h
  • The type of minidump to generate, using the MINIDUMP_TYPE definitions in DbgHelp.h
  • A pipe name that can be used to communicate with a crash generation server
  • A pointer to a CustomClientInfo class that can be used to send custom data along with the minidump when using OOP generation

你也可以查看 src/client/windows/tests/crash_generation_app/* 中的示例程序。

下面是一个最简单的示例 minidump-generation 示例:

#include "client\windows\common\ipc_protocol.h"
#include "client\windows\handler\exception_handler.h"

#pragma comment(lib, "exception_handler.lib")
#pragma comment(lib, "common.lib")
#pragma comment(lib, "crash_generation_client.lib")

google_breakpad::ExceptionHandler* handler;
static size_t kCustomInfoCount = 2;
static google_breakpad::CustomInfoEntry kCustomInfoEntries[] = {
    google_breakpad::CustomInfoEntry(L"prod", L"wincrash"),
    google_breakpad::CustomInfoEntry(L"ver", L"1.0"),
};

std::wstring dump_path = L"C:\\Dumps\\";
const wchar_t kPipeName[] = L"\\\\.\\pipe\\BreakpadCrashServices\\TestServer";

bool ShowDumpResults(const wchar_t* dump_path,
    const wchar_t* minidump_id,
    void* context,
    EXCEPTION_POINTERS* exinfo,
    MDRawAssertionInfo* assertion,
    bool succeeded) {
    if (succeeded) {
        // 如果指定OOP Minidump Generation(管道),dump_path和minidump_id将为NULL
        printf("dump path is %ws\n", dump_path);
        printf("dump guid is %ws\n", minidump_id);
    }
    else {
        printf("dump failed\n");
    }
    system("pause");
    return succeeded;
}

void DerefZeroCrash() {
    int* x = 0;
    *x = 1;
}

void InvalidParamCrash() {
    printf(NULL);
}

void RequestDump() {
    if (!handler->WriteMinidump()) {
        MessageBoxW(NULL, L"Dump request failed", L"Dumper", MB_OK);
    }
    kCustomInfoEntries[1].set_value(L"1.1");
}

int main()
{
    if (_wmkdir(dump_path.c_str()) && (errno != EEXIST)) {
        MessageBoxW(NULL, L"Unable to create dump directory", L"Dumper", MB_OK);
        return 1;
    }
    google_breakpad::CustomClientInfo custom_info = { kCustomInfoEntries, kCustomInfoCount };
    handler = new google_breakpad::ExceptionHandler(dump_path, // 如果指定OOP Minidump Generation(管道)将忽略该参数
        NULL, ShowDumpResults, NULL, google_breakpad::ExceptionHandler::HANDLER_ALL, MiniDumpNormal, 
        kPipeName, &custom_info);
    
    DerefZeroCrash();
    //InvalidParamCrash();
    //RequestDump();

    system("pause");
    return 0;
}

如果不在 ExceptionHandler 指定管道,当程序发生异常的时候,就直接生成 dump 文件,这种方法叫进程内生成 dump,这是一种比较简单直接的做法。

进程内生成 dump 有两个缺点:

  1. 当进程发生异常频率崩溃的时候,进程是处于一种微弱和不稳定的状态,如果继续在进程内做生成 dump 的操作可能会破坏崩溃的现场,导致我们后来从 dump 看到的信息并非一开始导致崩溃的信息。
  2. 有些进程不支持进程内生成 dump。比如 renderer 进程,它在沙箱中没有访问文件系统的权限,因此它自己无法生成 dump。

所以有时可以使用下面的方法。

OOP Minidump Generation

在实际的项目中,你需要单独启动一个进程( crash generation server)监视客户端的崩溃情况,进行相应的操作(例如:重启程序、上传 dump 文件等)。src/client/windows/crash_generation 目录中的 crash_generation_server.h 文件是生成 crash generation server 的接口。

如上图所示,这种方法也叫进程外的生成 dump,先在 Server 进程里面创建一个 CrashGenerationServer 对象,CrashGenerationServer 对象会创建一个命名管道。然后 Client 进程里面还是会创建一个 ExceptionHandler 对象,ExceptionHandler 里面又包含了一个 CrashGenerationClient 对象,它通过命名管道与 Server 进程里面的 CrashGenerationServer 通信。

下面是一个最简单的示例 crash generation server 示例:

#include <string>
#include "client/windows/crash_generation/crash_generation_server.h"
#include "client\windows\common\ipc_protocol.h"
#include "client\windows\crash_generation\client_info.h"

#pragma comment(lib, "common.lib")
#pragma comment(lib, "crash_generation_server.lib")

// Maximum length of a line in the edit box.
const size_t kMaximumLineLength = 256;
std::wstring dump_path = L"C:\\Dumps\\";
// 这个管道名要和上面客户端所指定的管道名相同
const wchar_t kPipeName[] = L"\\\\.\\pipe\\BreakpadCrashServices\\TestServer";

static google_breakpad::CrashGenerationServer* crash_server = NULL;

static void ShowClientConnected(void* context,
    const google_breakpad::ClientInfo* client_info) {
    TCHAR* line = new TCHAR[kMaximumLineLength];
    line[0] = _T('\0');
    int result = swprintf_s(line,
        kMaximumLineLength,
        L"Client connected:\t\t%d\r\n",
        client_info->pid());

    if (result == -1) {
        delete[] line;
        return;
    }
}

static void ShowClientCrashed(void* context,
    const google_breakpad::ClientInfo* client_info,
    const std::wstring* dump_path) {
    TCHAR* line = new TCHAR[kMaximumLineLength];
    line[0] = _T('\0');
    int result = swprintf_s(line,
        kMaximumLineLength,
        TEXT("Client requested dump:\t%d\r\n"),
        client_info->pid());

    if (result == -1) {
        delete[] line;
        return;
    }

    google_breakpad::CustomClientInfo custom_info = client_info->GetCustomInfo();
    if (custom_info.count <= 0) {
        return;
    }

    std::wstring str_line;
    for (size_t i = 0; i < custom_info.count; ++i) {
        if (i > 0) {
            str_line += L", ";
        }
        str_line += custom_info.entries[i].name;
        str_line += L": ";
        str_line += custom_info.entries[i].value;
    }

    line = new TCHAR[kMaximumLineLength];
    line[0] = _T('\0');
    result = swprintf_s(line,
        kMaximumLineLength,
        L"%s\n",
        str_line.c_str());
    if (result == -1) {
        delete[] line;
        return;
    }
}

static void ShowClientExited(void* context,
    const google_breakpad::ClientInfo* client_info) {
    TCHAR* line = new TCHAR[kMaximumLineLength];
    line[0] = _T('\0');
    int result = swprintf_s(line,
        kMaximumLineLength,
        TEXT("Client exited:\t\t%d\r\n"),
        client_info->pid());

    if (result == -1) {
        delete[] line;
        return;
    }
}

void CrashServerStart() {
    // Do not create another instance of the server.
    if (crash_server) {
        return;
    }

    if (_wmkdir(dump_path.c_str()) && (errno != EEXIST)) {
        MessageBoxW(NULL, L"Unable to create dump directory", L"Dumper", MB_OK);
        return;
    }

    crash_server = new google_breakpad::CrashGenerationServer(kPipeName,
        NULL,
        ShowClientConnected,
        NULL,
        ShowClientCrashed,
        NULL,
        ShowClientExited,
        NULL,
        NULL,
        NULL,
        true,
        &dump_path);

    if (!crash_server->Start()) {
        MessageBoxW(NULL, L"Unable to start server", L"Dumper", MB_OK);
        delete crash_server;
        crash_server = NULL;
    }
}

void CrashServerStop() {
    delete crash_server;
    crash_server = NULL;
}

int main()
{
    CrashServerStart();
    system("pause");
    CrashServerStop();
    return 0;
}

注意:这里指定的管道名要和上面客户端所指定的管道名相同。

上传 minidump

src/client/windows/sender 目录有一个 CrashReportSender 类,这个类能够在单独的 CLI(命令行界面)编译或集成到 crash generation server 中用于上传 minidump 文件,你可以在 CrashGenerationServer 对象或 ExceptionHandler 对象的回调函数中使用它。

minidump 文件调试

在 wincrash.exe 程序所指定的 dump_path 目录下可看到生成的 .dmp 扩展名的崩溃转储文件,用对应版本的 Visual Studio 打开该文件。

可以看到 minidump 文件摘要信息,点击右侧 使用 仅限本机 进行调试,就可以看到错误详情及错误具体发生在哪行代码。

经过试验,调试(从其他机器发送过来的 dump 文件)时需要指定客户端程序的路径和 pdb 文件路径,还需要保证 pdb 文件与其对应的源代码版本统一。

值得注意的是:

  • 如果应用程序是以管理员权限运行,crash generation server 程序也需要以管理员权限运行。
  • 需要为可执行程序(exe,dll等)生成 pdb 文件才能进行 dump 调试。Visual Studio 项目需要在项目属性 -> 链接器 -> 调试 -> 生成调试信息 中选择 “生成调试信息 (/DEBUG)” 选项;Qt Creator 项目需要在 .pro 文件中添加下面的一段话:

    # 在 Release 模式是产生 pdb 文件,编译 Breakpad 调试
    QMAKE_CFLAGS_RELEASE -= $$QMAKE_CFLAGS_RELEASE
    QMAKE_CFLAGS_RELEASE += $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO
    QMAKE_CXXFLAGS_RELEASE -= $$QMAKE_CXXFLAGS_RELEASE
    QMAKE_CXXFLAGS_RELEASE += $$QMAKE_CXXFLAGS_RELEASE_WITH_DEBUGINFO
    QMAKE_LFLAGS_RELEASE -= $$QMAKE_LFLAGS_RELEASE
    QMAKE_LFLAGS_RELEASE += $$QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO

参考文章:
Breakpad崩溃报告系统介绍

标签: 崩溃, minidump, dump, Breakpad, crash

添加新评论