Android gdb调试原生代码

一、前言

  • 之前的文章介绍了Android 动态调试so文件,在最近的工作中,又使用gdb无源码调试了原生代码的进程,这里记录一下。
  • Google对使用gdb调试Android原生代码也有说明,不过在其中使用了Google自己编写的一个gdbclient.py脚本,这种方法Google说的已经比较清晰了,可以自己去看。我们这里着重介绍在没有源代码的情况下(例如OEM开发的原生代码)直接使用gdb进行调试的方法。

二、准备工作

  • 使用gdb进行嵌入式调试的必需品是gdb和gdbserver二进制文件,对于Android平台而言,Google已经提供了预编译的版本,所以无需自行编译它,你可以在Android SDK的目录下找到它们
%ANDROID_SDK_PATH%/ndk/%NDK_VERSION%/prebuilt/windows-x86_64/bin/gdb.exe
%ANDROID_SDK_PATH%/ndk/%NDK_VERSION%/prebuilt/android-arm64/gdbserver/gdbserver
%ANDROID_SDK_PATH%/ndk/%NDK_VERSION%/prebuilt/android-arm/gdbserver/gdbserver
  • 同时对Android手机需要做的准备工作,则是打开开发者选项以启用ADB调试,并且需要具有root权限。同时在某些机型上,SELinux可能会阻止gdbserver附加到目标进程,这种情况下建议暂时将SELinux改为宽容模式:
adb shell setenforce 0
  • 最关键的一步当然是要在目标设备上获取一份被调试目标的二进制文件,并把它保存在你的电脑上,由于需要对汇编代码的逻辑进行分析并计算偏移地址,所以你还需要使用诸如IDA Pro这样的反汇编器打开你的调试目标。
  • 同时你还需要设置端口转发,这里使用的端口在后续将作为gdbserver的监听端口,这里可以使用任意合法的且未被其他本地进程占用的端口
adb forward tcp:8129 tcp:8129

三、附加进程方式

  • 在手机上启动gdbserver并附加到目标进程
./gdbserver :8129 --attach $(pidof process_name)
  • 在PC上启动gdb
gdb.exe "path-to-your-elf/elf_name"

四、自行启动进程方式

  • 有时因为某些原因,需要自行启动一个进程进行调试,而不是直接附加到现有进程,参数就直接附加在后面,就像正常启动进程一样。
./gdbserver :8129 elf_name params
  • 在PC上启动gdb的方法是相同的
gdb.exe "path-to-your-elf/elf_name"

五、计算断点位置

  • 由于现代Android操作系统都配备有地址空间布局随机化(ASLR),以至于每次运行时使用的基址都是不同的,所以我们需要在每次运行时候获取它:
cat /proc/$(pidof process_name)/maps | grep module_name
  • 我们需要找到一个有执行权限x的模块,并且记录行开头的十六进制格式地址,例如下面可执行的lib_sth.so的基址是0x701554b000,用这个地址加上IDA中代码相对于.text段的偏移地址,即可得到实际运行时候的虚拟地址。
701554b000-7015568000 --xp 0000b000 fd:00 29062776  /system/lib64/lib_sth.so
  • 这里要注意,如上所示该模块是只有执行权限没有读取权限的。对于Android 10而言,部分代码段启用了只执行内存,所以你可能无法使用IDA Pro这样的工具进行调试,只能使用gdb。参见:AArch64 二进制文件的只执行内存 (XOM)
  • 计算出地址之后,就可以设置断点,并且继续运行直到触发断点
(gdb) b *0x701554b000+0x129
Breakpoint 1 at 0x701554b129
(gdb) c
Continuing.
Thread 1 "process_name" hit Breakpoint 1, 0x000000701554b129 in ??() from target:/system/lib64/lib_sth.so
(gdb)

六、在进程崩溃时定位问题

  • 有时可能需要调试进程崩溃的问题,这时候可以在gdb中捕获到相应的信号:
Thread 1 "process_name" received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ??()
(gdb)
  • 这时可以使用各类gdb命令查看调用栈、查看寄存器和内存等方法,查找崩溃的原因。
  • 有时候在进程崩溃的情况下需要进行溯源,这时候需要在崩溃之前设置断点,但由于ASLR的存在,断点设置的时机又必须在库加载之后,否则无法观察到其基址,这时候可以先正常启动进程,再设置catchpoint,以在目标库加载完毕后暂停。这样就可以查看基址并且设置正常的断点了:
0x0000007fbf6381d0 in __dl__start() from target:/system/bin/linker64
(gdb) catch load lib_sth.so
Catchpoint 1 (load)
(gdb) c
Continuing.
Reading ...
Thread 1 "process_name" hit Catchpoint 1
  Inferior loaded target:/system/lib64/lib_sth.so
0x0000007fbf6381c8 in __dl_rtld_db_dlactivity() from target:/system/bin/linker64
(gdb)
  • 如上我们在lib_sth.so库加载之后进行了暂停,并且还没有到达触发崩溃的代码,这时候可以直接按上述方法计算断点位置并且设置断点即可。

七、总结

  • 本文介绍了Android gdb调试原生代码的方法,该方法几乎适用于除内核代码之外的各类原生代码,综合之前针对apk的各类调试技巧,我们已经掌握了调试Android各类代码的基本技能。