最近学习了一下BreakPad获取native crash的系统信息和堆栈信息,这是极客时间的一个作业。
做android开发的都知道,crash是非常致命的问题,有两种crash,java本地Crash和native crash,第一种比较好解,因为java堆栈已经帮你定位到问题,而第二种,主要指的是C/C++代码,在android中以动态链接的形式存在,由于是跨语言的,所以往往很难定位。
当第二种崩溃发生的时候,你的内心恐怕也是:
因此很多公司都开发了很多工具来进行,比如腾讯的bugly,阿里的啄木鸟。
要想做一个可用的崩溃日志收集系统,需要做到以下目的:
- 打印logcat和应用日志。
- 上报crash。
- 能根据不同的crash做不同的恢复措施。
接下来,我会通过操作系统信号处理,捕获Crash机制与一个小实践完成一次捕获native异常的过程。
信号处理机制
异常发生时,CPU会触发异常中断处理流程,linux将这些中断处理,统一为信号量,可以理解为代码中的标志位。
用户态与内核态是linux的一种权限机制,运行在3级特权级的称为用户态,无法访问系统内核数据和程序,运行在0级就是内核态。
- 步骤一:第一次进程用户-内核切换
首先,内核会收到信号量,然后将其放入到【对应出现异常的进程】的【信号队列】中,同时发送一个中断,告诉这个进程,你得去内核态了。
进程进入内核态后,执行指定的指令可以回到用户态,回到用户态时会进行一次信号检测,也就是检测上面说的信号队列。
也就是说,进程第一次的用户-内核切换是为了执行linux发出的信号量。 - 步骤二:第二次进程用户-内核切换
此时进程在用户态,并准备执行【信号处理程序】,在执行之前,内核会将当前内核栈备份一下,以便将指令寄存器指向信号处理函数,进程可以找到该函数执行。
执行完毕后会返回内核态,回到步骤一中,因为每次只能执行一个信号量,执行完后再回到内核态检测是否有新的信号量,这里是一个循环。当所有的信号量执行完毕,此时内核就会将原有的内核栈恢复。
所以这里的第二次进程-用户切换是为了进程能找到准确的信号处理函数,同时内核做备份恢复,以便处理完成后能保存现场。
使用breakpad捕捉native crash
上面是从进程和操作系统的角度看待异常,那么上面的【信号处理程序】干了什么事情呢?
这里的【信号处理程序】包含所有可能的异常,本文我们以native crash来做例子,
在编译C++时,就需要保存对应带符号信息的文件。
对于android系统来说来说,捕捉崩溃的时候会手机尽可能多的信息写入log文件并上报,然后服务端通过一定的程序解析出来。
现在我们将用谷歌的breakpad来捕捉,先看看他是怎么做的:
如图:
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
他只有一个页面,点击就会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查看设备文件夹
可看到我们这里已经生成了crash文件,每点击一次就会生成一个
我用的是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位置。