3、Oops信息分析:Oops信息结构解析、寄存器与栈回溯分析、通过PC指针定位代码行
内核崩溃了,屏幕上刷出一堆乱糟糟的十六进制数字——这就是我们常说的Oops。说实话,我刚入行那会儿,看到这玩意儿就头大。但后来我发现,Oops其实是个宝藏,它把问题出在哪、怎么出的,都写得明明白白。
今天我就带你把这堆"天书"拆开看看。你想想看,内核都死给你看了,总得死个明白吧?
3.1 Oops信息结构解析
一个典型的Oops信息,大概长这样:
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 5 [#1] PREEMPT SMP ARM
Modules linked in: my_driver(O)
CPU: 0 PID: 123 Comm: insmod Tainted: G O 4.19.0
Hardware name: MyBoard (DT)
PC is at my_function+0x14/0x30 [my_driver]
LR is at another_func+0x8/0x20
pc : [<bf000014>] lr : [<bf000008>] psr: 60000013
sp : c7827d80 ip : c7827d98 fp : c7827d94
r10: 00000000 r9 : c7827e00 r8 : bf000000
r7 : 00000001 r6 : c7827e00 r5 : 00000000 r4 : c7827e00
r3 : 00000000 r2 : 00000001 r1 : 00000000 r0 : 00000000
Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c5387d Table: 4a6a806a DAC: 00000055
Process insmod (pid: 123, stack limit = 0xc7826000)
Stack: (0xc7827d80 to 0xc7828000)
7d80: c7827e00 bf000000 00000001 c7827e00 c7827e00 bf000014
7da0: c7827dc0 c0108a4c c7827dc0 bf000000 c7827e00 00000001
...
嗯,看着确实吓人。但别慌,我们一层层剥开它。
第一行是错误类型:Unable to handle kernel NULL pointer dereference。说白了就是访问了空指针。我在项目中遇到过,十次Oops有七八次都是这个原因。
接着是CPU和进程信息:CPU: 0 PID: 123 Comm: insmod。这告诉我们,是insmod这个进程在CPU 0上触发的。如果你有多个CPU,这个信息能帮你判断是不是并发问题。
PC和LR寄存器:这是最关键的。PC指向了my_function+0x14/0x30,意思是程序执行到my_function函数偏移0x14字节处挂掉了,这个函数总长0x30字节。LR是返回地址,告诉你谁调了它。
核心要点:PC指针 + 偏移量 = 问题代码的精确位置。这是定位bug的命门。
3.2 寄存器与栈回溯分析
寄存器值能告诉我们什么?我举个例子:
r0 : 00000000 r1 : 00000001 r2 : 00000001
r3 : 00000000 r4 : c7827e00 r5 : 00000000
r0是0,r3也是0。结合第一行的"NULL pointer dereference",基本可以断定:代码试图通过一个空指针访问内存。为什么会这样?
我个人习惯先看r0和r3,因为ARM的ATPCS调用约定里,r0通常是第一个参数或返回值。如果它是0,十有八九是传了个NULL进来。
再看栈回溯部分:
Stack: (0xc7827d80 to 0xc7828000)
7d80: c7827e00 bf000000 00000001 c7827e00 c7827e00 bf000014
7da0: c7827dc0 c0108a4c c7827dc0 bf000000 c7827e00 00000001
栈里存的是函数调用链。每个bf000xxx这样的地址,都指向一个函数入口。配合vmlinux或模块的符号表,就能还原出调用路径。
小技巧:用addr2line工具把地址翻译成文件名和行号。比如:addr2line -e vmlinux bf000014。我曾经靠这招,五分钟定位了一个藏了三天的bug。
3.3 通过PC指针定位代码行
好了,重头戏来了。怎么用PC指针找到具体的代码行?
假设PC是bf000014,函数是my_function+0x14。我们需要知道my_function在模块里的偏移量。
步骤一:获取模块基址
从Oops信息里找Modules linked in: my_driver(O),然后看寄存器r8 : bf000000——这就是模块的加载基址。
步骤二:计算函数内偏移
PC - 基址 = 0xbf000014 - 0xbf000000 = 0x14。嗯,就是函数内偏移0x14字节。
步骤三:反汇编找对应行
用objdump -d my_driver.ko反汇编模块,找到my_function:
00000000 <my_function>:
0: e52db004 push {fp}
4: e28db000 add fp, sp, #0
8: e24dd00c sub sp, sp, #12
c: e50b0008 str r0, [fp, #-8]
10: e51b3008 ldr r3, [fp, #-8]
14: e5932000 ldr r2, [r3] <-- 这里崩了!
18: e1a00002 mov r0, r2
1c: e24bd000 sub sp, fp, #0
20: e49db004 pop {fp}
24: e12fff1e bx lr
看到没?偏移0x14处是ldr r2, [r3],它试图从r3指向的地址读数据。而r3是0——空指针!
步骤四:关联源码行
用addr2line -e my_driver.ko -f 0x14,或者查编译时生成的.dwarf调试信息,就能直接得到:
my_function
/path/to/my_driver.c:42
第42行!打开源码一看:
40: int my_function(struct my_data *data)
41: {
42: int val = data->value; // data是NULL!
43: ...
44: }
真相大白。调用者传了个NULL指针进来,而我们没做检查。
避坑指南:我曾经在调试一个USB驱动时,PC指针指向的地址在模块范围之外。后来发现是模块加载时地址重定位出了问题。这种情况下,别死磕PC,先检查模块是否正常加载。
3.4 实战流程图
下面这张图,是我自己总结的Oops分析流程。每次遇到内核崩溃,我就按这个步骤走,基本没失手过:
这张图把整个分析流程串起来了。你按这个顺序走,从捕获Oops到定位源码行,每一步都有章可循。
3.5 常用工具速查表
| 工具 | 用途 | 示例命令 |
|---|---|---|
| addr2line | 地址转源码行号 | addr2line -e vmlinux -f bf000014 |
| objdump | 反汇编查看指令 | objdump -d my_driver.ko |
| gdb | 离线分析vmcore | gdb vmlinux vmcore |
| decode_stacktrace | 美化栈回溯 | ./scripts/decode_stacktrace.sh |
个人经验:我习惯在编译内核时开启CONFIG_DEBUG_INFO,这样addr2line能给出更精确的行号。另外,CONFIG_KALLSYMS一定要开,否则你看到的PC地址全是裸的十六进制,根本不知道对应哪个函数。
好了,Oops分析的核心思路就这些。说白了就是三步:看错误类型、抓PC指针、反汇编找源码。你多练几次,就会发现这玩意儿比想象中简单得多。
公众号:蓝海资料掘金营,微信deep3321