Android 调试速成课程

撰写于 2023 年 10 月 10 日,作者:Nicholas Lim (niclimcy) & Nolen Johnson (npjohnson)

An ocean scene with floating holes of blank space that should be filled. Large light teal shapes, the Lineage logo, and the word Engineering sit on top.

术语表

  • ADB: Android 调试桥。
  • 缓冲区 (Buffer): 内存中固定大小的存储区域。
  • CLI: 命令行界面。
  • 提交 (Commits): 代码库的原子性更改,用于版本控制。
  • 调试 (Debugging): 查找和修复错误、漏洞和意外行为的过程。
  • 设备块文件 (Device block files): /dev 目录中的特殊文件,允许与内核驱动程序进行标准化交互。
  • DTS: 设备树源。
  • EDL: 高通紧急下载模式。
  • gdb: GNU 调试器。
  • HAL: 硬件抽象层。
  • 内核空间 (Kernel Space): 内核运行并与设备驱动程序交互的空间。
  • 日志记录 (Logging): 记录和存储运行软件时发生的事件,例如错误消息、警告和调试信息。
  • 内存地址 (Memory Address): 唯一标识符,用于指定内存中存储数据或指令的位置。
  • OEM: 原始设备制造商(例如,谷歌、Fairphone、三星等)。
  • PID: 进程 ID。
  • pstore: 持久存储。
  • 变基 (Rebase): 将提交从一个分支移动到另一个分支的过程。
  • 堆栈跟踪 (Stack Trace): 显示导致程序中错误或异常的函数调用序列。
  • TID: 线程 ID。
  • UART: 通用异步收发器。
  • 用户空间 (User Space): 正常用户进程(例如应用程序)运行的空间。

什么是调试?

要理解 Android 调试,重要的是要了解 Android 系统的不同部分。 从高层次来看,Android 系统由三个主要组件组成:应用、平台和内核。

Android Stack

用户空间调试

用户空间调试允许我们查找和修复应用和平台问题。 如果我们使用正确的工具,这个过程在 Android 上可能相当简单。

ADB

Android 调试桥 (ADB) 允许我们访问设备的 CLI(或 shell),从而让我们使用 Logcat 等原生调试工具。 请参阅我们的 wiki,了解如何开始在您的设备上使用 ADB 和 fastboot

logcat

logcat 是一个 CLI 工具,用于输出系统消息的日志,包括您使用 Log 类从您的应用写入的消息。

logcat 进程存储了各种循环缓冲区,可以使用 -b 选项访问它们,以下是可用的选项

  • radio: 查看包含无线电/电话相关消息的缓冲区。
  • events: 查看已解释的二进制系统事件缓冲区消息。
  • main: 查看主日志缓冲区(默认),其中不包含系统和崩溃日志消息。
  • system: 查看系统日志缓冲区(默认)。
  • crash: 查看崩溃日志缓冲区(默认)。
  • all: 查看所有缓冲区。
  • default: 报告 main、system 和 crash 缓冲区。

您可以在 Android 开发者上找到更多关于如何使用 logcat 的信息。

logcat 解释崩溃缓冲区

$ adb logcat -b crash
+--------------------+-----+-----+-------+-----------------------------------------------------------------------+
| Date  Time         | PID | TID | Level | ProcessName   : Message                                               |
+--------------------+-----+-----+-------+-----------------------------------------------------------------------+
| 04-14 11:22:34.256 | 5199| 5199| E     | AndroidRuntime: FATAL EXCEPTION: main                                 |
| 04-14 11:22:34.256 | 5199| 5199| E     | AndroidRuntime: Process: com.android.settings, PID: 5199              |
| 04-14 11:22:34.256 | 5199| 5199| E     | AndroidRuntime: java.lang.RuntimeException: Unable to resume activity |
|                    |     |     |       |            {com.android.settings/com.android.settings.SubSettings}:   |
|                    |     |     |       |            java.lang.ArrayIndexOutOfBoundsException length=7; index=7 |
+--------------------+-----+-----+-------+-----------------------------------------------------------------------+

崩溃缓冲区对于调试应用崩溃(例如,“设置”已停止)、识别运行时错误特别有用。

Tombstones

有时 ADB 服务可能未运行(可能的原因包括系统进程导致在 adb 启动之前重启)。 在这种情况下,我们无法访问 logcat 命令。 别担心,tombstone 文件会写入 /data/tombstones,其中包含导致崩溃的堆栈跟踪。

Tombstones 也更详细,如果 logcat 输出不足,则提供更长的堆栈跟踪。 因此,也可以使用以下命令从正在运行的进程导出 tombstone

$ adb shell debuggerd {PID}

提示:将 {PID} 替换为您想要进程的实际进程 ID。

Stack

stack 是一个 Python 脚本,以人类可读的格式表示崩溃转储(符号化原生崩溃转储)。 您可以在 LineageOS 仓库的任何本地同步中找到 stack,路径为 ~/android/lineage/development/scripts/stack。 您可以使用 stack < /path/to/tombstone_0 在提取的 tombstone 上运行 stack。

原生崩溃转储通常如下所示

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Android/aosp_angler/angler:7.1.1/NYC/enh12211018:eng/test-keys'
Revision: '0'
ABI: 'arm'
pid: 17946, tid: 17949, name: crasher  >>> crasher <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xc
    r0 0000000c  r1 00000000  r2 00000000  r3 00000000
    r4 00000000  r5 0000000c  r6 eccdd920  r7 00000078
    r8 0000461a  r9 ffc78c19  sl ab209441  fp fffff924
    ip ed01b834  sp eccdd800  lr ecfa9a1f  pc ecfd693e  cpsr 600e0030

backtrace:
    #00 pc 0004793e  /system/lib/libc.so (pthread_mutex_lock+1)
    #01 pc 0001aa1b  /system/lib/libc.so (readdir+10)
    #02 pc 00001b91  /system/xbin/crasher (readdir_null+20)
    #03 pc 0000184b  /system/xbin/crasher (do_action+978)
    #04 pc 00001459  /system/xbin/crasher (thread_callback+24)
    #05 pc 00047317  /system/lib/libc.so (_ZL15__pthread_startPv+22)
    #06 pc 0001a7e5  /system/lib/libc.so (__start_thread+34)
Tombstone written to: /data/tombstones/tombstone_06

运行 stack < /data/tombstones/tombstone_06 将显示以下内容

Revision: '0'
pid: 17946, tid: 17949, name: crasher  >>> crasher <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xc
     r0 0000000c  r1 00000000  r2 00000000  r3 00000000
     r4 00000000  r5 0000000c  r6 eccdd920  r7 00000078
     r8 0000461a  r9 ffc78c19  sl ab209441  fp fffff924
     ip ed01b834  sp eccdd800  lr ecfa9a1f  pc ecfd693e  cpsr 600e0030
Using arm toolchain from: ~/android/lineage/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9/bin/

Stack Trace:
  RELADDR   FUNCTION                   FILE:LINE
  0004793e  pthread_mutex_lock+2       bionic/libc/bionic/pthread_mutex.cpp:515
  v------>  ScopedPthreadMutexLocker   bionic/libc/private/ScopedPthreadMutexLocker.h:27
  0001aa1b  readdir+10                 bionic/libc/bionic/dirent.cpp:120
  00001b91  readdir_null+20            system/core/debuggerd/crasher.cpp:131
  0000184b  do_action+978              system/core/debuggerd/crasher.cpp:228
  00001459  thread_callback+24         system/core/debuggerd/crasher.cpp:90
  00047317  __pthread_start(void*)+22  bionic/libc/bionic/pthread_create.cpp:202 (discriminator 1)
  0001a7e5  __start_thread+34          bionic/libc/bionic/clone.cpp:46 (discriminator 1)

stack 的工作方式与内核空间调试工具 decode_stacktrace.sh 非常相似。 它们都提供堆栈跟踪引用的原始代码的确切文件和行。 继续阅读以了解更多关于如何使用 decode_stacktrace.sh 的信息。

ramoops-pmsg

ramoops-pmsg 是 ramoops 的用户空间可访问版本。 要访问上次重启之前来自 pstore 的这些日志,您可以运行

$ adb logcat -b all -L

有关 ramoops 内核功能的更详细解释,请参见下文。

内核空间调试

内核空间调试帮助我们识别内核内的问题。 设备制造商除了提供设备内核驱动程序外,还可能在发布设备内核源代码时自定义内核的其他部分。 因此,当设备维护者将他们的设备内核变基到较新的内核版本(以跟上安全补丁)时,可能会发生回归。

dmesg

dmesg 是一个 CLI 工具,用于显示内核缓冲区消息。 它提供了内核级活动的详细视图,允许设备维护者诊断系统崩溃、驱动程序问题并监控系统事件。 请注意,所有 LineageOS 构建默认都带有 SELinux 强制执行,这要求您在使用 dmesg 之前处于 adb root 模式。

截断的 dmesg 输出示例,显示 NULL 指针解引用

# adb shell dmesg
| Unable to handle kernel NULL pointer dereference at virtual address 0000000000000010
| Internal error: Oops: 96000006 [#1] SMP
| Call trace:
| update_insn_emulation_mode+0xc0/0x148
| emulation_proc_handler+0x64/0xb8
| proc_sys_call_handler+0x9c/0xf8
| proc_sys_write+0x18/0x20
| __vfs_write+0x20/0x48
| vfs_write+0xe4/0x1d0
| ksys_write+0x70/0xf8
| __arm64_sys_write+0x20/0x28
| el0_svc_common.constprop.0+0x7c/0x1c0
| el0_svc_handler+0x2c/0xa0
| el0_svc+0x8/0x200

ramoops

ramoops 是一个 Linux 内核功能,可在系统崩溃前写入内存。 ramoops 可以在设备的内核设备树源 (DTS) 中设置,方法是为 ramoops-pmsg 保留内存缓冲区。 它与内核 pstore 驱动程序协同工作,以在重启前将 ramoops 保存到 /sys/fs/pstore 的持久性文件中。

带有已分配 pmsg 缓冲区的 ramoops 配置示例

/{
    reserved-memory {
        ramoops: ramoops@b0000000 {
            compatible = "ramoops";
            reg = <0 0xb0000000 0 0x00400000>;
            record-size = <0x40000>; /*256x1024*/
            console-size = <0x40000>;
            ftrace-size = <0x40000>;
            pmsg-size = <0x200000>;
            ecc-size = <0x0>;
        };
    };
};

在较新的内核上,可以通过以下配置选项启用 ramoops

CONFIG_PSTORE=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_RAM=y

pstore 通常默认压缩,使其在调试期间更难使用。 您可能希望通过以下方式禁用它

# CONFIG_PSTORE_COMPRESS is not set

虽然 ramoops 和 pstore 是强大的工具,但使用它们有一些注意事项。 由于 pstore 默认情况下将数据作为缓冲写入,并且我们通常仅在系统即将崩溃时使用它,因此我们在之后检索 pstore 时往往会看到很多损坏。

addr2line

dmesg 和 ramoops 通常会在堆栈跟踪中生成神秘的内存地址,例如 ffffff9405cebf10,来自

CFI failure (target: [\<\ffffff9405cebf10\>] __typeid__ZTSFvP10net_deviceE_global_addr+0x170/0x17c):

在这种情况下,我们可以使用地址到行 (addr2line) 来查找问题发生的特定文件和行,使用

$ addr2line -e /path/to/kernel-module.o ffffff9405cebf10

decode_stacktrace.sh

decode_stacktrace.sh 是每个 Linux 内核源代码捆绑的脚本,位于 linux/blob/master/scripts/decode_stacktrace.sh,它使用了 addr2line。 要使用它,您首先必须在内核配置中启用 CONFIG_DEBUG_INFO=y 并构建内核。

接下来,您必须从您尝试调试的内核 panic 的 dmesg 中提取调用跟踪,并将其保存在文本文件中,例如这里的 dmesg.txt

| update_insn_emulation_mode+0xc0/0x148
| emulation_proc_handler+0x64/0xb8
| proc_sys_call_handler+0x9c/0xf8
| proc_sys_write+0x18/0x20
| __vfs_write+0x20/0x48
| vfs_write+0xe4/0x1d0
| ksys_write+0x70/0xf8
| __arm64_sys_write+0x20/0x28
| el0_svc_common.constprop.0+0x7c/0x1c0
| el0_svc_handler+0x2c/0xa0
| el0_svc+0x8/0x200

最后,将 decode_stacktrace.sh 指向您创建的 dmesg.txt 文件以及您编译的内核

$ ./scripts/decode_stacktrace.sh /path/to/vmlinux /path/to/kernel-source-dir < dmesg.txt

正如您在以下示例(不同的堆栈跟踪)中看到的那样,堆栈跟踪中每个调用的内存地址都已替换为特定的代码文件和行,您可以参考您的内核源代码。

| dump_stack (lib/dump_stack.c:52)
| warn_slowpath_common (kernel/panic.c:418)
| warn_slowpath_null (kernel/panic.c:453)
| _oalloc_pages_slowpath+0x6a/0x7d0
| ? zone_watermark_ok (mm/page_alloc.c:1728)
| ? get_page_from_freelist (mm/page_alloc.c:1939)
| __alloc_pages_nodemask (mm/page_alloc.c:2766)

串行 / gdb

具有 UART 端口的设备(参见带有 3.5 毫米耳机端口的旧 Nexus/Google Pixel,以及带有 USB-C 调试器的新 Google Pixel)可以使用 UART 电缆连接以查看内核控制台消息 (kgdb)。 通过串行,您可以调试甚至在内核启动之前发生的问题。

例如,当您的设备卡在启动徽标上时,您可以考虑使用串行。

绝望调试

如果所有其他方法都失败了,您可以在您希望调试的内核部分中使用 panic()SebaUbuntu 在此处的补丁演示了如何使用 panic() 来捕获早期初始化问题。

芯片组供应商/OEM 特定调试

以下是我们多年来发现的一些由 OEM 开发的自定义调试工具,这些工具已被证明很有帮助。

EDL 内存转储 (qcom)

Qualcomm CrashDump

某些高通设备启用了 CrashDump,这允许您使用高通的 firehose 工具来获取内存转储。 由于 firehose 工具是闭源的,我们建议使用 Bjoern Kerler 重写的开源版本工具,可以在 bkerler/edl 找到。 您可以使用 edl memorydump 检索内存转储。

/dev/block/by-name/debug (三星)

/dev/block/by-name/debug 是三星设备上的一个特殊设备块文件,其中包含 XBL 日志、内核日志等的流。 您可以运行 adb pull /dev/block/by-name/debug debug.bin 来转储日志流。

截断的 debug.bin 文件示例

{340532} ** XBL(1) **
{340532}
Format: Log Type - Time(microsec) - Message - Optional Info
Log Type: B - Since Boot(Power On Reset),  D - Delta,  S - Statistic
S - QC_IMAGE_VERSION_STRING=BOOT.XF.2.1-00133-SDM710LZB-3
S - IMAGE_VARIANT_STRING=SDM670LA
S - OEM_IMAGE_VERSION_STRING=21DJFC21
S - Boot Interface: eMMC
S - Secure Boot: On
S - Boot Config @ 0x00786070 = 0x000000c9
S - JTAG ID @ 0x00786130 = 0x100910e1
S - OEM ID @ 0x00786138 = 0x00200000
S - Feature Config Row 0 @ 0x007841a0 = 0x08d020000b588420
S - Feature Config Row 1 @ 0x007841a8 = 0xe0140000000311a0
S - Core 0 Frequency, 1516 MHz
S - PBL Patch Ver: 0
S - PBL freq: 600 MHZ
S - I-cache: On
S - D-cache: On

我们之前关于 高通信任链的工程博客更详细地介绍了什么是可扩展引导加载程序 (XBL)。

常见错误(以及如何解决它们)

现在我们对 Android 开发期间使用的一些调试工具有了基本的了解,现在让我们学习如何识别和修复调试期间遇到的常见错误。

dlopen 失败

许多设备都预装了使用旧版本库编译的库,这些旧版本库已删除某些符号。 可能发生的错误如下所示

* java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "_ZN7android21SurfaceComposerClient11Transaction5applyEb" referenced by "/product/lib64/libsecureuisvc_jni.so"...

要解决此错误,我们需要内插库,我们通常将其称为“shims”。 通过拦截对缺失函数的调用并提供替代实现,我们可以基本模拟使用您可能正在使用的预构建库构建的原始库的行为。

或者,一些现代设备选择将更新库的旧 VNDK 版本复制到 $libname-v$vndkVersion.so,然后 patchelf 有问题的库以加载此版本库。

您可以查看我们已标准化的现有 shims,此处,这些 shims 以前由 LineageOS 20 之前的各个设备维护者管理。 对于上面的示例,您可以参考 补丁,了解如何为需要它们的预构建库使用 shim 包。

隐藏的 dlopen 失败

由于 dlopen 错误仅在运行时发生,因此某些故障不会立即显示,甚至会从日志记录中隐藏。 因此,我们提出了一个库钩子 dlopen.so,您可以将其放在 LD_PRELOAD 中,以显示所有链接器操作,帮助我们查看当前哪些库缺少任何符号,甚至缺少依赖项。

这是使用 该库的设备的日志

instantnoodlep / # LD_PRELOAD=dlopen.so /vendor/bin/hw/android.hardware.gnss\@2.1-service-qti
dlopen(libnetd_client.so) -> 0x0, errno: dlopen failed: library "libnetd_client.so" not found
dlopen(libgnss.so) -> 0xdc9f08e905187e63, errno: (null)
dlopen(liblbs_core.so) -> 0x618992fa4f6a1e6d, errno: (null)
dlopen(liblocdiagiface.so) -> 0x0, errno: dlopen failed: library "liblocdiagiface.so" not found
dlopen(libloc_net_iface.so) -> 0x0, errno: dlopen failed: library "libloc_net_iface.so" not found
dlopen([email protected]) -> 0xe8b09305c7a1c55f, errno: (null)
dlopen(libdataitems.so) -> 0xc4ba0f7c15946aef, errno: (null)
dlopen([email protected]) -> 0xd950565f49bbcc01, errno: (null)
dlopen(libgnss.so) -> 0xdc9f08e905187e63, errno: (null)
dlopen(libxtadapter.so) -> 0x39eb3dbc835592b5, errno: (null)
dlopen(libcdfw.so) -> 0x59b12f3c0b5e3e1b, errno: (null)
dlopen(libloc_socket.so) -> 0x2229c8abec54dac9, errno: (null)

奖励

在开发非私有系统应用(如 Aperture)时,您可以使用 Android Studio 来更轻松地进行测试和调试!