最近学习了一下BreakPad获取native crash的系统信息和堆栈信息,这是极客时间的一个作业。

做android开发的都知道,crash是非常致命的问题,有两种crash,java本地Crash和native crash,第一种比较好解,因为java堆栈已经帮你定位到问题,而第二种,主要指的是C/C++代码,在android中以动态链接的形式存在,由于是跨语言的,所以往往很难定位。

当第二种崩溃发生的时候,你的内心恐怕也是:

Android 收集崩溃信息 安卓native崩溃堆栈定位_Android 收集崩溃信息

因此很多公司都开发了很多工具来进行,比如腾讯的bugly,阿里的啄木鸟。

要想做一个可用的崩溃日志收集系统,需要做到以下目的:

  1. 打印logcat和应用日志。
  2. 上报crash。
  3. 能根据不同的crash做不同的恢复措施。

接下来,我会通过操作系统信号处理,捕获Crash机制与一个小实践完成一次捕获native异常的过程。

信号处理机制

异常发生时,CPU会触发异常中断处理流程,linux将这些中断处理,统一为信号量,可以理解为代码中的标志位。

用户态与内核态是linux的一种权限机制,运行在3级特权级的称为用户态,无法访问系统内核数据和程序,运行在0级就是内核态。

  • 步骤一:第一次进程用户-内核切换
    首先,内核会收到信号量,然后将其放入到【对应出现异常的进程】的【信号队列】中,同时发送一个中断,告诉这个进程,你得去内核态了。
    进程进入内核态后,执行指定的指令可以回到用户态,回到用户态时会进行一次信号检测,也就是检测上面说的信号队列。
    也就是说,进程第一次的用户-内核切换是为了执行linux发出的信号量。
  • 步骤二:第二次进程用户-内核切换
    此时进程在用户态,并准备执行【信号处理程序】,在执行之前,内核会将当前内核栈备份一下,以便将指令寄存器指向信号处理函数,进程可以找到该函数执行。

执行完毕后会返回内核态,回到步骤一中,因为每次只能执行一个信号量,执行完后再回到内核态检测是否有新的信号量,这里是一个循环。当所有的信号量执行完毕,此时内核就会将原有的内核栈恢复。

所以这里的第二次进程-用户切换是为了进程能找到准确的信号处理函数,同时内核做备份恢复,以便处理完成后能保存现场。

Android 收集崩溃信息 安卓native崩溃堆栈定位_android_02

使用breakpad捕捉native crash

上面是从进程和操作系统的角度看待异常,那么上面的【信号处理程序】干了什么事情呢?

这里的【信号处理程序】包含所有可能的异常,本文我们以native crash来做例子,

在编译C++时,就需要保存对应带符号信息的文件。

对于android系统来说来说,捕捉崩溃的时候会手机尽可能多的信息写入log文件并上报,然后服务端通过一定的程序解析出来。

Android 收集崩溃信息 安卓native崩溃堆栈定位_信号处理_03

现在我们将用谷歌的breakpad来捕捉,先看看他是怎么做的:

如图:

Android 收集崩溃信息 安卓native崩溃堆栈定位_Android 收集崩溃信息_04

breakpad由三个部分组成

Client ,以library的形式内置在应用中,当崩溃发生时作为目击者写minidump文件,主要记录了crash发生时的线程,地址,寄存器等。

symbol dumper,符号卸载器,读取由编译器生成的调试信息,生成symbol file,主要记录我们上面提过的C++符号信息。比如模块记录,文件记录,函数记录,行号记录等。

Processor ,将minidump和symbol file合并生成可读的c/c++ 堆栈。

可以说,Client保存案发现场,symbol dumper指出嫌疑人,Processor是警察,而你就是柯南~

实际操作定位crash

现在我们来实战一下吧。

我们的目的,是执行一个native crash,用breakpad获取他的日志并进行解析

首先进入网址

https://github.com/AndroidAdvanceWithGeektime/Chapter01

clone这里的代码,编译成一个Apk

Android 收集崩溃信息 安卓native崩溃堆栈定位_Android 收集崩溃信息_05

他只有一个页面,点击就会crash,当然,在此之前,我们要建立一个保存crash信息的文件夹。

private File externalReportPath;
public void onClick(View view) {
    initBreakPad();
    crash();
    // copy core dump to sdcard
}

private void initBreakPad() {
    if (externalReportPath == null) {
        externalReportPath = new File(getFilesDir(), "crashDump");
        if (!externalReportPath.exists()) {
            externalReportPath.mkdirs();
        }
    }
    BreakpadInit.initBreakpad(externalReportPath.getAbsolutePath())
}
public native void crash();

上面是核心代码,点击之后,首先执行initBreakPad创建一个crashDump文件夹,BreakPad是谷歌的一个捕捉崩溃的框架,然后执行native的crash函数,待会我们就是要反向定位到这里。

点击crash按钮,然后查看点击View -Tool Windows- Device File Explorer查看设备文件夹

Android 收集崩溃信息 安卓native崩溃堆栈定位_Android 收集崩溃信息_06

可看到我们这里已经生成了crash文件,每点击一次就会生成一个

Android 收集崩溃信息 安卓native崩溃堆栈定位_内核_07

我用的是mac电脑,建议将breakpad也克隆下来自己一个

执行

git clone https://github.com/google/breakpad 
cd breakpad
./configure && make

后会出现一个minidump_stackwalk程序

位于breakpad/src/processor/minidump_stackwalk

将上面的crash日志复制到breakpad/src/processor/中

执行

./minidump_stackwalk  6a6cc671-97c7-4e42-30ae3092-4dcf84eb.dmp >crashLog.txt

即可解析,得到txt日志

Operating system: Android
                  0.0.0 Linux 4.19.81-perf-gb895fc2 #1 SMP PREEMPT Tue Oct 20 02:06:12 CST 2020 aarch64
CPU: arm64
     8 CPUs

GPU: UNKNOWN

Crash reason:  SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available

Thread 0 (crashed) //crash 发生时候的线程
 0  libcrash-lib.so + 0x650  //发生 crash 的位置和寄存器信息,下面的符号解析需要用到
     x0 = 0x0000007f768c8a40    x1 = 0x0000007fc4aed494
     x2 = 0x0000000000000000    x3 = 0x0000007f76860000
     x4 = 0x0000007fc4aee610    x5 = 0x0000007ee4e6f965
     x6 = 0x0000000000000051    x7 = 0x0000000000000000
     x8 = 0x0000000000000000    x9 = 0x0000000000000001
    x10 = 0x0000000000430000   x11 = 0x0000000000000030
    x12 = 0x0000000092902370   x13 = 0x000000000048ef50
...

通过这个txt,拿到对应的位置信息,使用

(base) iMac:bin chensong$ aarch64-linux-android-addr2line -f -C -e xxx/Chapter01/sample/build/intermediates/transforms/mergeJniLibs/debug/0/lib/arm64-v8a/libcrash-lib.so 0x650

Crash()
xxx/Chapter01/sample/src/main/cpp/crash.cpp:10
(base) iMac:bin chensong$

可以看到,是执行了cpp的crash()函数,和我们原来的代码一致,这样,就算是找到了。

总结一下,breakpad是通过dump保存日志信息,使用minidump_stackwalk生成crash.txt,这里保存了crash发生的位置,然后通过aarch64-linux-android-addr2line进行符号解析,获得crash位置。