内核 Oops 日志经常很长,里面有寄存器、调用栈、模块列表、CPU 状态和一段反汇编。第一次看时如果直接跳到调用栈最后一个函数,很容易把“崩溃现场”误当成“根因”。
第一遍阅读的目标不是马上定位补丁,而是先回答几个基础问题:系统为什么停下来,在哪个上下文停下来,访问了什么地址,以及这个访问为什么不应该发生。
先看异常类型
日志开头通常会说明异常类型,例如空指针访问、非法地址访问、BUG、WARNING 或 page fault。
需要区分:
- 空指针还是野指针
- 读访问还是写访问
- 用户地址还是内核地址
- 同步异常还是异步错误
- panic 是直接触发,还是 warning 后升级
如果地址接近 0x0,通常要怀疑空指针或结构体成员偏移。如果地址看起来像已经释放的对象,就要往生命周期和并发方向看。
IP 告诉你在哪里炸,不告诉你为什么炸
RIP 或其它架构上的异常 PC 指向真正触发异常的指令。它很重要,但它通常只回答“在哪里炸”。
根因可能在更早的位置:
- 指针初始化失败
- 错误路径没有清理状态
- 引用计数提前归零
- 回调在对象释放后仍然执行
- probe 失败后还有异步 work 在跑
所以看到某个函数名后,不要急着认为这个函数有 bug。要继续看它拿到的输入是否已经坏了。
调用栈要结合上下文
同一个函数在不同上下文里意义不同。比如它是在进程上下文、软中断、硬中断、workqueue 还是 timer 里执行。
上下文会限制可能的原因:
- 中断上下文不能睡眠
- workqueue 常常暴露异步生命周期问题
- timer 回调常见 use-after-free
- probe/remove 相关栈要重点看资源释放顺序
- 文件系统路径要看锁和引用关系
如果栈里出现 kworker,要继续确认 work 的来源。单看 process_one_work 没有意义。
寄存器能还原参数
很多架构会在 Oops 中打印通用寄存器。对于 x86_64,函数参数通常能从寄存器里反推。结合反汇编,可以判断哪个参数参与了非法访问。
这一步能把问题从“某函数崩了”缩小到“某个对象字段坏了”。
如果有调试符号,可以用:
addr2line -e vmlinux <address>
或者在 crash/gdb 中查看具体源码行。没有源码行时,也可以通过反汇编判断访问偏移。
第一遍只做判断框架
第一次读 Oops,最好先写下四句话:
- 异常类型是什么
- 崩溃指令访问了什么
- 当前上下文是什么
- 最可疑的对象是谁
这四句话写清楚后,后续分析会稳定很多。否则很容易被调用栈里的熟悉函数名带偏。
结尾
Oops 日志不是答案,而是现场记录。第一遍阅读的价值,是把混乱日志整理成可以验证的假设。
能稳定回答“在哪里、什么上下文、访问了什么、对象为什么会坏”,调试就已经走出了最容易误判的一步。
评论