内核 panic 发生以后,最容易犯的错误是马上去改代码。日志里出现了某个函数名,就开始怀疑这个函数;调用栈里出现了某个驱动,就开始怀疑这个驱动;最近合入过一个补丁,就直接认为它是根因。
这些判断可能最后是对的,但如果没有把现场固定下来,后面很难区分“证据”和“直觉”。对内核问题来说,第一个小时的工作质量,往往决定了后面一天甚至一周的效率。
先保护现场
panic 现场的第一件事不是分析,而是保存。保存动作包括:
- 完整串口日志
- 触发条件和复现步骤
- kernel config
- vmlinux 和 System.map
- dmesg 中 panic 之前的上下文
- 模块版本和加载顺序
- 设备树、启动参数、固件版本
- 如果有 kdump,保存 vmcore
如果是偶现问题,还要记录当时的负载、运行时长、外设状态、网络状态和电源状态。偶现问题最怕只保存最后几十行 panic 日志。最后几十行通常只能说明“死在哪里”,不能说明“为什么走到这里”。
不要只看最后一屏
panic 文本通常会把注意力吸到最后一个 fault 上。例如:
Unable to handle kernel NULL pointer dereference
pc : foo_irq_handler+0x48/0x120
lr : __handle_irq_event_percpu+0x64/0x1b0
这说明 CPU 在 foo_irq_handler 访问了空指针,但不等于 foo_irq_handler 一定是根因。它可能只是第一个被错误状态影响的执行点。
更有价值的问题是:
- 这个指针在哪里初始化
- 生命周期由谁管理
- 是否存在 probe 失败后的半初始化状态
- remove、suspend、runtime PM 是否可能释放资源
- 中断是否可能在资源准备好之前打开
- 是否存在并发路径同时修改这个字段
内核调试里,faulting instruction 是入口,不是结论。
把调用栈分层
调用栈不应该只当作函数列表。更好的方式是把它分成几层:
- 触发层:用户态、软中断、硬中断、workqueue、timer、kthread
- 框架层:IRQ、VFS、block、net、driver core、PM core
- 子系统层:具体 subsystem 的回调路径
- 驱动层:厂商驱动或板级代码
- 故障点:真正发生异常的指令和对象
这样分层以后,问题会更清楚。比如同样是驱动函数崩溃,如果触发层是硬中断,分析重点会偏向锁、寄存器状态和中断打开时机;如果触发层是 workqueue,重点会偏向对象生命周期、取消 work 的顺序和并发释放。
先确认异常类型
panic 类型决定分析方向。常见几类:
- NULL pointer dereference:关注初始化、错误路径、生命周期
- use-after-free:关注引用计数、RCU、异步 work、remove 顺序
- general protection fault:关注野指针、结构体越界、函数指针损坏
- hung task:关注锁、IO 等待、调度点和不可中断睡眠
- soft lockup:关注长时间关抢占、死循环、持锁忙等
- hard lockup:关注中断关闭、NMI、硬件访问卡死
- slab corruption:关注越界写、重复释放、错误对象类型
不要把所有 panic 都当成“空指针”处理。异常类型本身已经给了第一轮排查方向。
建立最小假设集
第一小时的目标不是直接找到根因,而是建立一个足够小的假设集。一个好的假设应该能回答三个问题:
- 它能解释当前日志吗
- 它能解释触发条件吗
- 它能被验证或排除吗
例如:
假设 A:中断在 probe 完成前被打开,handler 访问未初始化私有数据。
验证:检查 request_irq、enable_irq、硬件 unmask、私有结构体初始化顺序。
假设 B:runtime suspend 释放了寄存器映射,中断晚到后访问失效资源。
验证:增加 PM trace,检查 irq disable 与资源释放顺序。
假设 C:错误路径没有清理状态,下一次 probe 复用脏状态。
验证:强制 probe defer 或注入资源申请失败,观察状态恢复。
这比“怀疑驱动有问题”有用得多。
用 addr2line 还原具体代码
如果有 vmlinux,第一步通常是把 PC 映射回源码行:
addr2line -e vmlinux -fip 0xffffffff81234567
如果是模块,需要确认模块加载基址,再换算偏移。对于发行版或厂商内核,必须确认 vmlinux 和运行内核完全匹配。版本号相同不代表符号一定匹配,编译选项、补丁集、LTO、调试信息都会影响结果。
如果源码行定位到一个结构体字段访问,下一步不是马上判空,而是追这个对象的来源。判空可能只是把 panic 变成静默失败,并没有解决状态机错误。
日志要按时间线重排
很多问题不是单点事件,而是时间线问题。建议把日志按阶段整理:
- boot 参数和内核版本
- 设备枚举和 probe
- 资源申请和中断注册
- 第一次异常日志
- panic 前的 warning、timeout、reset
- panic 栈
如果日志中有多个 CPU 的输出交错,还要注意时间戳。串口输出顺序不一定等于真实执行顺序,尤其是在多核并发和中断上下文中。
第一小时的产出
一小时后,理想产出不是补丁,而是一份简短的问题卡片:
现象:
系统运行约 20 分钟后 panic,fault 在 foo_irq_handler。
环境:
kernel 版本、config、设备树、启动参数、板卡版本。
关键日志:
panic 栈、panic 前 warning、相关 probe 日志。
当前判断:
更像生命周期问题,不像单纯寄存器访问失败。
待验证假设:
1. 中断打开早于私有数据初始化
2. runtime PM 后中断未关闭
3. remove 错误路径残留状态
下一步:
加 trace,复现并确认对象初始化和中断打开顺序。
这份卡片能让后续调试有方向,也方便和同事同步。内核问题最怕每个人都看过日志,但每个人脑子里的问题模型都不一样。
结论
panic 分析的核心不是背命令,而是把混乱现场变成可验证的问题模型。第一个小时要做的事情很朴素:保存现场、还原路径、区分现象和根因、建立假设、设计验证。
真正的内核调试能力,体现在能不能把一个看似随机的崩溃收敛成几个具体问题,而不是在日志里寻找一个最像罪魁祸首的函数名。
评论