Android 如何解读 Native 崩溃栈信息

Android 如何解读 Native 崩溃栈信息

在开始本篇正文之前,希望你对 ELF 格式的文件有基本的了解,如果没有相关的了解,可能对阅读本篇文章有一些困难,我之前写过相应的文章,不懂 ELF 格式的可以看看:

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

大部分的 Android 开发者使用的主要语言都是 Kotlin / Java,他们的崩溃栈信息非常清晰,页非常好定位到问题,如果是线上的崩溃通常还会对类名进行混淆,还需要一个混淆文件对堆栈翻译一下就能够得到源码中的类名。

但是很多人对 C/C++ 的崩溃栈就无能为力了,今天这篇文章就来扒一扒 Native 的崩溃栈信息。

Native 崩溃栈信息

我们经常能够看到有类似下面的 Native 崩溃信息:

shell 复制代码
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 包就没有抹掉符号表,如果是有符号表信息,我们看到的上面异常信息通常是下面这样的:

shell 复制代码
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 中添加以下配置来避免符号表被移除:

groovy 复制代码
// ...
packagingOptions {
    doNotStrip "*/arm64-v8a/*.so"
    doNotStrip "*/armeabi-v7a/*.so"
    doNotStrip "*/x86/*.so"
    doNotStrip "*/x86_64/*.so"
}
// ...

如果符号表被移除了那我们不是永远都不知道崩溃的方法是什么了?当然不是,被移除的符号会被保存到另外的文件中,线上的崩溃可以通过这个文件再次翻译成对应的方法。以下就是符号文件对应的路径:

它解压后如下:

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

那么我们怎么判断一个 .so 文件是不是有符号表呢?可以通过 file 命令查看:

text 复制代码
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 就表示有符号表。

text 复制代码
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] 读取符号表:

以下是有符号表的数据:

text 复制代码
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):

text 复制代码
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] 命令可以反编译机器码到汇编) 后找到了这个地址所在的方法:

text 复制代码
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 的崩溃信息和符号表有一个全新的认识。

相关推荐
LSL666_2 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton5 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_940094027 小时前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器
下位子7 小时前
『OpenGL学习滤镜相机』- Day9: CameraX 基础集成
android·opengl
参宿四南河三9 小时前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我10 小时前
mmkv的 mmap 的理解
android
没有了遇见10 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong11 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强11 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸11 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试