系列文章
简介
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 主要由三个部分组成:
- 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 为例)
-
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。
-
minidump_writer
Breakpad Client 组件提前注册好 SIGSEGV、SIGABRT 等异常信号的回调方法,当端上发生崩溃时,会生成 minidump 文件,其中会包含线程信息、链接库信息、堆栈信息 等等。
-
symupload(可选)
Breakpad 支持将生成的 minidump 文件上传到指定服务器,这是可选的步骤,可以选择自己上传。
-
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 的源码解析。