Android 怎样解读 Native 溃散栈信息

在开端本篇正文之前,期望你对 ELF 格局的文件有根本的了解,假如没有相关的了解,或许对阅览本篇文章有一些困难,我之前写过相应的文章,不明白 ELF 格局的能够看看:

关于 ELF 格局文件的笔记(一)
关于 ELF 格局文件的笔记(二)
关于 ELF 格局文件的笔记(三)

大部分的 Android 开发者运用的首要语言都是 Kotlin / Java,他们的溃散栈信息十分明晰,也十分好定位到问题,假如是线上的溃散一般还会对类名进行混杂,还需要一个混杂文件对仓库翻译一下就能够得到源码中的类名。
但是很多人对 C/C++ 的溃散栈就力不从心了,今天这篇文章就来扒一扒 Native 的溃散栈信息。

Native 溃散栈信息

咱们常常能够看到有相似下面的 Native 溃散信息:

Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 17356 (tMediaPlayerDec), pid 15253 (ediaplayer.demo)
pid: 15253, tid: 17356, name: tMediaPlayerDec  >>> com.tans.tmediaplayer.demo <<<

#01 pc 000000000001bd2c  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#02 pc 000000000001ba98  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#03 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#04 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#05 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#06 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#07 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so
#08 pc 000000000001cf08  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (Java_com_tans_tmediaplayer_tMediaPlayer_decodeNative+52) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#11 pc 000000000000952c  [anon:dalvik-classes5.dex extracted in memory from /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/base.apk!classes5.dex] (com.tans.tmediaplayer.tMediaPlayer.decodeNativeInternal$tmediaplayer_debug+0)
#13 pc 00000000000057f6  [anon:dalvik-classes5.dex extracted in memory from /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/base.apk!classes5.dex] (com.tans.tmediaplayer.tMediaPlayerDecoder$decoderHandler$2$1.dispatchMessage+850)

Native 中的溃散是经过体系信号来完成的,比方咱们上面的反常信号便是 SIGABRTAndroid 的进程在启动时就会增加一个 SignalCatcher,来捕获信号,不同的信号有不同的处理方式,SIGABRT 便是会直接退出程序,也便是咱们常说的溃散,Android 中还有一个十分重要的信号便是 SIGQUIT,在 Android 中表明发生了 ANR,默许的处理逻辑是 dump 栈信息和内存 GC 相关的信息到本地文件。
好了这里说得有点远了,回到上面的问题,咱们刚开端看到上面的数据或许有点懵逼,pc 后边有一串 16 进制的数字表明程序计数器的方位 (简单了解便是履行的机器码对应的方位),后边的文件表明溃散的栈中相关的 .so 动态链接库。但是你又要说了,这一串地址谁能够看出什么问题啊?️ 你先不要急,一般线上的用户溃散看到的栈是这样的,由于 AndroidRelease 包默许会抹掉一部分叫做符号表的东西,假如你看过我上面的文章你就会豁然开朗,这个符号表描绘了指令地址和对应办法或许变量的映射(办法名,全局变量名都是符号),一般咱们用的别人的 .so 包也会抹掉符号表(这或许便是不想让你看,起到了一个混杂效果),少了一个表在线上的运转中性能会更加好(至少这部分内存不用消耗了)。

一般咱们自己打的 Debug 包就没有抹掉符号表,假如是有符号表信息,咱们看到的上面反常信息一般是下面这样的:

Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 17356 (tMediaPlayerDec), pid 15253 (ediaplayer.demo)
pid: 15253, tid: 17356, name: tMediaPlayerDec  >>> com.tans.tmediaplayer.demo <<<

#01 pc 000000000001bd2c  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::parseDecodeAudioFrameToBuffer(tMediaDecodeBuffer*)+464) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#02 pc 000000000001ba98  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+1076) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#03 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+532) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#04 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+532) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#05 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+532) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#06 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+532) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#07 pc 000000000001b878  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (tMediaPlayerContext::decode(tMediaDecodeBuffer*)+532) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#08 pc 000000000001cf08  /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/lib/arm64/libtmediaplayer.so (Java_com_tans_tmediaplayer_tMediaPlayer_decodeNative+52) (BuildId: 58ab2061a06db613d9c3ca66a214872ad88636f7)
#11 pc 000000000000952c  [anon:dalvik-classes5.dex extracted in memory from /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/base.apk!classes5.dex] (com.tans.tmediaplayer.tMediaPlayer.decodeNativeInternal$tmediaplayer_debug+0)
#13 pc 00000000000057f6  [anon:dalvik-classes5.dex extracted in memory from /data/app/~~fIT1aTQ88Pxdg1-7Ax4AXQ==/com.tans.tmediaplayer.demo-8WVpgXB0od_2YBBfM4FnZQ==/base.apk!classes5.dex] (com.tans.tmediaplayer.tMediaPlayerDecoder$decoderHandler$2$1.dispatchMessage+850)

你看这里就有调用所对应的办法了,由于我这里的 decode() 办法是递归调用的,所以你看到上面的栈中有多个,办法后边还有一个 +532 表明该地址离办法开端的地址的偏移量,假如你的 .so 文件里边还有 debug 信息,这个 +532 能够定位到某一行 C C++ 源码,其实便是每条指令都映射了某一行代码。

Android 的打包过程中假如你期望 Release 包也不要移除符号表信息,能够经过在 build.gradle 中增加以下配置来防止符号表被移除:

// ...
packagingOptions {
    doNotStrip "*/arm64-v8a/*.so"
    doNotStrip "*/armeabi-v7a/*.so"
    doNotStrip "*/x86/*.so"
    doNotStrip "*/x86_64/*.so"
}
// ...

假如符号表被移除了那咱们不是永远都不知道溃散的办法是什么了?当然不是,被移除的符号会被保存到别的的文件中,线上的溃散能够经过这个文件再次翻译成对应的办法。以下便是符号文件对应的途径:

Android 怎样解读 Native 溃散栈信息

它解压后如下:

Android 怎样解读 Native 溃散栈信息

他们也是 ELF 格局的文件,每个都对应了一个 .so 库。

那么咱们怎样判别一个 .so 文件是不是有符号表呢?能够经过 file 命令检查:

libtmediaplayer.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=58ab2061a06db613d9c3ca66a214872ad88636f7, with debug_info, not stripped

not stripped 就表明有符号表。

libtmediaplayer.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=58ab2061a06db613d9c3ca66a214872ad88636f7, stripped

stripped 就表明没有符号表。

Tips: 假如你上架的应用没有这个符号表,Google Play 还会提示你上传,Google Play 是能够帮你捕获 Native 溃散的,它需要符号表解析这些地址信息。

符号表

咱们了解 ELF 文件就知道,咱们上面说的符号表便是本地符号表对应的便是 .symtab Section,这个表对咱们的程序运转没有任何的影响,咱们调用本地办法都是经过地址直接跳转,而不需要本地符号表。
还有一个符号表是 .dynsym,它是动态链接的符号表,这个表是供 ld.so 运用的,由于这里边的符号都是露出给其他的程序调用的,ld.so 需要经过这个表去查询露出给其他程序的符号的地址,所以不能删除。

咱们能够经过 readelf -s [elf file] 读取符号表:

以下是有符号表的数据:

Symbol table '.dynsym' contains 447 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   // ...
   441: 000000000001c494    40 FUNC    GLOBAL DEFAULT   15 Java_com_tans_tmediaplayer_tMediaPlayer_durationNative
   442: 000000000001cac4   136 FUNC    GLOBAL DEFAULT   15 Java_com_tans_tmediaplayer_tMediaPlayer_getVideoFrameUBytesNative
   // ...
Symbol table '.symtab' contains 2470 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
  // ...
  2067: 000000000001ae20  2116 FUNC    GLOBAL DEFAULT   15 _ZN19tMediaPlayerContext29parseDecodeVideoFrameToBufferEP18tMediaDecodeBuffer
  // ...
  2086: 000000000001bef4    36 FUNC    WEAK   DEFAULT   15 _ZN18tMediaDecodeBufferC2Ev
  2087: 000000000001bf18    28 FUNC    WEAK   DEFAULT   15 _ZN17tMediaAudioBufferC2Ev
  2088: 000000000001bf34    80 FUNC    WEAK   DEFAULT   15 _ZN17tMediaVideoBufferC2Ev
  2089: 000000000001bf84   360 FUNC    GLOBAL DEFAULT   15 _Z16freeDecodeBufferP18tMediaDecodeBuffer
  2090: 000000000001c0ec   336 FUNC    GLOBAL DEFAULT   15 _ZN19tMediaPlayerContext7releaseEv
  // ...

假如没有本地符号表就只有以下信息(少了 .symtab):

Symbol table '.dynsym' contains 447 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   // ...
   441: 000000000001c494    40 FUNC    GLOBAL DEFAULT   15 Java_com_tans_tmediaplayer_tMediaPlayer_durationNative
   442: 000000000001cac4   136 FUNC    GLOBAL DEFAULT   15 Java_com_tans_tmediaplayer_tMediaPlayer_getVideoFrameUBytesNative
   // ...

咱们再来看看上面的那个溃散栈地址 000000000001bd2c,我在 反编译 .text 代码(.text Section 便是用来存储代码的,经过 objdump --dissassemble --section=.text [elf file] 命令能够反编译机器码到汇编) 后找到了这个地址地点的办法:

000000000001bb5c:
   1bb5c: ff 83 01 d1  	sub	sp, sp, #96
   // ...
   1bd2c: 11 7a 00 94  	bl	0x3a570 <abort@plt>
   1bd30: 44 79 00 94  	bl	0x3a240 <__stack_chk_fail@plt>

办法的指令有点长,我省略了大部分,咱们看到 1bd2c 处调用了 abort() 办法,这个办法便是用来发送 SIGABORT 的,这是我测验时增加的,咱们再来看看 1bb5c 在符号表中对应的是哪个符号,正好便是 _ZN19tMediaPlayerContext29parseDecodeVideoFrameToBufferEP18tMediaDecodeBuffer 办法,哈哈。

最后

本篇文章介绍了符号表,还经过溃散栈中的地址,在符号表中去查询到了咱们对应的办法,期望你对 Native 的溃散信息和符号表有一个全新的认识。