native crash相关小结

一、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描述
SIGABRT6通过C函数abort()发送;为assert()使用
SIGKILL9迅速完全终止进程;不能被捕获
SIGFPE8浮点数运算错误,如除0操作
SIGSEGV11段地址错误,例如空指针、野指针、数组越界等
SIGPIPE13管道错误,例如向没有reader的管道中写,linux中socket断掉后继续写
SIGILL4非法指令,例如损坏的可执行文件或代码区损坏
SIGBUS7不存在的物理地址,更多为硬件或系统引起

(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代码的崩溃捕获机制及实现

    原文作者:lily君
    原文地址: https://www.jianshu.com/p/e2fab762fff5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞