嵌入式调试,从“打印重启”到精准定位的跃迁 🛠️
你有没有经历过这样的夜晚?
设备突然死机,串口只留下一行模糊的日志:
Unable to handle kernel NULL pointer dereference...
。
你反复烧录、重启、加
printk
,像盲人摸象般猜测问题出在哪。几个小时过去,问题依旧,而客户那边已经开始催交付了。
这曾是很多嵌入式开发者的日常。但其实,我们手里早就有一套强大的工具集——它们不依赖图形界面,不需要昂贵的硬件探针,只需要一个串口或 SSH 连接,就能让你在资源受限的 ARM 板上,完成对系统状态的全面“体检”。
今天,我们就来聊聊那些真正能帮你 把调试从“玄学”变成科学”的 Linux 命令 。这些不是教科书里的冷知识,而是我在车载网关、工业 PLC 和边缘 AI 盒子项目中,一次次踩坑后总结出的实战利器。
内核说了什么?用
dmesg
听懂它的“低语” 🔊
当你插上 USB 模块,系统却毫无反应时,第一个该问的不是应用程序,而是内核:“你看到它了吗?”
这就是
dmesg
的主场。它读取的是内核的 ring buffer——一块记录着从开机那一刻起所有关键事件的内存区域。驱动加载失败、设备树匹配不上、DMA 传输错误……这些底层信号都会第一时间出现在这里。
dmesg | tail -30
这条命令我几乎每天敲十几次。特别是在新硬件首次上电时,我会盯着最后几十行输出,看是否有类似这样的线索:
i2c_designware ff3c0000.i2c: can't claim bus, busy
my_sensor_driver: probe of ff3c0000.i2c failed with error -16
短短两行,信息量巨大:
- I²C 总线被占用(可能是时序问题或物理冲突)
- 驱动初始化直接失败
- 错误码
-16
对应
EBUSY
—— 不是代码逻辑错,是资源争抢!
这时候再去查电路设计和设备树配置,效率高得多。
更聪明地使用
dmesg
别再无差别翻屏了。学会过滤,让问题自己跳出来:
# 只看错误和警告
dmesg -l err,warn
# 实时监控新消息(带人类可读时间)
dmesg -H --follow
--follow
模式特别适合热插拔场景。比如测试 PCIe 扩展卡时,我常开两个终端:一个跑
dmesg -Hf
,另一个执行插拔操作。每一次物理动作对应哪些内核行为,清清楚楚。
⚠️
但要注意
:ring buffer 大小有限!默认可能只有 512KB,在长时间运行或日志密集的系统中,早期的关键错误很可能已经被覆盖。如果你发现
dmesg
输出太少,记得检查内核配置是否启用了
CONFIG_LOG_BUF_SHIFT=18
(即 256KB)甚至更大。
更好的做法是结合 syslog 或 journald 做持久化存储——后面会讲到。
谁在吃我的 CPU 和内存?进程监控的艺术 🕵️♂️
有时候你会觉得板子“变慢了”,SSH 登录延迟,UI 卡顿。这时候第一反应应该是: 哪个进程在作祟?
快照式排查:
ps aux
ps
就像给系统拍一张照片。最经典的组合就是:
ps aux
输出看起来平平无奇,但每一列都有故事:
-
USER
:是不是某个服务以 root 权限运行?安全隐患预警。
-
%CPU
/
%MEM
:有没有异常占用?
-
STAT
:看到
Z
(zombie)了吗?说明有父进程没回收子进程。
-
COMMAND
:完整命令行能暴露很多细节,比如参数是否正确传递。
有一次我发现一个语音唤醒服务总是崩溃重启,
ps
显示它每分钟都短暂出现又消失。进一步用:
ps aux | grep wakeup
确认 PID 变化频繁,基本锁定为守护进程管理问题。后来查出是 systemd 的 restart 策略设置不当,导致无限循环拉起失败的服务。
动态观察:
top
是你的“任务管理器”
如果说
ps
是静态快照,那
top
就是实时直播。
top
一进界面,先按
Shift + M
按内存排序,再按
Shift + P
切回 CPU 排序。几秒钟就能看出谁是资源大户。
我还喜欢用批处理模式做自动化采集:
top -b -n 10 -d 2 > system_load.log
这表示以非交互方式运行,采样 10 次,每次间隔 2 秒。生成的日志可以传回分析,尤其适合现场无法长期连接的情况。
💡
小技巧
:在
top
界面里按
1
可以展开显示每个 CPU 核心的负载情况。对于多核 ARM 平台(如 i.MX8),这能帮你判断任务调度是否均衡。
不过得提醒一句:
top
自身也会消耗一点 CPU。在某些超轻量级系统(比如基于 MCU+Linux 的 RPMsg 架构)上,建议控制刷新频率,或者改用更轻量的
ps
脚本轮询。
程序为什么卡住了?用
strace
看穿系统调用 💡
你有没有写过这样的程序?
int fd = open("/dev/i2c-1", O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
结果运行时报错,但
perror
只告诉你 “No such file or directory”。问题是:到底是路径错了?节点没创建?还是权限不够?
这时候
strace
就派上用场了。
它是怎么工作的?
简单说,
strace
利用
ptrace()
系统调用,“附身”到目标进程上,拦截它每一次进入内核的动作。无论是打开文件、读写设备、发送信号,全都无所遁形。
试试这个命令:
strace ls /dev/ttyS*
你会看到类似这样的输出:
openat(AT_FDCWD, "/dev/ttyS0", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFCHR|0660, st_rdev=makedev(4, 64), ...}) = 0
close(3) = 0
看到了吗?
ls
其实是通过
openat
打开设备节点来判断是否存在。如果返回
-1 ENOENT
,那说明
/dev/ttyS0
根本不存在——可能是串口驱动没加载,也可能是设备树里没声明。
实战案例:定位阻塞点
曾经有个项目,应用启动时总是在某一步卡住十几秒才继续。用
ps
看进程状态是
S
(sleeping),怀疑是网络请求超时。
于是我们附加
strace
:
strace -p $(pgrep myapp)
很快发现问题所在:
connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("192.168.1.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
...
poll([{fd=5, events=POLLOUT}], 1, 5000) = 0 (Timeout)
原来是连接局域网某服务时设置了 5 秒超时,但由于网络不通,每次都耗尽等待时间。最终解决方案是优化重试机制,并增加本地降级策略。
⚠️ 注意权限问题 :某些系统默认禁止跨用户追踪进程。你需要确保:
echo 0 > /proc/sys/kernel/yama/ptrace_scope
否则会遇到
Operation not permitted
。
另外,
strace
会让程序变慢不少,别在生产环境长时间开启。调试完记得及时退出。
网络不通?别急着换网线,先看看 socket 状态 🌐
在网络密集型设备中(比如视频流网关、远程监控终端),最常见的问题之一就是“连不上”、“断连频繁”。
很多人第一反应是 ping、抓包、换路由器……但其实应该先问一句: 当前有哪些连接?处于什么状态?
netstat
已老,
ss
正当时
传统工具
netstat
虽然功能全,但它需要遍历
/proc/net/tcp
等多个文件,性能较差,尤其在连接数上千时,卡顿明显。
现代替代品
ss
(Socket Statistics)直接通过 netlink 接口与内核通信,速度快得多。
# 查看所有监听端口
ss -tuln
输出示例:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 128 *:22 *:*
tcp LISTEN 0 100 *:8080 *:*
udp UNCONN 0 0 *:5353 *:*
一眼就能看出:
- SSH(22)、Web 服务(8080)正常监听
- mDNS 在用 UDP 5353 端口
如果发现某个预期的服务没在这里,那就不是网络问题,而是服务根本没启动。
更深入的诊断
# 查看已建立的 TCP 连接
ss -tnp | grep ESTAB
-t
TCP,
-n
不解析服务名,
-p
显示关联进程。输出中能看到源/目的 IP 和端口,以及对应的 PID/程序名。
有一次我们遇到设备频繁重连 MQTT 服务器的问题。用这条命令发现:
ESTAB 0 0 192.168.1.100:49876 10.0.0.50:1883 users:(("mosquitto",pid=456,fd=6))
但几分钟后同一个 PID 消失了,新的连接来自不同端口。说明客户端主动断开了。再结合
dmesg
发现电源波动导致 RF 模块复位,最终定位到供电设计缺陷。
高级用法:查看连接详情
ss -i -p -t src :8080
加上
-i
可以看到 RTT、拥塞窗口、重传次数等 TCP 内部指标。如果发现重传率偏高(>5%),就要考虑网络质量或缓冲区配置问题了。
🛠️
提示
:BusyBox 默认不包含
ss
。如果你的系统里没有,记得在 Buildroot 或 Yocto 中显式添加
iproute2
包。
日志太多太乱?让
journalctl
统一起来 🗂️
以前我们在嵌入式系统中常用
syslog + rsyslog
来集中管理日志。但现在越来越多项目转向
systemd
,相应的,
journalctl
成为了首选工具。
它最大的优势是什么? 结构化日志 。
每条日志不再是一段纯文本,而是带有元数据的记录:
- 时间戳(精确到微秒)
- 优先级(emerg、alert、crit、err、warning、notice、info、debug)
- 来源单元(unit)
- PID、UID、主机名……
这意味着你可以做精准查询。
实用命令合集
# 查看全部日志(最新在后)
journalctl
# 查看某个服务的日志
journalctl -u bluetooth.service
# 实时跟踪
journalctl -f
# 查看本次启动以来的日志
journalctl -b
最后一个非常有用!避免看到几天前的历史记录干扰当前问题分析。
还可以按时间筛选:
journalctl --since "2024-03-15 14:00" --until "14:30"
或者组合条件:
journalctl -u myapp.service -p err
只看
myapp.service
的错误级别日志。
如何避免日志刷爆 Flash?
没错,这是个现实问题。嵌入式设备常用 eMMC 或 SPI Flash 存储,寿命有限。持续高频写日志可能导致存储提前损坏。
解决办法有三:
-
限制日志级别 :生产版本关闭 debug 输出。
bash systemctl set-log-level warning -
配置保留策略 :编辑
/etc/systemd/journald.conf
[Journal] SystemMaxUse=100M MaxFileSec=1week -
启用远程转发 :
ForwardToSyslog=yes
将日志发往外部日志服务器(如 ELK 或 Loki),本地不留存。
我在一个车载 T-Box 项目中就采用了第三种方案。车辆运行时所有日志实时上传云端,既保证了可追溯性,又延长了 eMMC 寿命。
二进制看不懂?
hexdump
和
xxd
来帮忙 🔤
当你要调试 EEPROM、Flash 固件、设备树 blob(.dtb)、core dump 文件时,总会遇到一堆原始字节。
这时你需要一种方式,把它们变成人类能看懂的形式。
hexdump -C
:最常用的格式
hexdump -C firmware.bin | head -n 10
输出长这样:
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 b7 00 01 00 00 00 78 00 00 00 00 00 00 00 |........x.......|
左边是偏移地址,中间是十六进制数据,右边是 ASCII 显示(不可打印字符用
.
)。这种布局清晰直观,适合快速浏览头部信息。
比如上面这段开头是
7f 45 4c 46
,正是 ELF 文件魔数,说明是个标准可执行文件。
xxd
:更灵活的选择
xxd
的优势在于支持反向转换:
# 生成紧凑 hex 文本(用于嵌入配置)
xxd -p config.bin > config.hex
# 还原回去
xxd -r -p config.hex > config_restored.bin
我还常用它来构造测试数据:
# 创建一个 64 字节的 pattern 数据
yes 'Aa0' | head -c 64 | xxd
然后写入模拟传感器输入,验证协议解析逻辑。
实战:分析设备树兼容性
假设你的驱动始终不加载,怀疑是
.dtb
里的
compatible
字符串写错了。
可以用:
hexdump -C /boot/myboard.dtb | grep -A2 -B2 "sensor_name"
搜索关键词前后内容,看看实际编译进去的字符串是不是和 DTS 文件一致。有时候因为宏定义或拼写错误,会导致匹配失败。
一套完整的调试流程:从现象到根因 🧩
让我们把上面这些工具串起来,走一遍真实的调试路径。
场景:I²C 温度传感器读数异常
现象
:应用读取温度总是返回
-1
,偶尔正常。
第一步:看内核说了啥
dmesg | tail -20
发现:
i2c i2c-1: failed to transfer [1] bytes to 0x48
my_temp_driver: read temperature failed: -110
-110
是
ETIMEDOUT
,说明 I²C 通信超时。方向明确了:总线层面问题。
第二步:确认设备存在且驱动加载
ls /sys/bus/i2c/devices/
输出:
1-0048 1-0049
地址
0x48
存在,说明设备探测成功。再查驱动模块:
lsmod | grep my_temp
也在。排除驱动未加载的可能。
第三步:动态跟踪应用行为
strace -p $(pgrep temp_app)
看到:
read(3, "\x01\x02\x03", 3) = 3
ioctl(3, I2C_RDWR, 0xbeaa3f40) = -1 ETIMEDOUT (Connection timed out)
果然,在调用
ioctl
发送 I2C 请求时失败。说明问题不在应用层读写,而在内核 I2C 子系统。
第四步:检查系统负载
top
发现有个图像处理线程占用了近 90% CPU。怀疑是高负载导致 I²C 中断响应延迟。
尝试降低其优先级:
renice -10 $(pgrep img_proc)
再次测试,读数恢复正常。
✅ 结论 :CPU 负载过高 → I²C 中断得不到及时处理 → 通信超时。
最终解决方案是将图像处理任务迁移到独立核心(使用 CPU affinity),并为 I²C 设置更高的中断优先级。
工具之外:构建可持续的调试能力 ⚙️
掌握命令只是起点。真正高效的团队,会在工程层面做好准备。
1. 裁剪系统时别砍掉“救命工具”
用 Buildroot 或 Yocto 构建固件时,为了节省空间,很容易把
strace
、
iproute2
、
systemd
当成“非必需”组件去掉。
但等到现场出问题,才发现没法查
ss
、不能
strace
,只能靠加日志重新烧录——这种代价远高于那几十 MB 的存储空间。
建议做法:
- 开发版:保留完整调试工具链
- 生产版:选择性移除 GUI、文档等,但保留
strace
,
ss
,
hexdump
,
gdbserver
2. 编译时带上符号信息
无论是内核还是应用,都要开启调试符号:
-
内核:
CONFIG_DEBUG_INFO=y -
应用:编译加
-g参数
这样即使要用
gdb
远程调试,也能看到函数名、变量值,而不是一堆地址。
当然,发布前可以用
strip
去除符号减小体积。
3. 统一日志体系 + 时间同步
多个模块各自打日志,时间还不准?联合分析时简直噩梦。
必须做到:
- 使用
systemd-journald
或统一 syslog 格式
- 启用 NTP 或 PTP 同步时间
- 关键事件打时间戳(UTC)
我见过太多因为“相差 8 小时”而导致误判的问题。
4. 远程日志收集机制
理想情况下,设备应具备自动上报日志的能力。比如:
- 异常重启后,自动打包最近 5 分钟日志上传
-
提供 REST API 获取
journalctl输出 - 支持 OTA 更新时附带诊断包下载
这些设计看似琐碎,但在大规模部署时,能极大降低运维成本。
写在最后:调试的本质是理解系统 🧠
这些命令本身并不神秘。但为什么有些人能十分钟定位问题,有些人却要折腾好几天?
区别往往不在工具,而在 对系统的整体认知 。
你知道
dmesg
背后是
printk
缓冲区;
你知道
ps
读的是
/proc/[pid]/stat
;
你知道
strace
依赖
ptrace
;
你也知道
ss
比
netstat
快是因为绕过了用户态解析……
正是这些底层理解,让你能在复杂现象中迅速抓住主线。
所以,不要满足于“怎么用”,更要追问“为什么能用”。
当你开始思考这些问题时,你就不再是被动使用者,而是真正意义上的系统工程师了。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
531

被折叠的 条评论
为什么被折叠?



