在我们的日常开发中, Java 的堆栈崩溃信息相对来说是比较好排查定位的. 可以直接通过 logcat 堆栈信息较为明确的跟踪到出现问题的源代码对应行号. 但是要进阶 Android 避免不了要和 native 打交道.
我们来看一个 native 崩溃日志(后文实践都以这个日志为准)
上图这是啥啊, 看着一脸懵逼.到底是啥呀?
还有 bugly 上大量的 native 相关的 crash 经常也是搁置在那. 对于第三方提供的 native 也还好我们能够直接将捕获的日志提供给对应开发, 如果是我们自研自己维护的第三方库到底对于 native crash 如何分析, 如何定位到出问题的 c/cpp 代码?
native crash 日志分析
熟悉 NDK JNI 开发的小伙伴应该知道 addr2line, 我们来通过前文第一个 native crash 来回顾一下如何通过 addr2line 来分析, 以及 native 日志
2023-02-24 10:35:12.289 23285-23285/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2023-02-24 10:35:12.289 23285-23285/? A/DEBUG: Build fingerprint: 'Lenovo/LenovoTB-X606F/X606F:10/QP1A.190711.020/TB-X606F_USR_S300451_2111291201_V9.56_BMP_PRC:user/release-keys'
2023-02-24 10:35:12.289 23285-23285/? A/DEBUG: Revision: '0'
2023-02-24 10:35:12.289 23285-23285/? A/DEBUG: ABI: 'arm'
2023-02-24 10:35:12.290 23285-23285/? A/DEBUG: Timestamp: 2023-02-24 10:35:12+0800
2023-02-24 10:35:12.290 23285-23285/? A/DEBUG: pid: 23226, tid: 23226, name: testappwithlame >>> com.icebear.testappwithlame <<<
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: uid: 10210
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: Abort message: 'FORTIFY: fread: null FILE*'
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: r0 00000000 r1 00005aba r2 00000006 r3 ff9aa9c8
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: r4 ff9aa9dc r5 ff9aa9c0 r6 00005aba r7 0000016b
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: r8 ff9aa9d8 r9 ff9aa9c8 r10 ff9aa9f8 r11 ff9aa9e8
2023-02-24 10:35:12.291 23285-23285/? A/DEBUG: ip 00005aba sp ff9aa998 lr eef982c3 pc eef982d6
2023-02-24 10:35:12.320 462-520/? I/hwcomposer: [HWCDisplay] [Display_0 (type:1)] fps:5.919848,dur:1013.54,max:828.91,min:15.84
2023-02-24 10:35:12.372 489-489/? I/SurfaceFlinger: screenshot (com.icebear.testappwithlame/com.icebear.testappwithlame.MainActivity#0)
2023-02-24 10:35:12.711 23285-23285/? A/DEBUG: backtrace:
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #00 pc 0005f2d6 /apex/com.android.runtime/lib/bionic/libc.so (abort+166) (BuildId: 4431b35d55c1d9b5f8568451110b8281)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #01 pc 0007a873 /apex/com.android.runtime/lib/bionic/libc.so (__fortify_fatal(char const*, ...)+26) (BuildId: 4431b35d55c1d9b5f8568451110b8281)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #02 pc 000a1e73 /apex/com.android.runtime/lib/bionic/libc.so (fread+70) (BuildId: 4431b35d55c1d9b5f8568451110b8281)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #03 pc 0000fcb9 /data/app/com.icebear.testappwithlame-GjAEdrlNIXx6f-H1oLQ76Q==/lib/arm/libnative-lib.so (Mp3Encoder::Encode()+176) (BuildId: b263b919f59c0f28e2aad7ae05c133c4b0970604)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #04 pc 00010027 /data/app/com.icebear.testappwithlame-GjAEdrlNIXx6f-H1oLQ76Q==/lib/arm/libnative-lib.so (Java_com_icebear_testappwithlame_lame_Mp3Encoder_encode+18) (BuildId: b263b919f59c0f28e2aad7ae05c133c4b0970604)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #05 pc 000dc519 /apex/com.android.runtime/lib/libart.so (art_quick_generic_jni_trampoline+40) (BuildId: f1506223e2f05f1cab06d9998d63b921)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #06 pc 000d7bc5 /apex/com.android.runtime/lib/libart.so (art_quick_invoke_stub_internal+68) (BuildId: f1506223e2f05f1cab06d9998d63b921)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #07 pc 0042e3a7 /apex/com.android.runtime/lib/libart.so (art_quick_invoke_stub+250) (BuildId: f1506223e2f05f1cab06d9998d63b921)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #08 pc 000dffb7 /apex/com.android.runtime/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+174) (BuildId: f1506223e2f05f1cab06d9998d63b921)
复制代码
根据上述的 native crash 我们先来提取几个关键信息:
- 包名
- signal 6 (SIGABRT), code -1 (SI_QUEUE)
- 架构 "arm"
- 产生问题的 so
.so文件 : 动态链接库(shared object library)在 linux 里以.so结尾,c/c++ 在 Linux 下编译生成的文件,Android 下可以通过 ndk-build 将 .c .h .mk 文件编译成 so 文件
通过上述关键信息进行提炼分析, 1 我们大致得知关键 com.android.runtime/lib/libart.so 应该是 Android 系统 so 文件 2 再根据包名 可以定位两行关键的日志
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #03 pc 0000fcb9 /data/app/com.icebear.testappwithlame-GjAEdrlNIXx6f-H1oLQ76Q==/lib/arm/libnative-lib.so (Mp3Encoder::Encode()+176) (BuildId: b263b919f59c0f28e2aad7ae05c133c4b0970604)
2023-02-24 10:35:12.712 23285-23285/? A/DEBUG: #04 pc 00010027 /data/app/com.icebear.testappwithlame-GjAEdrlNIXx6f-H1oLQ76Q==/lib/arm/libnative-lib.so (Java_com_icebear_testappwithlame_lame_Mp3Encoder_encode+18) (BuildId: b263b919f59c0f28e2aad7ae05c133c4b0970604)
复制代码
我们接着来分析剩下两行关键日志 通过
(Mp3Encoder::Encode()+176
Mp3Encoder_encode+18
复制代码
我们隐约能看出来是 Mp3Encoder 的问题 CPP 文件也确实有这个类
+176 +18 是什么, 这个 cpp 文件一共才 74 行, 显然不是行号. 到这线索断了.
0000fcb9 , 00010027 又是什么呢?
符号表
Android 动态库符号表是Android NDK编译环境中用来加载和找到共享库中的符号的一个文件。它的主要功能是把编译的公用函数映射到程序中,使程序可以正确调用这些公用函数
符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:
<起始地址> <结束地址> <函数> [<文件名:行号>] 对于native crash,为了能快速并准确地定位用户APP发生Crash的代码位置,Bugly使用符号表对APP发生Crash的程序堆栈进行解析和还原。 举一个例子:
Debug SO文件是指具有调试信息的SO文件,其中包含用户还原堆栈的符号信息。
为了方便找回Crash对应的Debug SO文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好Debug SO文件。
CMake编译项目: 默认情况下,Debug编译的Debug SO文件将位于: <项目文件夹>/<Module>/build/intermediates/cmake/debug/obj/local<架构>/
PS:符号表部分描述来自 bugly 文档
找到 addr2line
/[NDK Path]/toolchains/[cpu架构]/prebuilt/darwin-x86_64/bin
复制代码
执行
/.../arm-linux-androideabi-addr2line -C -f -e xxx.so 0003deb4
复制代码
addr2line 帮助命令:
arm-linux-androideabi-addr2line [option(s)] [addr(s)]
Convert addresses into line number/file name pairs.
If no addresses are specified on the command line, they will be read from stdin
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
复制代码
实际案例:
我们可以清晰的看到, 通过 ndk 的 addr2line 工具我们把发生 native 问题的对应的 c/cpp 代码精准定位到,排查解决问题.
注意:在实际操作中选择 NDK 工具链 addr2line 我们一定要注意选中对应手机的 CPU 架构, 可以根据 native 的堆栈信息进行获取, 或者还可以通过 adb shell cat /proc/cpuinfo 命令来查询
不仅仅于此
通过前文,我们已经对一些排查 Native Crash 的常规手段基本概念进行了掌握,也有了一个实操案例.到这结束了吗?当然不仅仅于此, 这样和网上千篇一律的文章没有优势. 接下来进阶了:
使用 ndk objdump 工具将带符号表 .so 文件转成汇编 .asm 文件:
那 .asm 文件又是什么, 恰巧 asm 和 Android ASM 插桩撞名了,我们搜索到的一些资料都是关于 ASM 插桩介绍. 暂且认为 : ASM是汇编语言源程序的扩展名
命令:
/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
./arm-linux-androideabi-objdump -S xxx.so > xxx.asm
复制代码
通过上图操作我们得到了 asm 文件 . 比原 so 文件大了不少, 我大致理解这是一个 decode 的动作
so 有了 asm 也拿到了, 我们把 crash 日志准备好(crash 日志可以随意抓取不用特殊整理,只需要包含 crash信息), 接下来的步骤我们需要 python 环境, 来执行如下脚本
**import** sys
**import** re
**import** os
sohead = re.compile('(.+\.so):')
funchead = re.compile('([0-9a-f]{8}) <(.+)>:')
funcline = re.compile('^[ ]+([0-9a-f]+):.+')
**def** parsestack( lines, libname ):
crashline = re.compile('.+pc.([0-9a-f]{8}).+%s' % libname )
ret = []
**for** l **in** lines:
m = crashline.match(l)
**if** m:
addr = m.groups()[0]
ret.append(int(addr,16))
**return** ret
**def** parseasm( lines ):
ret = []
current = **None**
restartcode = **False**;
**for** l **in** lines:
m = funchead.match(l)
**if** m:
**if** current:
ret.append(current)
startaddr, funcname = m.groups()
current = [ funcname, int(startaddr,16), int(startaddr,16), int(startaddr,16), [] ]
**continue**
m = funcline.match(l)
**if** m:
restartcode = **True**;
addr = m.groups()[0]
**if** current != **None**:
current[3] = int(addr,16)
**continue**
m = sohead.match(l)
**if** m:
so = m.groups()[0]
so = os.path.split(so)[1]
**continue**
#Assume anything else is a code line
**if** restartcode:
# print 'XXX',l
restartcode = **False**;
ret.append(current)
current = [ current[0], current[1], current[3], current[3], [] ]
**if** current != **None**:
current[4].append(l);
**return** so, ret
**if** __name__=="__main__":
asm, stack = sys.argv[1],sys.argv[2]
libname, asm = parseasm( file(asm).read().split('\n') )
stack = parsestack( file(stack).read().split('\n'), libname )
**for** addr **in** stack:
**for** func, funcstart, segstart, segend, code **in** asm:
**if** addr >= segstart **and** addr <= segend:
print "0x%08x:%32s + 0x%04x %s" % ( addr, func, addr-funcstart, "".join(["\n"+x **for** x **in** code]))
复制代码
执行: python parse_stack.py file.asm crashlog.txt
这样我们就从一个可以是很大的文件中提取出关键信息, 快速定位排查 native 崩溃问题
发散扩展
实际上一些和 native 打交道少的测试和 Android 技术人员不了解甚至没有听说过 符号表或addr2line. asm 这样对大众不友好, 如果我们能将上述的方式通过 CI/CD(jenkins) 跑成流程, 不需要知道任何符号表或addr2line相关 NDK 的测试或者技术支持人员, 只需要抓到对应 native 崩溃日志上传执行自动化流程自动分析, 应该可以较大的节省 native 问题的寻因定位成本.