Google Breakpad 源码解析(一)

2,942 阅读3分钟

系列文章

简介

Breakpad 是 Google 开源的,用于实现崩溃上报(crash-report)的系统,其中包括客户端和服务端两部分。

官网上的介绍:

Breakpad is a set of client and server components which implement a crash-reporting system.

编译构建

Breakpad 的源码依赖管理使用的是 Google 自己开发的 depot_tools,但因为某些特殊原因,使用 depot_tools 下载源码经常会卡住,所以网上也有很多教程是让大家直接用 git 下载,相关文章很多这里就不贴链接了。

Breakpad 构建出来的脚本是区分系统环境的,所以要根据当前使用系统,构建不同的产物。Breakpad 目前支持 macOS、Linux 和 Windows 三个系统环境的构建。

笔者自己本身是 macOS(Mojave 10.14.4) 环境,但是没有编译成功,提示缺少某些依赖库,最后索性用 Docker 跑了个 Linux(ubuntu 18.04) 镜像,意外的顺利,一次成功了。

这里放上 Dockerfile 和 docker-compose.yml,习惯使用 docker-compose 了。

Dockerfile

 FROM ubuntu:18.04
 ​
 # 配置 git
 ENV DEBIAN_FRONTEND=noninteractive
 RUN apt update && \
     apt install --no-install-recommends git-all curl wget build-essential --assume-yes
 ​
 # 配置 depot_tools
 RUN cd /opt && \
     git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
 ENV PATH ${PATH}:/opt/depot_tools
 ​
 COPY src/ /opt/breakpad/
 ​
 # 配置 breakpad
 RUN cd /opt/breakpad && \
     ./configure && \
     make && \
     make install

这里之所以使用 COPY 命令拷贝 breakpad 源码,是因为需要修改 Breakpad 源码,这样可以方便调试。

docker-compose.yml

 version: "3.7"
 
 services:
     yybreakpad:
         build: ./
         command: tail -F anything
         volumes:
             - ./temp:/opt/temp

这条命令是让 docker 容器不自动退出:

 command: tail -F anything

挂载了 temp 目录,可以将一些执行完的产物放到这个目录里面,不需要频繁在容器和主机之间拷贝:

 volumes:
     - ./temp:/opt/temp

这是构建目录的层级结构:

 ├── Dockerfile
 ├── docker-compose.yml
 ├── src
 └── temp

架构

先放一张 Breakpad 官方的架构图:

Breakpad-System

Breakpad 主要由三个部分组成:

  • Client,当端上发生崩溃时,会默认生成 minidump 文件。
  • Symbol Dumper,这个工具用于生成 Breakpad 专属的符号表,要作用在带有调试信息原始库才行。
  • Processor,这个工具通过读取 Client 生成的 minidump 文件,再去匹配 Symbol Dumper 生成的对应符号表,最后生成人类可读的 C/C++ 堆栈跟踪。

minidump 格式

minidump 文件可以认为是 coredump 文件的简化版,之所以使用它,官方的理由是:

  • coredump 非常大,在端上传输不方便。
  • coredump 记录不全,例如,Linux 标准库没有描述寄存器如何存储在 PT_NOTE 段中。
  • 说服 Windows 机器生成 coredump 比 其他系统生成 minidump 更难。
  • 实现各平台的 dump 文件格式统一。

处理流程(以 Linux 为例)

  1. dump_syms

    当我们用 C/C++ 代码编写了一个库后,编译器默认会生成带调试信息的 ELF 文件,这时候我们可以通过执行以下命令生成符号表:

     dump_syms [elf_file] > [elf_file.sym]
    

    elf_file.sym 不是强制格式,可以使用任意文件名称和后缀。

    带调试信息的 ELF 文件要大的多,所以,一般端上使用的是 strip 后的文件,这会剔除一些不必要的信息,让文件变得更小。

    生成的符号表需要按照指定格式存放,这里后面才能正确匹配上,首先是外层的目录名字需要是 ELF 文件的名称,包含后缀,接着是符号表 ID 的目录,最后才是存放对应符号表。

    例如:

     └── symbols
         └── libtest.so
             └── D6CAF1C3E374EFD057659926ABA14AD00
                 └── libtest.so.sym
    

    符号 ID 可以通过读取符号表文件的首行获取:

     $ head -n1 libtest.so.sym
     MODULE Linux arm D6CAF1C3E374EFD057659926ABA14AD00 libtest.so
    

    其中 D6CAF1C3E374EFD057659926ABA14AD00 就是对应符号表的 ID。

  2. minidump_writer

    Breakpad Client 组件提前注册好 SIGSEGV、SIGABRT 等异常信号的回调方法,当端上发生崩溃时,会生成 minidump 文件,其中会包含线程信息链接库信息堆栈信息 等等。

  3. symupload(可选)

    Breakpad 支持将生成的 minidump 文件上传到指定服务器,这是可选的步骤,可以选择自己上传。

  4. minidump_stackwalk

    在获取到 minidump 文件后,就可以使用 minidump_stackwalk 配合对应的符号表,将 minidump 文件解析成人类可读的堆栈跟踪了。

     minidump_stackwalk [minidump_file] ./symbols > [stacktrace_file]
    

    ./symbols 是用于指定符号表目录,具体内容可以看步骤 1,[stacktrace_file] 用于保存最终生成的堆栈跟踪。

符号表匹配(以 Linux 为例)

minidump 文件是根据符号 ID 来匹配对应的符号表

符号 ID 的生成规则,默认会使用 ELF 文件中的 BuildId,如果不存在 BuildId,则会根据 text section 摘要生成。对应代码片段如下:

text section 在 ELF 文件中一般用来存放代码部分,所以这里可以理解为根据代码做摘要。

关于 ELF 文件格式,可以通过 wiki 来做更深入了解。

 // https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/main/src/common/linux/file_id.cc
 // static
 bool FileID::ElfFileIdentifierFromMappedFile(const void* base,
                                              wasteful_vector<uint8_t>& identifier) {
   // Look for a build id note first.
   // 首先使用 build id
   if (FindElfBuildIDNote(base, identifier))
     return true;
   // Fall back on hashing the first page of the text section.
   // 否则,使用 text section 的 hash 值
   return HashElfTextSection(base, identifier);
 }

所以,当我们使用 minidump_stackwalk 没有把符号地址转换成对应的符号时,可以检查下符号 ID 是否匹配正确

小结

Breakpad 总体架构是非常清晰的,每个模块负责的职责都不一样,Client 在端上捕获 crash 并生成 minidump 上报,服务端利用提前生成的符号表,使用 minidump_stackwalk 将 minidump 解析成人类可读的堆栈跟踪

这一节,我们主要是从宏观上去看 Breakpad 的总体设计,包括编译构建,使用流程等等,这有助我们在下一节,对 Breakpad 的源码解析。