第3章:日志结构解析——Oops消息、Backtrace、寄存器与栈信息
内核崩溃了,第一件事是什么?
别急着重启。先看日志。
日志就是事故现场。你想想看,法医到了现场,总得先拍照取证吧?内核崩溃日志就是我们的「现场照片」。我见过太多工程师,一看到Oops就慌,直接重启机器,结果啥线索都没留下。嗯,这是大忌。
3.1 Oops消息——内核的「遗言」
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: hello_driver(O)
CPU: 0 PID: 123 Comm: insmod Tainted: G O 4.19.0
Hardware name: MyBoard (DT)
PC is at hello_write+0x14/0x30 [hello_driver]
LR is at vfs_write+0xb4/0x1a8
pc : [] lr : [] psr: 60000013
sp : c0a03e80 ip : c0a03e90 fp : c0a03e8c
r10: 00000000 r9 : c0a03f00 r8 : 00000000
r7 : 00000000 r6 : bf000000 r5 : c0a03f00 r4 : c0a03f00
r3 : 00000000 r2 : 00000000 r1 : c0a03f00 r0 : 00000000
Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c5387d Table: 8a8a806a DAC: 55555555
看着挺吓人对吧?别怕,我来拆解一下。
第一行是死因:Unable to handle kernel NULL pointer dereference at virtual address 00000000。翻译成人话就是:内核试图访问地址0x0,但这是个空指针,非法操作。我在项目中遇到过,90%的内核崩溃都是空指针解引用,剩下的10%是内存越界。
第二行是进程信息:PID: 123 Comm: insmod。谁干的?insmod这个进程。PID是123。这能帮你定位到是哪个模块加载时出了问题。
第三行是PC指针:PC is at hello_write+0x14/0x30 [hello_driver]。这是关键中的关键。PC(Program Counter)指向了hello_driver模块中hello_write函数的偏移0x14处。整个函数大小是0x30。也就是说,程序执行到hello_write函数中间位置时崩了。
核心要点:PC指针告诉你「死在哪儿」,死因告诉你「怎么死的」。这两个信息必须同时看。
3.2 Backtrace——回溯调用链
Oops消息后面通常会跟着Backtrace。它就像事故现场的脚印,一步步回溯,告诉你程序是怎么走到这一步的。
Backtrace:
[<bf000000>] (hello_write) from [<c008a2b4>] (vfs_write+0xb4/0x1a8)
[<c008a200>] (vfs_write) from [<c008a8c0>] (sys_write+0x48/0x78)
[<c008a878>] (sys_write) from [<c000f000>] (ret_fast_syscall+0x0/0x28)
读Backtrace有个技巧:从下往上读。最下面是入口,最上面是崩溃点。
这个例子告诉我们:
- 用户程序调用了
sys_write(系统调用) - 系统调用进入
vfs_write(虚拟文件系统层) - VFS层调用了
hello_write(驱动层) - 在
hello_write里崩了
你看,调用链清清楚楚。我建议你每次分析崩溃时,先把Backtrace画成流程图,这样思路会清晰很多。
个人经验:我曾经遇到一个诡异的崩溃,Backtrace显示在某个驱动函数里,但查了半天代码都没问题。后来发现是栈被踩坏了,Backtrace本身是假的。嗯,这种情况虽然少见,但遇到了就特别坑。所以,Backtrace要结合寄存器状态一起看。
3.3 寄存器状态——CPU的「快照」
寄存器是CPU的临时记忆。崩溃那一刻,每个寄存器里存了什么值,都能告诉我们很多信息。
再看刚才的例子:
r3 : 00000000 r2 : 00000000 r1 : c0a03f00 r0 : 00000000
r0是0,r3也是0。而崩溃原因是空指针解引用。你想想看,如果函数里用r0作为指针去访问内存,那肯定崩啊。
寄存器分析有个套路:
- r0-r3:函数参数。看它们是不是合法地址
- PC:当前执行位置。必须看
- LR:返回地址。谁调了我?
- SP:栈指针。栈有没有溢出?
- FP:帧指针。栈回溯的起点
我一般先看PC和LR,再看r0-r3。如果r0是0,那基本就是空指针了。如果r0是个很大的数,那可能是野指针。
注意:寄存器值要结合反汇编来看。比如PC指向的地址,你得反汇编出对应的指令,才知道它在干什么。光看寄存器值,有时候会误判。
3.4 栈信息——最后的「现场」
栈信息通常跟在寄存器后面,是一段原始的内存数据:
Stack: (0xc0a03e80 to 0xc0a04000)
3e80: bf000000 c0a03f00 c0a03f00 00000000 c0a03f00 bf000014 c008a2b4 60000013
3ea0: c0a03e80 c0a03e80 00000000 00000000 00000000 00000000 00000000 00000000
3ec0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
左边是栈地址,右边是数据。怎么读?
首先看SP(栈指针)的值。这里SP是c0a03e80,所以栈数据从0xc0a03e80开始。
然后看栈里有没有函数地址。比如bf000014,这个地址和PC的值bf000014一模一样。这说明栈里保存了返回地址。
栈分析有个实用技巧:搜索函数地址。如果你知道某个函数的地址范围,可以在栈数据里搜一下。如果找到了,说明这个函数可能还在调用链上,但Backtrace没显示出来。这种情况我遇到过好几次,都是因为栈被部分破坏了。
3.5 实战:从日志到定位
好了,理论说完了。咱们来实战一把。
假设你拿到这样一份Oops日志:
Unable to handle kernel paging request at virtual address 0a0a0a0a
PC is at my_function+0x20/0x40
LR is at caller_function+0x14/0x2c
r0: 0a0a0a0a r1: c1234567 r2: 00000001
第一步,看死因。地址0a0a0a0a,这个值很特别。0a0a0a0a在十六进制里就是10.10.10.10,但更关键的是,它是个「野指针」的典型值。很多内核开发者会用0a0a0a0a来初始化释放后的内存,所以看到这个地址,基本可以断定是「释放后使用」(Use-After-Free)。
第二步,看PC。PC在my_function+0x20处。反汇编这个函数,看看偏移0x20是什么指令。
第三步,看r0。r0等于0a0a0a0a,和崩溃地址一致。说明r0就是那个野指针。那么问题来了:r0是怎么来的?是函数参数?还是从某个结构体里取出来的?
我个人的习惯是,先查r0的来源。如果是函数参数,那就看调用者传了什么。如果是结构体成员,那就看这个结构体是不是已经被释放了。
总结一下我的分析流程:
- 读死因(第一行)
- 读PC和LR(死在哪儿,谁调的我)
- 读Backtrace(调用链)
- 读寄存器(参数和状态)
- 读栈数据(有没有隐藏线索)
- 反汇编PC附近的代码(确认具体指令)
这套流程我用了十年,基本没失手过。你刚开始可能会觉得慢,但熟练了以后,看一份Oops日志就像看一份体检报告,哪儿有问题一目了然。
嗯,今天就到这儿。下一章咱们聊聊怎么用这些信息去修复内核崩溃。到时候我会拿几个真实案例出来,手把手带你走一遍。