第四章 常用调试工具:addr2line、objdump、gdb、crash工具、Trace32的使用

做内核崩溃分析,说白了就是跟一堆二进制数据打交道。你面对的可能只是一个PC指针、一堆寄存器值,或者一个完整的RAM dump。怎么从这些冰冷的数据里还原出事故现场?靠的就是这几把趁手的兵器。

我个人习惯把这几个工具分成两类:一类是离线分析工具,比如addr2line、objdump、crash;另一类是动态调试工具,比如gdb和Trace32。今天咱们一个一个过,每个我都会讲讲我在项目中踩过的坑。

4.1 addr2line:把地址翻译成代码行

这工具看着简单,但用不好真能把你坑哭。它的作用就是把一个虚拟地址翻译成对应的源文件名和行号。

基本用法:

addr2line -e vmlinux -f -C 0xffffff8001234567

参数说明:

  • -e vmlinux:指定带符号表的ELF文件
  • -f:显示函数名
  • -C:C++符号反修饰(内核里其实用得少)
注意:这里有个大坑——vmlinux必须跟你设备上跑的内核版本完全一致。我曾经有一次拿着A版本的内核去解析B版本的崩溃日志,结果addr2line给出的行号差了十万八千里。排查了半天,最后发现是内核版本没对齐。

还有一个常见场景:你拿到的是一个相对偏移地址,比如某个ko模块里的偏移。这时候需要先算出模块的加载基址:

# 先看模块加载地址
cat /proc/modules | grep my_driver
# 假设基址是 0xffffff8000000000,崩溃地址是 0xffffff8000001234
# 偏移 = 0x1234
addr2line -e my_driver.ko -f -C 0x1234
小技巧:我习惯写个脚本,把dmesg里的崩溃栈批量丢给addr2line解析。一行命令搞定几十个地址,比手动敲快多了。

4.2 objdump:反汇编的瑞士军刀

addr2line能告诉你代码在哪一行,但有时候你还需要知道具体是哪条指令崩了。这时候就得请出objdump。

我最常用的几个场景:

  1. 反汇编某个函数objdump -d vmlinux --start-address=0x... --stop-address=0x...
  2. 查看某个地址附近的指令:结合addr2line找到函数入口,然后反汇编出前后几十条指令
  3. 检查全局变量objdump -t vmlinux | grep my_variable

举个例子,假设崩溃在do_exit+0x100

# 先找到do_exit的地址
grep "do_exit" /proc/kallsyms
# 假设是 ffff0000080a0000
# 反汇编从 ffff0000080a0100 开始的32条指令
objdump -d vmlinux --start-address=0xffff0000080a0100 --stop-address=0xffff0000080a0200

嗯,这里要注意一点:objdump默认反汇编的是整个文件,如果你直接对vmlinux用-d,输出能把你屏幕刷爆。一定要加地址范围限制。

我的经验:有一次遇到一个空指针解引用,addr2line指向了某个函数的第50行。但objdump一看,发现第50行对应的指令是ldr x0, [x1, #8],而x1寄存器是0。这说明问题不在第50行本身,而是调用方传了个空指针进来。如果没有objdump,你可能会在错误的方向上浪费大量时间。

4.3 gdb:动态调试的利器

gdb在用户态调试里用得很多,但在内核调试里,它通常配合kgdb或者QEMU使用。我个人觉得,gdb最大的价值在于单步跟踪查看复杂数据结构

内核调试的基本流程:

# 启动gdb并加载内核符号
gdb vmlinux

# 连接kgdb(假设串口是ttyS0)
(gdb) target remote /dev/ttyS0

# 设置断点
(gdb) b do_exit

# 继续执行
(gdb) c

# 查看寄存器
(gdb) info registers

# 查看内存
(gdb) x/10gx 0xffffff8000000000

# 查看结构体
(gdb) p *(struct task_struct *)0xffffff8001234567
注意:gdb连接kgdb时,目标内核必须开启了CONFIG_KGDBCONFIG_KGDB_SERIAL_CONSOLE。而且串口波特率建议用115200,别问我怎么知道的——我曾经用9600波特率等一个断点等了5分钟。

还有一个我常用的技巧:条件断点。比如我只想在看某个特定pid的进程调用schedule时停下来:

(gdb) b schedule if current->pid == 1234

这比手动敲continue到手指抽筋强多了。

4.4 crash工具:RAM dump分析神器

crash工具是我个人最推荐的内核崩溃分析工具,没有之一。它专门用来分析完整的RAM dump或者kdump产生的vmcore文件。

基本用法:

crash vmlinux vmcore

进去之后,常用的命令:

命令 作用 我的使用场景
bt 打印当前CPU的调用栈 看崩溃线程的完整回溯
bt -a 打印所有CPU的调用栈 排查多核并发问题
ps 列出所有进程 看崩溃时哪些进程在跑
vm 查看虚拟内存信息 检查某个地址的页表映射
rd 读取内存内容 查看某个地址附近的数据
log 查看dmesg日志 找崩溃前的内核日志
files 查看进程打开的文件 排查文件系统相关问题

举个例子,假设你看到一个NULL指针解引用的崩溃:

crash> bt
PID: 1234  TASK: ffff88003e4c0000  CPU: 2   COMMAND: "my_app"
 #0 [ffff88003e4c3d50] machine_kexec at ffffffff8105b1e2
 #1 [ffff88003e4c3db0] crash_kexec at ffffffff8105c8d2
 #2 [ffff88003e4c3e80] oops_end at ffffffff8103a0e2
 #3 [ffff88003e4c3ea0] no_context at ffffffff8103a1c2
 #4 [ffff88003e4c3ef0] __bad_area_nosemaphore at ffffffff8103a2b2
 #5 [ffff88003e4c3f30] bad_area_nosemaphore at ffffffff8103a3a2
 #6 [ffff88003e4c3f40] __do_page_fault at ffffffff8103a4b2
 #7 [ffff88003e4c3f70] do_page_fault at ffffffff8103a5c2
 #8 [ffff88003e4c3f90] page_fault at ffffffff8152b1e2
    [exception RIP: my_function+0x50]
    RIP: ffffffffa0000050  RSP: ffff88003e4c3e48  RFLAGS: 00010246
    RAX: 0000000000000000  RBX: ffff88003e4c0000  RCX: 0000000000000001
    RDX: 0000000000000000  RSI: 0000000000000000  RDI: 0000000000000000
    RBP: ffff88003e4c3e60   R8: 0000000000000000   R9: 0000000000000000
    R10: 0000000000000000  R11: 0000000000000000  R12: ffff88003e4c0000
    R13: 0000000000000000  R14: 0000000000000000  R15: 0000000000000000
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018

看到RAX是0,RIP在my_function+0x50。这时候用crash的dis命令反汇编一下:

crash> dis my_function+0x50
0xffffffa0000050:  mov    rax, [rdi+0x10]   ; rdi是0,所以崩了

一目了然——rdi寄存器是0,说明调用者传了个空指针进来。

小技巧:crash里可以用struct命令直接查看内核结构体。比如struct task_struct ffff88003e4c0000,比gdb的p命令更直观,因为它会按字段对齐输出。

4.5 Trace32:硬件调试的终极武器

Trace32(也叫Lauterbach)是硬件调试器里的劳斯莱斯。它贵,但确实好用。我一般只在两种情况下用它:一是内核还没起来就崩了,二是需要看硬件级别的时序

Trace32的核心能力:

  • JTAG/SWD调试:直接控制CPU,不依赖操作系统
  • 实时trace:记录CPU执行的每一条指令,带时间戳
  • 内存读写:在CPU停止时直接读写物理内存
  • 外设寄存器查看:直接访问硬件寄存器

一个典型的使用场景——内核在start_kernel之前就挂了:

// Trace32脚本示例
// 1. 连接目标板
SYStem.CONFIG.CPU CORTEXA72
SYStem.CONFIG.COREMULTI 4
SYStem.UP

// 2. 加载符号表
Data.LOAD.ELF vmlinux /NOCODE

// 3. 在start_kernel设断点
Break.Set start_kernel /Program

// 4. 单步执行
Step

// 5. 查看寄存器
Register.view

// 6. 查看内存
Memory.view 0x80000000--0x80001000
注意:Trace32的脚本语法跟主流编程语言差别很大,刚上手会很不习惯。我建议你从官方示例改起,别自己从头写。另外,Trace32的license是按功能模块卖的,买之前想清楚你到底需要哪些功能——别像我一样,买了个全功能版结果90%的功能从来没碰过。

Trace32还有一个杀手锏——实时trace。它能记录CPU执行的每一条指令,包括分支跳转、异常触发等。这对于分析那些概率性崩溃特别有用。你想想看,一个bug可能跑几万次才出现一次,用printk打日志会影响时序,用gdb断点会改变执行流。但Trace32的trace是完全非侵入式的,它只是默默地在旁边记录,不影响CPU的正常运行。

我记得有一次,一个客户反馈他们的设备在高温下偶尔会重启。我们用Trace32的trace功能连续跑了48小时,终于抓到了那个概率极低的崩溃点——是一个DMA操作完成后没有正确更新内存屏障,导致CPU读到了脏数据。如果没有Trace32,这种问题可能永远都复现不了。

4.6 工具选型建议

说了这么多,到底该用哪个?我的建议是:

场景 推荐工具 理由
只有崩溃日志,没有dump addr2line + objdump 轻量,快速定位代码行
有vmcore/dump文件 crash工具 功能全面,适合深度分析
需要单步调试内核 gdb + kgdb 灵活,适合复现问题
内核早期启动崩溃 Trace32 不依赖OS,硬件级调试
概率性、时序相关崩溃 Trace32 trace 非侵入式,记录完整执行流

说白了,没有哪个工具是万能的。我个人的工作流是:先用addr2line快速定位崩溃点,如果信息不够就上crash工具做深度分析。遇到那些特别诡异的、跟时序相关的bug,才会请出Trace32这个大杀器。

嗯,工具就介绍到这里。下一章咱们聊聊怎么把这些工具组合起来,搭建一个完整的崩溃分析工作流。