实习笔记四----breakpad和crashpad崩溃处理工具

实习笔记四—-breakpad和crashpad崩溃处理工具。


谷歌的chromium项目里面居然有两个崩溃处理工具,一个是crashpad,另一个是breakpad.研究了一下发现breakpad更容易集成到程序当中,因而对这两者进行了调研(主要内容为Mac下,但是其他操作系统下的使用差异不大)。

breakpad

源码编译:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$PATH:$PWD/depot_tools
mkdir breakpad & cd breakpad
fetch breakpad
cd src
./configure && make -j12
cd src/client/mac
xcodebuild -sdk macosx

使用

BreakPad.framework可以直接集成到代码当中作为client,用于捕获异常。在配置了头文件包含路径和framework链接路径之后,一个简单的程序如下:

#include <iostream>
#include <thread>
#include "client/mac/handler/exception_handler.h"

// 写完minidump后的回调函数
static bool dumpCallback(const char *dump_dir,
                         const char *minidump_id,
                         void *context, bool succeeded) {
  printf("Dump path: %s\n", dump_dir);
  return succeeded;
}

// 触发crash来测试
void my_crash() {
    volatile int* a = (int*)(NULL);
    *a = 1;
}

int main() {
  // 初始化ExceptionHandler
  google_breakpad::ExceptionHandler eh("/tmp",  // minidump文件写入到的目录
     NULL,
     dumpCallback, 
     NULL, 
     true, 
     NULL);

  my_crash();
  return 0;
}

这样,在ExceptionHandler捕获到异常之后,会先调用回调函数dumpCallback,然后将minidump写入到路径中。这个minidump是不可读的,但是如果配合debugging symbols,通过配套的工具dump_syms和minidump_stackwalk可以生成可读的trace文件。

在完成dump符号后,你就可以把程序的 debug 信息 strip 掉了,这个特性可以说是与 coredump 相比最大一亮点,dump 完 symbol 后 strip 完全不影响后续的崩溃现场还原。

个人认为崩溃后的回调函数更加重要,在这里可以fork一个新的进程去做其他的事情。

需要注意的是,你必须在callback回调函数中做尽量少的工作,因为你的程序处于一个不安全的状态,它需要无法安全的去分配内存,或调用其他共享库中的函数。安全的方式是forkexec一个新进程去做想要做的事情。如果你需要在回调中做一些工作,breakpad源码提供一些简单的重新实现的libc库里的方法,来避免直接调用libc, 并提供一个a header file for making linux system calls,来避免直接调用其他共享库的方法。 需要注意的是,在异常出现后,崩溃的进程常常已经被破坏了,因此在这个 callback 里不要做太多事情,特别是内存访问,写入以及调用别的库的函数等,这些很难保证能正常工作。 这里特指不要调用动态链接库中的函数,原因有如下几个:

  1. 有的动态库此时可能还没完全被加载完毕(如符号重定位),在这种时候调用其中的函数会触发动态动态库运行时加载的一系列动作,这在程序已经崩溃的情况下通常做不到。
  2. 动态库中的函数可能会引用 heap 上某些内存,而程序崩溃时,这些内存有可能被破坏了,库中函数未必能正常工作。
  3. 对某些 libc 中的函数,它们是有锁的,程序崩溃时,其它线程都会被暂停,此时如果某些线程恰好也调用了这些函数并取得了锁,那么在信号处理函数中就无法再获得锁,因此会死锁。

google_breakpad::ExceptionHandler的构造函数如下:

typedef bool (*FilterCallback)(void *context);  

typedef bool (*MinidumpCallback)(const char *dump_dir,
                                   const char *minidump_id,
                                   void *context, bool succeeded);

ExceptionHandler(const string &dump_path,
                   FilterCallback filter, MinidumpCallback callback,
                   void *callback_context, bool install_handler,
                   const char *port_name);
  • dump_path为mini_dump文件保存的路径
  • filter在写minidump文件之前,会先调用filter回调。根据它返回true/false来决定是否需要写minidump文件
  • callback回调函数
  • callback_context传给回调函数的
  • install_handler, 如果为ture,不管怎样当未捕捉异常被抛出时都会写入minidump文件,如果为false则必须明确调用了WriteMinidump才会写入minidump 文件
  • port_name

如果不需要写minudump文件,只需要处理回调函数,那么google_breakpad::ExceptionHandler还有一个更为简单的构造函数如下:

ExceptionHandler(DirectCallback callback,
                   void *callback_context,
                   bool install_handler);

触发原理和触发的情况

  • 在windows平台上,使用微软提供的 SetUnhandledExceptionFilter()方法来实现。
  • 在OS X平台上,通过创建一个线程来监听 Mach Exception port来实现。
  • 在Linux平台上,通过设置一个信号处理器来监听 SIGILL SIGSEGV等异常信号。

在mac下试验的时候发现,如果一个运行中的程序被kill掉了,这里并不会触发回调函数。。。不过也有解决方法,通过boost::asio监听信号,这样可以监听到kill的信号。

minidump栈文件的解码

breakpad解码的原理大概是这样:

编译dump_syms工具需要稍微改下代码,不然无法编译成功。dump_syms编译完成后,可以生成程序的调试符号。

对于minidump文件解码的流程如下:

建立一个工程,假设我们有这样的一个main.cpp文件

#include <iostream>
#include "client/mac/handler/exception_handler.h"

#include "crash_class.h"

// 写完minidump后的回调函数
static bool dumpCallback(const char *dump_dir,
                         const char *minidump_id,
                         void *context, bool succeeded) {
  printf("Dump path: %s\n", dump_dir);

  return succeeded;
}

int main() {
  google_breakpad::ExceptionHandler eh("/Users/shenweihong", NULL, dumpCallback, NULL, true, NULL);
  crash_class cls1;
  cls1.setVal(2);
  return 0;
}

crash_class.h的源码

#ifndef CRASH_CLASS_H
#define CRASH_CLASS_H


class crash_class
{
public:
    crash_class();

    void setVal(int val);
private:
    int mVal;
};

#endif // CRASH_CLASS_H

crash_class.cpp的源码:

#include "crash_class.h"

crash_class::crash_class()
{
    volatile int* a = (int*)(nullptr);
    *a = 1;
    mVal = 1;
}

void crash_class::setVal(int val) {
    mVal = val;
}

首先为编译器设置-g的编译选项(带调试符号),得到二进制可执行文件crash_crapture.运行后会在home路径下生成一个dmp文件,保存着栈的信息。方便起见,可以把crash_capture, dump_syms, minidump_stackwalk以及dmp文件都复制到同一个路径下。

运行命令

./dump_syms crash_capture > crash_capture.sym

可以得到分离的符号文件crash_capture.sym,且以文本信息储存,在开启-o2的优化后crash_capture.sym的内容如下:

MODULE mac x86_64 E6BF8070F0503F9191912ACEE42B01B10 crash_capture
FUNC 1d50 20 0 __ZN11crash_classC2Ev
FUNC 1d70 20 0 __ZN11crash_classC1Ev
FUNC 1d90 8 0 __ZN11crash_class6setValEi
FUNC 1da0 130 0 _main
FUNC 1ed0 22 0 __ZL12dumpCallbackPKcS0_Pvb
PUBLIC 0 0 _mh_execute_header
PUBLIC 1f5c 0 GCC_except_table0

minidump_stackwalk对于文件目录有要求,因此需要查看crash_capture.sym的第一行来创建正确的文件路径:

$ head -1 crash_capture.sym 
MODULE mac x86_64 E6BF8070F0503F9191912ACEE42B01B10 crash_capture
$ mkdir -p ./symbols/crash_capture/E6BF8070F0503F9191912ACEE42B01B10
$ mv crash_capture.sym ./symbols/crash_capture/E6BF8070F0503F9191912ACEE42B01B10/
$ ./minidump_stackwalk F5DA2B9D-A66C-4CB2-821F-69EB2DC406B1.dmp ./symbols/ > crashreport.txt

得到的crashreport的内容很多,其中开头的信息可以帮助定位崩溃所在的函数:

Operating system: Mac OS X
                  10.14.5 18F203
CPU: amd64
     family 6 model 158 stepping 10
     12 CPUs

GPU: UNKNOWN

Crash reason:  EXC_BAD_ACCESS / KERN_INVALID_ADDRESS
Crash address: 0x0
Process uptime: 0 seconds

Thread 0 (crashed)
 0  crash_capture!__ZN11crash_classC1Ev + 0x4
    rax = 0x0000000000000001   rdx = 0x0000000000000000
    rcx = 0x00007fff6591f20e   rbx = 0x0000000000000000
    rsi = 0x00007ffee1df78d8   rdi = 0x00007ffee1df78c0
    rbp = 0x00007ffee1df78a0   rsp = 0x00007ffee1df78a0
     r8 = 0x00000000090008ff    r9 = 0x0000000000000003
    r10 = 0x0000000118365108   r11 = 0x00007fff659dbe8c
    r12 = 0x0000000000000000   r13 = 0x0000000000000000
    r14 = 0x0000000000000000   r15 = 0x0000000000000000
    rip = 0x000000010de09d74
    Found by: given as instruction pointer in context
 1  crash_capture!_main + 0xb6
    rbp = 0x00007ffee1df79e0   rsp = 0x00007ffee1df78b0
    rip = 0x000000010de09e56
    Found by: previous frame's frame pointer
 2  libdyld.dylib + 0x163d5
    rbp = 0x00007ffee1df79f8   rsp = 0x00007ffee1df79f0
    rip = 0x00007fff657e83d5
    Found by: previous frame's frame pointer
 3  libdyld.dylib + 0x163d5
    rbp = 0x00007ffee1df79f8   rsp = 0x00007ffee1df79f8
    rip = 0x00007fff657e83d5
    Found by: stack scanning

minidump_stackwalk对于文件目录有要求,因此需要查看crash_capture.sym的第一行来创建正确的文件路径:

$ head -1 crash_capture.sym 
MODULE mac x86_64 E6BF8070F0503F9191912ACEE42B01B10 crash_capture
$ mkdir -p ./symbols/crash_capture/E6BF8070F0503F9191912ACEE42B01B10
$ mv crash_capture.sym ./symbols/crash_capture/E6BF8070F0503F9191912ACEE42B01B10/
$ ./minidump_stackwalk F5DA2B9D-A66C-4CB2-821F-69EB2DC406B1.dmp ./symbols/ > crashreport.txt

得到的crashreport的内容很多,其中开头的信息可以帮助定位崩溃所在的函数:

Operating system: Mac OS X
                  10.14.5 18F203
CPU: amd64
     family 6 model 158 stepping 10
     12 CPUs

GPU: UNKNOWN

Crash reason:  EXC_BAD_ACCESS / KERN_INVALID_ADDRESS
Crash address: 0x0
Process uptime: 0 seconds

Thread 0 (crashed)
 0  crash_capture!__ZN11crash_classC1Ev + 0x4
    rax = 0x0000000000000001   rdx = 0x0000000000000000
    rcx = 0x00007fff6591f20e   rbx = 0x0000000000000000
    rsi = 0x00007ffee1df78d8   rdi = 0x00007ffee1df78c0
    rbp = 0x00007ffee1df78a0   rsp = 0x00007ffee1df78a0
     r8 = 0x00000000090008ff    r9 = 0x0000000000000003
    r10 = 0x0000000118365108   r11 = 0x00007fff659dbe8c
    r12 = 0x0000000000000000   r13 = 0x0000000000000000
    r14 = 0x0000000000000000   r15 = 0x0000000000000000
    rip = 0x000000010de09d74
    Found by: given as instruction pointer in context
 1  crash_capture!_main + 0xb6
    rbp = 0x00007ffee1df79e0   rsp = 0x00007ffee1df78b0
    rip = 0x000000010de09e56
    Found by: previous frame's frame pointer
 2  libdyld.dylib + 0x163d5
    rbp = 0x00007ffee1df79f8   rsp = 0x00007ffee1df79f0
    rip = 0x00007fff657e83d5
    Found by: previous frame's frame pointer
 3  libdyld.dylib + 0x163d5
    rbp = 0x00007ffee1df79f8   rsp = 0x00007ffee1df79f8
    rip = 0x00007fff657e83d5
    Found by: stack scanning

可以发现,是__ZN11crash_classC1Ev的函数发生了崩溃,C1Ev就是构造函数的符号ABI。(https://itanium-cxx-abi.github.io/cxx-abi/abi.html)

_Z     | N      | 5Thing  | 3foo | E          | v
prefix | nested | `Thing` | `foo`| end nested | parameters: `void`  

<ctor-dtor-name>   ::= C1   # complete object constructor
                   ::= C2   # base object constructor
                   ::= C3   # complete object allocating constructor
                   ::= D0   # deleting destructor
                   ::= D1   # complete object destructor
                   ::= D2   # base object destructor

Crashpad

编译和链接

这个crashpad的编译还算简单,但是链接非常麻烦,需要处理许许多多的依赖。(Mac OS 10.14.5)

获取崩溃的地址

在生成了dmp文件之后,通常需要将崩溃文件上传至服务器。如果程序在某个地方反复崩溃,上传多次重复的崩溃是没有必要的。因此,需要根据崩溃产生的地址对崩溃去重,这里就需要我们去获取崩溃所在的地址。ExceptionHandler这个类在执行回调函数时是不会传入崩溃地址这个参数的,因此需要在回调函数中,我们读取生成的dmp文件,解析文件后即可得到崩溃的地址。

地址空间配置随机加载(ASLR, Address space layout randomization)是一种在应用启动时随机设置程序加载地址的基地址的机制,这一机制主要是是为了防止恶意代码的攻击,大多数现代操作系统都实现了这一机制。要获取崩溃地址的真正的相对地址,需要减去基地址,而程序执行的基地址是执行指令所在模块的地址。通常,我们需要遍历各个加载的模块,然后判断rip的地址位于哪个模块的内存空间中,再减去这个模块的地址即可得到相对地址。

下面的代码不仅需要链接client的库,还需要链接breakpad的库以实现对dump文件的解析。

#include <iostream>
#include <cassert>

using namespace std;

#include "google_breakpad/processor/minidump.h"
#include "client/mac/handler/exception_handler.h"

using namespace google_breakpad;

static void Crasher() {
  int *a = (int*)0x42;

  fprintf(stdout, "Going to crash...\n");
  fprintf(stdout, "A = %d", *a);
}

static void AbortCrasher() {
  fprintf(stdout, "Going to crash...\n");
  abort();
}

static void SoonToCrash(void(*crasher)()) {
  crasher();
}

static bool DumpCallback(const char *dump_dir, const char *file_name,
                               void *context, bool success) {
    string path(dump_dir);
    path.append("/");
    path.append(file_name);
    path.append(".dmp");

    Minidump minidumpReader(path);
    minidumpReader.Read();
    MinidumpContext* exceptionContext = 
            minidumpReader.GetException()->GetContext();
    uint64_t instructionPointer;
    exceptionContext->GetInstructionPointer(&instructionPointer);

    auto modlist = minidumpReader.GetModuleList();
    uint64_t baseAddr;
    for(auto i = 0; i < modlist->module_count(); i++) {
        mod = const_cast<google_breakpad::MinidumpModule *>(modlist->GetModuleAtIndex(i));
        auto mod_base = mod->base_address();
        auto mod_size = mod->size();
        ip_addr = instructionPointer;
        if (ip_addr > mod_base && ip_addr < mod_base + mod_size) {
            baseAddr = ip_addr - mod_base;
            break;
        }
    }
    uint64_t relativeAddr = instructionPointer - baseAddr;
    std::cout << std::hex << std::endl;
    std::cout << "指令指针地址    : " << instructionPointer << std::endl;
    std::cout << "基地址         : " << baseAddr << std::endl;
    std::cout << "相对指令指针地址 : " << relativeAddr << std::endl;
    return true;
}

int main(int argc, char** argv) {
    google_breakpad::ExceptionHandler eh(getenv("HOME"), 
                                         NULL, DumpCallback,
                                         NULL, true, NULL);
    bool aborting = false;
    SoonToCrash(aborting ? &AbortCrasher : &Crasher);
    return 0;
}

两次执行这一程序,可得到的结果分别为:

指令指针地址    : 105f01417
基地址         : 105eff000
相对指令指针地址 : 2417
----------------------------
指令指针地址    : 10c37e417
基地址         : 10c37c000
相对指令指针地址 : 2417

可以看出基地址会变化,相对指令指针地址不会变化,可以依据此去重。

## 其实崩溃处理还有很多细节问题在里面,比如每次构建程序二进制文件后对符号文件的储存,比如客户端程序崩溃后的上传工作等。在符号分析中,unix系统下可以多使用nm命令查看符号和地址,此外逆向工程软件cutter也是一个很有用的工具。

参考资料

https://www.jianshu.com/p/295ebf42b05b https://github.com/google/breakpad