内核 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 分析的核心不是背命令,而是把混乱现场变成可验证的问题模型。第一个小时要做的事情很朴素:保存现场、还原路径、区分现象和根因、建立假设、设计验证。

真正的内核调试能力,体现在能不能把一个看似随机的崩溃收敛成几个具体问题,而不是在日志里寻找一个最像罪魁祸首的函数名。