实践 | 解决GDB无法调试Android Coredump的问题

1,271 阅读8分钟

Coredump作为进程崩溃的内存镜像,经常在排查问题时扮演重要的作用。通过GDB或LLDB来调试Coredump,我们可以获取调用栈、寄存器、变量值等重要信息。由于Android的编译链很早就切到了Clang,因此官方建议的调试工具也由GDB切到了LLDB。既然如此,为何还要纠结用GDB来调试Android的Coredump呢?原因有二:

  1. LLDB在功能上没有GDB全面,譬如memory map信息的输出;另外之前版本中一直有些问题无法解决,譬如"art::mirror::Class"这样的符号无法解析。
  2. LLDB没有很好的GUI工具,目前最好的是VS Code,通过LLDB的插件来可视化呈现。但可视化的体验和GDB的GUI工具仍有差距,譬如Eclipse。

实际上我也好几年没用GDB了。每当我用GDB加载Coredump时,都会有下面的warning提示,然后共享库无法加载,所有的符号信息都获取不到,因此这几年我一直在用LLDB。

warning: Can't open file /memfd:jit-zygote-cache (deleted) during file-backed mapping note processing
warning: Can't open file /memfd:jit-cache (deleted) during file-backed mapping note processing
warning: Can't open file /system/framework/arm64/boot.oat during file-backed mapping note processing
...
warning: Can't open file /system/bin/app_process64 during file-backed mapping note processing

直到最近得空了才想起来解决它。既然GDB源码是开源的,那么就可以自己添加调试代码、重新编译来不断逼近问题的根因。通过几天的调试和阅读源码,最终问题得以解决。

结论

先说结论,问题由如下改动引入,引入的时间为2020年7月份。

image.png

之后由如下改动修复,修复的时间为2024年9月,此改动暂未合入任何release版本,因此想要解决此问题的伙伴需要git clone master分支的最新代码。

image2.png

问题解决很重要,但更重要的是弄清楚它的前因后果。彻底弄清楚后,就能对GDB源码祛魅,这样以后再遇到GDB的问题,就有信心去调试解决。

成因

下面来梳理问题产生的原因。

共享库之所以无法加载,是因为Coredump最初建立的so列表为空。因此,排查的核心就在于理顺so列表的建立过程,以及整个过程中到底哪个环节出了问题。

so列表的建立过程分为三个步骤,如下所示。

so_list建立过程.png

  1. 从app_process64(可执行文件)的地址空间中找到.dynamic section的起始地址。看这个section的名字就可以知道,这里面应该保存了和动态链接相关的一些数据。
llvm-readelf -S app_process64
There are 38 section headers, starting at offset 0x33dc8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000350 000350 000015 00   A  0   0  1
  ...
  [20] .dynamic          DYNAMIC         0000000000008090 008090 000250 10  WA  9   0  8

2. .dynamic section里保存了各种各样的数据,为了方便管理,它给不同的数据标注了不同的tag,譬如DT_NEEDED和DT_SYMTAB。其中DT_DEBUG标注的数据是一个指针值,它指向linker运行时的数据结构,而那里面则包含所有的共享库信息。

/* Dynamic section tags.  */
#define DT_NULL		0
#define DT_NEEDED	1
...
#define DT_SYMTAB	6
...
#define DT_DEBUG	21

3. linker运行时的数据结构位于linker的.data section,其中的共享库信息采用链表结构进行管理。根据上图可以知道,此链表并非单条链表。这是因为linker中有namespace的概念,因此上图中每个方框代表一个namespace,圆圈则代表一个共享库。

通过调试,我发现出问题时第一步获取到的.dynamic section起始地址是0x8090。这显然是个相对地址,而非内存空间可以访问的绝对地址。这个地址的无法访问,也就导致DT_DEBUG里的值读不到,因此后续过程无法完成,so列表自然也无法建立。这个过程的详细描述也可以参考这篇文章

那么问题回到了第一步,为什么.dynamic section的起始地址读取有误?

这牵扯到可执行文件的重定位。重定位会给这些section的相对地址加上一个偏移,使它和真实的内存空间能够吻合上。

Coredump加载时会从两个地方读取程序入口地址,一个地方是ELF文件,此时读取的entry point为相对地址;另一个地方是内存空间,Auxiliary Vector(auxv)作为一种向用户空间传输某些内核级信息的机制,它里面会记录运行时的一些重要信息,其中就包含entry point的绝对地址(运行时地址)。两个地址相减,便可以得到重定位所需的偏移值。

重定位.png

拿到这个偏移值还不算完,还得再做一层校验,以防加载的可执行文件和运行时的可执行文件不匹配,如果校验失败,则把偏移值置为0。这也是为什么.dynamic section起始地址为0x8090的原因,因为它加了个值为0的偏移。

校验的方式是分别从内存和文件中读取它的program header,然后进行比对。然而调试代码惊奇地发现,从内存中读取出来的program header竟然全是0,这显然不合逻辑。

按理说,program header位于只读段,Coredump里不会保存其数据。如果找不到vma对应的文件,那么读取将返回空,而不会是一个被0填满的有效数据。之所以返回0,恰恰是Commit db082f5这笔改动导致的。它会在加载Coredump时根据NT_FILE里的信息去同步加载memory maps里的文件。

CORE                 0x0002dca4	NT_FILE (mapped files)
  Page size: 4096
               Start                 End         Page Offset
  ...
  0x00000060c5fdc000  0x00000060c5fde000  0x0000000000000000
      /system/bin/app_process64
  0x00000060c5fe0000  0x00000060c5fe2000  0x0000000000000004
      /system/bin/app_process64
  0x00000060c5fe4000  0x00000060c5fe5000  0x0000000000000008
      /system/bin/app_process64
  0x00000060c5fe8000  0x00000060c5fe9000  0x000000000000000c
      /system/bin/app_process64

如果它能根据/system/bin/app_process64这种绝对路径在PC上找到匹配的文件,那么就可以将文件的内容装载进来填充只读段。但PC中一般不会存在这种绝对路径,因为它原本是手机里的路径。因此大多数情况下加载都是失败的,这也是如下warning出现的原因。

warning: Can't open file /system/bin/app_process64 during file-backed mapping note processing

但这笔改动吊诡的是,如果在加载失败的这些文件的只读段去读取数据,它并不会返回错误,而是一个被0填满的数据。以上便是问题的根因。

修复的方案主要就是更正了这种返回逻辑,当数据读取失败时,直接返回错误,而不是一个被0填满的数据。

这样的话,从内存里读到的program header为空,重定位的校验便会跳过,因此偏移值得到保留。

对于之前的GDB版本,还有一种work around方案,就是在Coredump加载前通过set sysroot指定root目录,按照绝对路径的格式将可执行文件放到root目录下(如下示例),这样加载memory maps文件信息时就能找到它。

cp app_process64 ~/workspace/test/symbols/system/bin/
set sysroot ~/workspace/test/symbols/

后记

人们遇到问题时,会习惯性地寻求帮助,然后复制已有的解决方案。但真正棘手的问题从来都没有参考答案,这种时候能直面问题,就已经解决了一半的难题。