大家好,我是仲一。
做嵌入式这些年,"设备挂了"这四个字,在不同人口中是完全不同的信息量。
研发自己试出来的挂,通常会给你一堆日志:Oops、panic、调用栈。这时候定位相对好办。真正难的是另一种——现场的设备挂了,客户只丢给你一句话:"黑屏了,起不来了。" 没有日志,没有栈,甚至不知道是软件崩的还是硬件死的。
这类问题我处理过不少,一套固定的流程走下来,大多数能有个着落。今天把这套流程串一遍:从拿到一个"黑屏",到最终定位到出问题的那一行,中间每一步该干什么。
先分清,设备到底是"怎么挂"的
拿到黑屏,第一件事不是急着分析,是先判断挂的类型。不同挂法,后续手段完全不同。
大致分三类。
内核还能输出,崩在某个点上。这时串口或日志里通常有 Oops 或者 panic,带调用栈,顺着栈往下挖就行,这是最友好的一种。
系统卡死,不是崩。比如死锁了,或者某段代码关了中断然后死循环。这种内核来不及打印什么,因为它已经"转不动"了——CPU 空转着等一把永远等不到的锁,中断被禁了谁也救不了。
硬件层面的复位。板子直接重启,串口啥都没有,像被拔了电。
判断完类型,就知道走哪条路。内核能输出的直接读日志;卡死的得靠 NMI 看门狗这类机制去"撬"现场;硬件复位的要回到复位原因去查。
卡死比崩溃更难搞:得靠"监工"撬真相
先说第二类,因为它最坑。
死锁或者关中断死循环这类问题,麻烦在:内核已经不会自己报告了。它卡住了,中断关了,软硬件都叫不醒。你要是干等,等到天荒地老也没日志。
这时候得靠 NMI 看门狗。你可以把它理解成内核里的一个监工:正常情况下 CPU 每秒产生大量时钟中断,说明系统活着。如果监工发现,好几秒了一个中断都没来,它就判断"坏了,中断被禁死了",于是动用 NMI(不可屏蔽中断)强行打断,把现场打出来,或者触发重启。
所以你去看内核日志,那些 soft lockup、hard lockup 的报错,多半就是它在干活。这也是为什么别把看门狗当敌人——它平时不吭声,关键时刻是把卡死现场的"唯一目击者"。
硬件复位:最容易被当成软件问题的一类
第三类硬件复位,比前两类都坑。因为它的现场干干净净——没有 Oops,没有卡死日志,板子就像被人拔了电。这时候如果脑子还停在软件思路上,最容易白费功夫。
第一步永远是查复位原因,别猜。看 SoC 的复位原因寄存器(很多 ARM 平台有 RESET_REASON 之类的寄存器,PMIC 也能查),区分到底是哪一路把板子复位了:看门狗超时、电压跌落、外部复位引脚被拉低、还是 ESD/浪涌这类瞬态触发。
这一步的意义在于定方向。如果复位原因指向电源或者外部信号,那再漂亮的软件分析都是白费——这时候该上的是示波器和电源分析仪,不是 gdb。我见过不少案子,软件排查了一星期,最后示波器一挂,发现是某路电源在低温下瞬间跌落触发的复位。
所以硬件复位这条路的钥匙,在硬件那一侧。先读寄存器,再看波形,别急着怀疑自己的代码。
真崩了,第一反应不是分析,是"保现场"
假设内核确实崩了,Oops 也打出来了。这时候新手最容易犯的错是:赶紧看栈,开始分析。
先别急。崩都崩了,当务之急是把现场完整保存下来。因为 Oops 打印的只是冰山一角——它可能只给了当前 CPU 的栈,但系统当时可能有八个核、几百个进程、复杂的内存状态,这些 Oops 里都没有。
保存现场靠 kdump。它的思路很巧妙:系统里其实住了"两个内核"。平时跑的是生产内核,另外预留一块内存,里面提前装好一个精简的捕获内核。
崩溃的瞬间,生产内核不挣扎了,直接跳进捕获内核,由它把整个生产内核的内存快照(也就是 vmcore)写到磁盘。这块内存是提前预留的,所以崩溃那一刻即使磁盘驱动已经坏了,捕获内核也能靠自己的驱动把数据写出去。
关键在这:kdump 必须提前配好。 等崩了再装就来不及了——崩溃那一刻,内核已经没能力从磁盘读任何东西了。所以这类问题真正的功夫,在"没出事的时候"。
很多现场是嵌入式设备,根本没有磁盘给你存 vmcore。这时候有个轻量方案——pstore。它利用一小块掉电不丢失的内存(ramoops),把内核最后打印的日志存进去,重启后还能读出来。
但要清楚 pstore 的边界:ramoops 通常只有几十到几百 KB,日志一长就会被截断,你拿到的可能只是最后几行,不是崩溃前的完整上下文;而且它是在 panic 路径里同步写的,如果电源跌得比写入还快——比如瞬间掉电——连这几行也保不住。它是"最后一根稻草",不是万能保险,别把它当成 kdump 的平替。
拿到 vmcore,才轮到 crash 出场
现在手里有 vmcore 了,分析工具轮到 crash。它是红帽工程师写的,本质是给内核转储文件配了个调试器,可以看堆栈、看 dmesg、查内核数据结构、反汇编。
在 crash 里最常用的几个命令:
crash> bt # 看调用栈
crash> log # 看内核日志,包括崩溃前的 dmesg
crash> ps # 看崩溃时系统里有哪些进程、什么状态
crash> kmem -i # 看内存使用概况,排查 OOM / slab 泄漏嫌疑
crash> dis -l 地址 # 反汇编,定位到具体哪一行
打开 vmcore 后,crash 会先给你一段概要——内核版本、崩溃时间、panic 原因、出事的进程:
crash>
KERNEL: /usr/lib/debug/vmlinux-5.10.0
DUMPFILE: vmcore [PARTIAL DUMP]
CPUS: 4
DATE: ...
UPTIME: ...
PANIC: "BUG: unable to handle kernel NULL pointer dereference at ..."
PID: 1234
COMMAND: "kworker/u8:2"
第一件事永远是 log,崩溃前最后那几行内核日志往往直接告诉你死因——空指针、非法内存访问,还是别的。然后是 bt 看栈,栈会告诉你崩时执行到哪个函数、是谁调进来的。如果怀疑跟内存有关,kmem -i 能快速看一眼内存概况——有时候 OOM 或者 slab 泄漏的嫌疑,这里比 ps 更早暴露。
一个典型的崩溃长什么样
拿最常见的空指针解引用举例。这种崩溃的内核日志,关键几行通常长这样:
BUG: unable to handle kernel NULL pointer dereference at virtual address 0000000000000000
...
pc : my_driver_worker+0x34/0x100
lr : ...
...
Call trace:
my_driver_worker+0x34/0x100
process_one_work+0x...
worker_thread+0x...
kthread+0x...
NULL pointer dereference,访问了地址 0。pc 指向 my_driver_worker,Call trace 显示是工作队列线程调进来的。
到这一步,线索是:"某个驱动的 worker 函数里解了空指针,发生在工作队列线程里。" 但空指针哪来的?为什么这个 worker 里会有个空指针?光看日志还不够。
如果之前配了 kdump,重启后能在 /var/crash 下找到 vmcore。开 crash:
crash> bt
能看到崩溃那一刻的完整调用栈和寄存器。再配合 log 看崩溃前内核在忙什么。如果怀疑是某个结构体指针没初始化,可以用 crash 直接查那个结构体的内容,看它到底是没分配、还是被释放了、还是被写坏了——这三种情况的处理方向完全不同。
到这一步,问题基本就圈定在一个具体的怀疑点上了,剩下的就是回代码里确认。
功夫一半在平时
把这套流程走一遍你会发现,真正难的往往不是最后那一下"找到 bug",而是中间每一环都别断:先分清挂的类型,卡死靠 NMI 撬现场,崩溃靠 kdump 保现场,硬件复位去查复位原因,最后靠 crash 从 vmcore 里挖真相。任何一环没准备好,到黑屏那一刻都是零。
所以内核调试这事,功夫一半在平时。留个最简单的自查清单,你回工位就能对一遍:
kdumpctl status
-
- /
systemctl status kdump
-
- ——kdump 服务活着吗?启动参数里有没有
crashkernel=
-
- ——内存预留了吗?
/sys/class/watchdog
- 或 pstore 挂载验证——看门狗和 pstore 真的在工作吗?
这三条都过了,设备再挂你才手里有粮。要是哪条没过,别等客户电话,现在就去配好。
你遇到过"黑屏"最后查到是电源、ESD、看门狗超时、还是 kdump 没配好的情况?留言聊聊。
仲一做嵌入式已经六七年了,现在在某头部芯片公司做驱动开发,之前带过团队、也面试招过人。简历看多了,面试也面多了,所以职业怎么走、简历哪里不对、面试卡在哪儿,这些事我算是有点发言权。
这些年陆陆续续有读者找过来,问方向、问简历,也拉我做过不少模拟面试。带过的师弟师妹有大二进小米海康的,也有四个月拿到大疆、oppo、华为 offer 的,简历前前后后改过两千多份。
就一个要求:建议你得真去做。不然别来找我,浪费彼此时间。
职业方向、简历、面试,哪块有疑惑都可以找我聊聊。想聊的,公众号后台回复「咨询」。
43