嵌入式Linux崩溃分析实战:没有JTAG,怎么"空手"定位死机问题?
一句话总结:产品量产了才发现死机,没有JTAG接口,串口日志也不够用------用ramoops/pstore、kdump、ftrace这三个武器,大部分崩溃问题不用硬件调试器也能定位。
做嵌入式Linux产品的人,最怕的不是功能做不出来,而是产品在客户现场死机了,你拿不到任何调试信息。
量产设备通常没有JTAG接口(成本原因或者外壳已经封死),甚至串口都不一定引出来。这时候你怎么定位问题?
90%问题靠以下三个工具可以解决。今天把它们分享一下。
武器一:ramoops/pstore --- 内核崩溃后的"黑匣子"
问题
内核panic了,系统直接重启。你没看到任何日志,只知道"设备又重启了"。
方案
用ramoops(在ARM64上是pstore)保留panic前后的内核日志,重启后自动保存。
操作
内核配置:
ini
CONFIG_PSTORE=y
CONFIG_PSTORE_RAM=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_PMSG=y
CONFIG_PSTORE_FTRACE=y
设备树中预留内存:
dts
reserved-memory {
ramoops@0xBF000000 {
compatible = "ramoops";
reg = <0x0 0xBF000000 0x0 0x00100000>;
record-size = <0x00020000>;
console-size = <0x00020000>;
ftrace-size = <0x00020000>;
pmsg-size = <0x00020000>;
};
};
重启后读取:
bash
# 重启后检查
mount -t pstore pstore /sys/fs/pstore
ls /sys/fs/pstore/
cat /sys/fs/pstore/console-ramoops-0
cat /sys/fs/pstore/dmesg-ramoops-*
真实案例
去年一个客户反馈说设备每天凌晨3点准时重启。我们在ramoops里可以看到类似如下关键线索:
csharp
[12345.678901] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000018
[12345.678902] Internal error: Oops: 96000004 [#1] PREEMPT SMP
[12345.678903] Modules linked in: xxx_driver(O)
[12345.678904] PC is at xxx_driver_ioctl+0x34/0x120 [xxx_driver]
一看就知道是驱动里的ioctl回调里解引用了一个空指针。再看调用栈,是一个定时器在凌晨3点触发的清理任务。问题定位加修复,两小时搞定。如果没有ramoops,你可能要盲目加printk再等两天复现。
武器二:kdump + kexec --- 宕机后拍一张"CT"
问题
ramoops只能保留内核日志文本。如果问题需要查看完整的内存状态(进程列表、内存分配、寄存器值),ramoops不够用。
方案
kdump在系统崩溃时启动一个"捕获内核",把崩溃时的内存状态完整dump下来,后面可以用crash工具分析。
操作
安装:
bash
# 在Buildroot中开启
BR2_PACKAGE_KEXEC=y
BR2_PACKAGE_KEXEC_ZEROHUNTER=y
# 配置内核
CONFIG_KEXEC=y
CONFIG_CRASH_DUMP=y
CONFIG_PROC_VMCORE=y
预留内存(内核启动参数):
ini
crashkernel=64M
触发测试:
bash
# 手动触发panic测试
echo c > /proc/sysrq-trigger
重启后分析:
bash
# vmcore文件位置
ls /var/crash/
# 用crash工具分析
crash vmlinux /var/crash/vmcore
crash工具里最常用的命令:
bash
bt # 看调用栈(哪个函数导致的崩溃)
log # 看内核日志
ps # 看进程列表
vm PID # 看某个进程的内存映射
files PID # 看某个进程打开的文件
什么时候用kdump而不是ramoops?
- 怀疑是内存 corruption 问题
- 需要看进程间的资源竞争状态
- 需要分析spinlock死锁
- 内核panic但日志信息不够
kdump的代价是预留内存(通常64-128MB)和重启后转储的时间。对于内存敏感的设备,ramoops更轻量。
武器三:ftrace --- 追踪"慢动作"和"神秘挂起"
问题
系统没崩溃,但莫名其妙"卡住了"------某个线程不响应了,或者中断响应延迟突然变大。这种问题不致命但很烦人。
方案
ftrace是内核内置的追踪器,可以记录函数调用、中断开关、调度事件。关键是零开销(启用时才有额外开销)。
操作
bash
# 挂载tracefs
mount -t tracefs tracefs /sys/kernel/tracing
# 查看可用的追踪器
cat /sys/kernel/tracing/available_tracers
# 启用function_graph追踪
echo function_graph > /sys/kernel/tracing/current_tracer
echo my_driver_function > /sys/kernel/tracing/set_ftrace_filter
echo 1 > /sys/kernel/tracing/tracing_on
# 复现问题后
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace
高阶用法:追踪中断延迟
bash
# 追踪irq_disable时间
echo irqsoff > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# 或者追踪抢占关闭时间
echo preemptoff > /sys/kernel/tracing/current_tracer
真实案例
一个工控设备偶尔出现"控制输出延迟响应"。用ftrace的irqsoff追踪器发现,某个驱动的spin_lock_irqsave持有了超过50ms。
bash
# irqsoff追踪输出(简化)
# tracer: irqsoff
#
# irqsoff latency 50321 us (临界区长度50ms!)
# 导致中断被屏蔽了50ms
#
# 罪魁函数: xxx_driver_do_blocking_io
# 调用路径:
# xxx_driver_write -> spin_lock_irqsave -> xxx_driver_do_blocking_io
问题:驱动在spinlock保护的临界区里做了阻塞操作(等待硬件完成标志)。spinlock的本意是"短时持有",但硬件响应慢了,导致中断被屏蔽了50ms。
fix很简单:用mutex代替spinlock,或者把等待移到临界区外面。
三件武器的选择策略
| 场景 | 工具 | 原因 |
|---|---|---|
| 内核panic重启 | ramoops/pstore | 轻量,不需要预留大块内存 |
| 需要完整内存分析 | kdump | 可做post-mortem分析 |
| 系统卡死/延迟大 | ftrace | 零基线开销,精准追踪 |
| 用户态进程崩溃 | core dump | 跟嵌入式Linux关系不大,用ulimit -c |
总结
没有JTAG不代表不能调试。嵌入式Linux内核提供了足够多的调试工具,关键是你知不知道用。
我的习惯是:所有量产产品必开ramoops + pstore,这是一个配置项的事儿,成本为零,收益巨大。kdump在内存充足(256MB以上)的设备上也推荐开启。ftrace则是按需启用,系统出问题了再打开复现。
别再跟我说"量产设备没法调试"------不是没法调试,是你没配。这三个工具配置一次,能用一辈子。
P.S. 如果你还在用"出问题就重新烧个带printk的固件等复现"的方式,看完这篇,花半天时间把ramoops加上。下次客户半夜打电话说设备死机了,你不用去现场,远程要个日志文件就能定位。这才是嵌入式工程师该有的水平。