第四章 常用调试工具: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++符号反修饰(内核里其实用得少)
还有一个常见场景:你拿到的是一个相对偏移地址,比如某个ko模块里的偏移。这时候需要先算出模块的加载基址:
# 先看模块加载地址
cat /proc/modules | grep my_driver
# 假设基址是 0xffffff8000000000,崩溃地址是 0xffffff8000001234
# 偏移 = 0x1234
addr2line -e my_driver.ko -f -C 0x1234
4.2 objdump:反汇编的瑞士军刀
addr2line能告诉你代码在哪一行,但有时候你还需要知道具体是哪条指令崩了。这时候就得请出objdump。
我最常用的几个场景:
- 反汇编某个函数:
objdump -d vmlinux --start-address=0x... --stop-address=0x... - 查看某个地址附近的指令:结合addr2line找到函数入口,然后反汇编出前后几十条指令
- 检查全局变量:
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
CONFIG_KGDB和CONFIG_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,说明调用者传了个空指针进来。
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还有一个杀手锏——实时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这个大杀器。
嗯,工具就介绍到这里。下一章咱们聊聊怎么把这些工具组合起来,搭建一个完整的崩溃分析工作流。