一、native crash捕获原理
native crash捕获的原理摘选完善自:Android 开发中常见 Crash 的情况。native crash捕获主要利用了Linux的信号机制(进程间通信方式的一种)。当应用程序异常,Linux内核将产生的错误信息通知当前进程。当前进程在接收到该错误信号后,可以有三种不同的处理方式。
(1)忽略该信号。
(2)捕捉该信号并执行对应的信号处理函数(signal handler)。
(3)执行该信号的缺省操作(如 SIGSEGV, 其缺省操作是终止进程)。
当 Linux 应用程序在执行时发生严重错误,一般会导致程序 crash。其中,Linux 专门提供了一类 crash 信号,在程序接收到此类信号时,缺省操作是将 crash 的现场信息记录到 core 文件,然后终止进程。
Android Native程序本质上就是一个Linux程序,在执行时发生错误程序crash之后,也会产生一个记录crash现场信息的文件,在Android系统中就是tombstone文件,这个文件保存在路径/data/tombstones/目录下,以tombstone_数字编号命名。
参见下面这个tombstone文件,是我从data/tombstones/目录下拷贝出来的tombstone_00文件,可以看到此文件记录了死亡进程的基本信息(比如进程的进程号(pid)、线程号(tid)),死亡的地址,死亡现场的堆栈调用信息等:
Build fingerprint: 'Android/rk3288/rk3288:5.1.1/LMY49F/elc-liubei06091434:userdebug/test-keys'
Revision: '0'
ABI: 'arm'
pid: 25243, tid: 25260, name: Binder_2 >>> com.tencent.daemon <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x14
r0 00000000 r1 a1c47c02 r2 a1c47c08 r3 a2180820
r4 00000000 r5 72da7878 r6 00000006 r7 a1c47c08
r8 12e01660 r9 b72e3cd0 sl 12e09aa0 fp 12e09b20
ip b520d450 sp a21807b0 lr a1c0e1db pc a1b85be0 cpsr 200d0030
d0 65742e6d6f632330 d1 616d2e746e656330
d2 3036314070706130 d3 3030313031303031
d4 7674710e0cf77572 d5 75f677890bf50ff6
d6 52b34d2a4aaa02e1 d7 71b31cc653c98a50
d8 0000000000000000 d9 0000000000000000
d10 0000000000000000 d11 0000000000000000
d12 0000000000000000 d13 0000000000000000
d14 0000000000000000 d15 0000000000000000
d16 0000000000000000 d17 4020000000000000
d18 4024000000000000 d19 72dd989072dd9858
d20 72dd974072dd9708 d21 4020000000000000
d22 6f6181706f618170 d23 72dd96d072dd9698
d24 6f6181706f618170 d25 6f6181706f618170
d26 6f6181706f618170 d27 6f6181706f618170
d28 6f6181706f618170 d29 6f6181706f618170
d30 6f6181706f618170 d31 4000000000000000
scr 80000011
backtrace:
#00 pc 0007ebe0 /data/app/com.tencent.daemon-1/lib/arm/libhwnetcore.so (std::string::_M_assign(char const*, char const*)+7)
#01 pc 001071d7 /data/app/com.tencent.daemon-1/lib/arm/libhwnetcore.so (Java_com_tencent_wechat_HWNetcore_addCommonRequest+30)
#02 pc 001e0e79 /data/dalvik-cache/arm/data@app@com.tencent.daemon-1@base.apk@classes.dex
stack:
a2180770 b520c9b8 /system/lib/libart.so
a2180774 00000030
a2180778 b72e3cd0 [heap]
a218077c b45fbe0e
a2180780 b72f0628 [heap]
a2180784 b510e895 /system/lib/libart.so (art::JNI::ReleaseByteArrayElements(_JNIEnv*, _jbyteArray*, signed char*, int))
a2180788 b72e4310 [heap]
a218078c a21807cc [stack:25260]
a2180790 a2180820 [stack:25260]
a2180794 12e01660 /dev/ashmem/dalvik-main space (deleted)
tombstone文件主要的组成部分:
(1) Build fingerprint
(2) ABI(Application binary interface):应用程序二进制接口,定义了一套规则,允许编译好的二进制目标代码在所有兼容该ABI的操作系统和硬件平台中无需改动就能运行。而具体的实现是由编译器、CPU和操作系统共同完成的。不同的CPU芯片(如ARM、Intel x86等)支持不同的ABI架构,常见的ABI类型包括:armeabi,armeabi-v7a,x86,x64,mips。从tombstone文件可看出ABI为ARM类型
(3) pid(进程号)和tid(线程号),如果pid和tid相同,则在主线程中crash,name里面则是进程名
pid: 25243, tid: 25260, name: Binder_2 >>> com.tencent.daemon <<<
(4) Terminated signal and fault address信息
可以看到是signal 11导致的crash,访问了非法的地址0x4
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x14
Android中信号量如下所示,只有signal 9可以无条件终止进程:
adb shell kill -l
1 HUP Hangup 33 33 Signal 33
2 INT Interrupt 34 34 Signal 34
3 QUIT Quit 35 35 Signal 35
4 ILL Illegal instruction 36 36 Signal 36
5 TRAP Trap 37 37 Signal 37
6 ABRT Aborted 38 38 Signal 38
7 BUS Bus error 39 39 Signal 39
8 FPE Floating point exception 40 40 Signal 40
9 KILL Killed 41 41 Signal 41
10 USR1 User signal 1 42 42 Signal 42
11 SEGV Segmentation fault 43 43 Signal 43
12 USR2 User signal 2 44 44 Signal 44
13 PIPE Broken pipe 45 45 Signal 45
14 ALRM Alarm clock 46 46 Signal 46
15 TERM Terminated 47 47 Signal 47
16 STKFLT Stack fault 48 48 Signal 48
17 CHLD Child exited 49 49 Signal 49
18 CONT Continue 50 50 Signal 50
19 STOP Stopped (signal) 51 51 Signal 51
20 TSTP Stopped 52 52 Signal 52
21 TTIN Stopped (tty input) 53 53 Signal 53
22 TTOU Stopped (tty output) 54 54 Signal 54
23 URG Urgent I/O condition 55 55 Signal 55
24 XCPU CPU time limit exceeded 56 56 Signal 56
25 XFSZ File size limit exceeded 57 57 Signal 57
26 VTALRM Virtual timer expired 58 58 Signal 58
27 PROF Profiling timer expired 59 59 Signal 59
28 WINCH Window size changed 60 60 Signal 60
29 IO I/O possible 61 61 Signal 61
30 PWR Power failure 62 62 Signal 62
31 SYS Bad system call 63 63 Signal 63
32 32 Signal 32 64 64 Signal 64
下表列举了几个常见的信号量:
信号量 | Value | 描述 |
---|---|---|
SIGABRT | 6 | 通过C函数abort()发送;为assert()使用 |
SIGKILL | 9 | 迅速完全终止进程;不能被捕获 |
SIGFPE | 8 | 浮点数运算错误,如除0操作 |
SIGSEGV | 11 | 段地址错误,例如空指针、野指针、数组越界等 |
SIGPIPE | 13 | 管道错误,例如向没有reader的管道中写,linux中socket断掉后继续写 |
SIGILL | 4 | 非法指令,例如损坏的可执行文件或代码区损坏 |
SIGBUS | 7 | 不存在的物理地址,更多为硬件或系统引起 |
(5) Call Stack信息
调用栈信息记录了程序在Crash前的函数调用关系以及正在执行函数的信息。#00,#01等为函数调用栈中栈帧的编号,编号越小的栈帧表示最近调用的函数信息,所以栈帧标号为#00 表示的是当前正在执行并导致程序crash的函数信息。pc后面的十六进制表示当前函数正在执行语句在共享链接库或可执行文件中的位置,/data/app/com.tencent.daemon-1/lib/arm/libhwnetcore.so表示执行指令在哪个文件中,括号里面注明了对应的函数。
backtrace:
#00 pc 0007ebe0 /data/app/com.tencent.daemon-1/lib/arm/libhwnetcore.so (std::string::_M_assign(char const*, char const*)+7)
#01 pc 001071d7 /data/app/com.tencent.daemon-1/lib/arm/libhwnetcore.so (Java_com_tencent_wechat_HWNetcore_addCommonRequest+30)
#02 pc 001e0e79 /data/dalvik-cache/arm/data@app@com.tencent.daemon-1@base.apk@classes.dex
二、解析native crash堆栈的三种常用方法
为了能正确解析出来crash的堆栈,我们需要保存好obj下面的so,libs目录下的so丢失了调试信息和符号表,不能正确地解析出来原代码。
1、使用ndk-stack
ndk-stack命令从r6版本开始提供,能自动分析tombstone文件,将崩溃时的调用内存地址和c++代码一一对应,位于ndk目录下。
(1)直接利用adb logcat作为input
adb logcat | $NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
(2)解析文件
adb logcat > /tmp/foo.txt (可选,已经有crash file,直接解析即可)
$NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi -dump foo.txt
2、使用arm-linux-androideabi-addr2line
arm-linux-androideabi-addr2line和arm-linux-androideabi-objdump工具需要根据目标机器不同的CPU结构进行选择,位于ndk的交叉编译器工具链目录下。由于我们是Android平台,arm架构,选择arm-linux-androidabi-4.9下的工具即可,这两个工具均位于目录:
$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
addr2line用于将地址转换为文件名称和行号,如果没有地址指定,将从stdin读取。通过addrline -h获得各个参数的含义:
arm-linux-androideabi-addr2line -h
The options are:
@<file> Read options from <file>
-a --addresses Show addresses
-b --target=<bfdname> Set the binary file format
-e --exe=<executable> Set the input file name (default is a.out)
-i --inlines Unwind inlined functions
-j --section=<name> Read section-relative offsets instead of addresses
-p --pretty-print Make the output easier to read for humans
-s --basenames Strip directory names
-f --functions Show function names
-C --demangle[=style] Demangle function names
-h --help Display this information
-v --version Display the program's version
一般用-C选项还原函数名称,-f选项展示函数名称,-e选项指定input file,使用范例如下,0x2c015d为令程序崩溃的汇编指令地址:
./arm-linux-androideabi-addr2line -C -f -e /Users/lily/prj/obj/local/armeabi/libhwnetcore.so 0x2c015d
onStatisticsCallBack(StatisticsValue, std::string, std::string)
/Users/lily/git_project/prj/jni/Java2C.cpp:177 (discriminator 4)
如果不使用-C选项,得到的解析结果就是
_Z20onStatisticsCallBack15StatisticsValueSsSs
/Users/lily/prj/jni/Java2C.cpp:177 (discriminator 4)
3、使用arm-linux-androideabi-objdump
arm-linux-androideabi-objdump与arm-linux-androideabi-addr2line位于同一目录下,用来从二进制文件中展示信息。使用objdump能够定位到出错的函数信息。使用方式为:
./arm-linux-androideabi-objdump <option(s)> <file(s)>
可选参数为:
At least one of the following switches must be given:
-a, --archive-headers Display archive header information
-f, --file-headers Display the contents of the overall file header
-p, --private-headers Display object format specific file header contents
-P, --private=OPT,OPT... Display object format specific contents
-h, --[section-]headers Display the contents of the section headers
-x, --all-headers Display the contents of all headers
-d, --disassemble Display assembler contents of executable sections
-D, --disassemble-all Display assembler contents of all sections
-S, --source Intermix source code with disassembly
-s, --full-contents Display the full contents of all sections requested
-g, --debugging Display debug information in object file
-e, --debugging-tags Display debug information using ctags style
-G, --stabs Display (in raw form) any STABS info in the file
-W[lLiaprmfFsoRt] or
--dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
=frames-interp,=str,=loc,=Ranges,=pubtypes,
=gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
=addr,=cu_index]
Display DWARF info in the file
-t, --syms Display the contents of the symbol table(s)
-T, --dynamic-syms Display the contents of the dynamic symbol table
-r, --reloc Display the relocation entries in the file
-R, --dynamic-reloc Display the dynamic relocation entries in the file
@<file> Read options from <file>
-v, --version Display this program's version number
-i, --info List object formats and architectures supported
-H, --help Display this information
其中-d选项用于展示可执行部分的汇编程序内容,-D展示全部的汇编程序内容,-x选项用于显示所有header的内容,-S选项显示源代码。使用范例如下,使用的so为obj下面的so:
./arm-linux-androideabi-objdump -dx /Users/lily/prj/obj/local/armeabi/libhwnetcore.so > /Users/lily/prj/obj/local/armeabi/dxobjdump.txt
再看一下 0x2c015d这个地址,搜索2c015d,可以看到对应的函数为onStatisticsCallBack
002bffe4 <_Z20onStatisticsCallBack15StatisticsValueSsSs>:
...
2c015c: f043 fa92 bl 303684 <__aeabi_llsl+0x2231c>
...
使用-S -D耗时会多一些:
./arm-linux-androideabi-objdump -S -D /Users/lily/prj/obj/local/armeabi/libhwnetcore.so > /Users/lily/prj/obj/local/armeabi/SDobjdump.txt
附录
从ndk r11开始,Android NDK已经废弃了gcc,Android默认使用clang/llvm, gcc只支持到4.9,但由于google的libc++库还不完善,所以gcc还会继续保留一段时间。
要想使用ndk-build命令或NDK,需要安装 GNU Make 3.81 或更新版本,通过下列命令可以看到GNU Make 3.81已安装。
gnumake --version
GNU Make 3.81
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
This program built for i386-apple-darwin11.3.0
1、ndk-build 常用参数
ndk-build 文件是 Android NDK r4 中引入的一个 shell 脚本。其用途是调用正确的 NDK 构建脚本。可通过内部构建和从命令行调用两种方式使用ndk-build。
(1)内部构建
运行 ndk-build 脚本相当于运行以下命令:
$GNUMAKE -f <ndk>/build/core/build-local.mk
<parameters>
$GNUMAKE 指向 GNU Make 3.81 或更新版本,<ndk> 指向 NDK 安装目录。 可以使用此信息从其他 shell 脚本甚至自己的 Make 文件调用 ndk-build。
范例: 我的ndk目录位于/Users/lily/Library/Android/sdk/ndk-bundle/,进入到proj目录后,调用如下命令即可进行编译
gnumake -f /Users/lily/Library/Android/sdk/ndk-bundle/build/core/build-local.mk
(2)使用命令行
ndk-build 的所有参数将直接传递到运行 NDK 构建脚本的底层 GNU make。
ndk-build <option>
clean 移除之前生成的任意二进制文件
V=1 启动构建,并显示构建命令
-B 强制执行完全的重新构建
NDK_DEBUG=1 强制执行可调试版构建(这个参数很有用,启用这个参数obj下可保留符号表)
NDK_DEBUG=0 强制执行发布
NDK_HOST_32BIT = 1 始终使用32位模式下的工具链(针对某些附带64和32位两个版本的工具链,64位的工具速度更快,能处理更大的程序,更好地利用主机资源)
-C <project> 构建位于<project>的项目路径的原生代码(可不cd切换到项目路径)
-jX X为线程数,ndk-build默认为单线程编译,一般为CPU核数-1
2、指定编译的toolchain
在Application.mk中添加
NDK_TOOLCHAIN_VERSION = 4.9
未指定工具链之前,使用ndk默认的工具链,编译log如下所示
/Users/lily/Library/Android/sdk/ndk-bundle/ndk-build -j8 NDK_DEBUG=1
[armeabi] Compile++ thumb: hwnetcore <= Java2C.cpp
[armeabi] Compile++ thumb: hwnetcore <= CallStack.cpp
[armeabi] Compile++ thumb: hwnetcore <= Java2C_LogLogic.cpp
[armeabi] Compile++ thumb: hwnetcore <= Java2C_Xlog.cpp
[armeabi] Prebuilt : libstlport_shared.so <= <NDK>/sources/cxx-stl/stlport/libs/armeabi/
[armeabi] Gdbserver : [arm-linux-androideabi] libs/armeabi/gdbserver
[armeabi] Gdbsetup : libs/armeabi/gdb.setup
指定了4.9的工具链以后,编译log如下所示,可以清楚地看到使用的工具链
ndk-build -j4 NDK_DEBUG=1
[armeabi] Compile++ thumb: hwnetcore <= Java2C.cpp
[armeabi] Compile++ thumb: hwnetcore <= CallStack.cpp
[armeabi] Compile++ thumb: hwnetcore <= Java2C_LogLogic.cpp
[armeabi] Compile++ thumb: hwnetcore <= Java2C_Xlog.cpp
[armeabi] Prebuilt : libstlport_shared.so <= <NDK>/sources/cxx-stl/stlport/libs/armeabi/
[armeabi] Gdbserver : [arm-linux-androideabi-4.9] libs/armeabi/gdbserver
[armeabi] Gdbsetup : libs/armeabi/gdb.setup
3、与NDK_DEBUG = 1相同效果的debug编译选项
在Application.mk中进行配置
APP_OPTIM := debug
参考文献:
1、google官方ndk-build
2、google官方ndk-stack
3、Android 开发中常见 Crash 的情况
4、ABI百度百科
5、Android平台Native代码的崩溃捕获机制及实现