八年Android不知道 addr2line? 动态库符号表?

在我们的日常开发中, Java 的堆栈崩溃信息相对来说是比较好排查定位的. 可以直接通过 logcat 堆栈信息较为明确的跟踪到出现问题的源代码对应行号. 但是要进阶 Android 避免不了要和 native 打交道.

我们来看一个 native 崩溃日志(后文实践都以这个日志为准)

image.png

上图这是啥啊, 看着一脸懵逼.到底是啥呀?

image.png

还有 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 文件也确实有这个类

image.png

+176 +18 是什么, 这个 cpp 文件一共才 74 行, 显然不是行号. 到这线索断了.

0000fcb9 , 00010027 又是什么呢?

符号表

Android 动态库符号表是Android NDK编译环境中用来加载和找到共享库中的符号的一个文件。它的主要功能是把编译的公用函数映射到程序中,使程序可以正确调用这些公用函数

符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:

<起始地址> <结束地址> <函数> [<文件名:行号>] 对于native crash,为了能快速并准确地定位用户APP发生Crash的代码位置,Bugly使用符号表对APP发生Crash的程序堆栈进行解析还原。 举一个例子:

image.png

Debug SO文件是指具有调试信息的SO文件,其中包含用户还原堆栈的符号信息。

为了方便找回Crash对应的Debug SO文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好Debug SO文件。

CMake编译项目: 默认情况下,Debug编译的Debug SO文件将位于: <项目文件夹>/<Module>/build/intermediates/cmake/debug/obj/local<架构>/

PS:符号表部分描述来自 bugly 文档

image.png

找到 addr2line

/[NDK Path]/toolchains/[cpu架构]/prebuilt/darwin-x86_64/bin
复制代码

image.png

执行

/.../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

复制代码

实际案例:

image.png

我们可以清晰的看到, 通过 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
复制代码

image.png

通过上图操作我们得到了 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]))


复制代码

image.png

执行: python parse_stack.py file.asm crashlog.txt

image.png

这样我们就从一个可以是很大的文件中提取出关键信息, 快速定位排查 native 崩溃问题

发散扩展

实际上一些和 native 打交道少的测试和 Android 技术人员不了解甚至没有听说过 符号表或addr2line. asm 这样对大众不友好, 如果我们能将上述的方式通过 CI/CD(jenkins) 跑成流程, 不需要知道任何符号表或addr2line相关 NDK 的测试或者技术支持人员, 只需要抓到对应 native 崩溃日志上传执行自动化流程自动分析, 应该可以较大的节省 native 问题的寻因定位成本.

分类:
Android
标签: