2、基础调试工具(上):printk与动态调试、devmem与寄存器调试、strace系统调用跟踪
调试嵌入式Linux,说白了就是跟内核和硬件打交道。很多刚入行的朋友一遇到系统卡死、驱动异常,第一反应就是加打印、看log。但怎么加、加在哪、加完怎么看,这里头门道不少。今天我就把几个最基础、也最实用的调试工具掰开揉碎了讲清楚。
本章核心脉络:从内核日志输出(printk/动态调试),到硬件寄存器直接访问(devmem),再到用户态系统调用追踪(strace),覆盖了从软件到硬件、从内核到用户态的完整调试链路。
2.1 printk:内核调试的"瑞士军刀"
printk 是内核态最直接的输出手段。它的用法和 printf 几乎一样,但多了个日志级别。我个人习惯在驱动初始化、中断处理、关键路径上都放几个 printk,这比任何调试器都来得快。
2.1.1 日志级别
printk 有 8 个级别,从 KERN_EMERG(0)到 KERN_DEBUG(7)。级别越低,越紧急。默认情况下,控制台只显示级别小于 console_loglevel 的消息。
| 宏定义 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
| KERN_EMERG | 0 | 紧急事件 | 系统崩溃前 |
| KERN_ALERT | 1 | 必须立即处理 | 硬件故障 |
| KERN_CRIT | 2 | 严重错误 | 驱动加载失败 |
| KERN_ERR | 3 | 错误 | DMA传输失败 |
| KERN_WARNING | 4 | 警告 | 资源不足 |
| KERN_NOTICE | 5 | 正常但重要 | 设备枚举 |
| KERN_INFO | 6 | 信息 | 驱动版本 |
| KERN_DEBUG | 7 | 调试信息 | 函数入口/出口 |
我的经验:调试阶段我习惯把 console_loglevel 设为 8,这样所有级别的 printk 都能在串口看到。命令很简单:echo 8 > /proc/sys/kernel/printk。但产品发布前一定要改回来,否则调试信息刷屏会影响性能。
2.1.2 使用示例
// 驱动初始化时
printk(KERN_INFO "mydriver: probe start, device id = 0x%x\n", dev_id);
// 中断处理中
printk(KERN_DEBUG "mydriver: irq handler, status = 0x%x\n", reg_val);
// 错误处理
printk(KERN_ERR "mydriver: DMA timeout, resetting...\n");
嗯,这里要注意:中断上下文里别用 printk 打印太多东西,尤其是高频中断。我曾经在一个项目中,GPIO 中断里加了 printk,结果系统直接卡死——打印太慢,中断嵌套了。
2.2 动态调试:按需开启,精准定位
printk 有个问题:编译时就得决定打不打印。如果产品已经发布,想再加个调试信息就得重新编译内核,太麻烦了。动态调试(dynamic debug)就是解决这个问题的。
2.2.1 基本原理
动态调试利用内核的 dynamic_debug 模块,可以在运行时动态开启或关闭某个文件、某个函数、甚至某一行代码的调试输出。它基于 pr_debug()、dev_dbg() 这些宏。
2.2.2 操作命令
控制接口在 /sys/kernel/debug/dynamic_debug/control。我个人最常用的几个命令:
# 开启某个文件的所有调试信息
echo "file mydriver.c +p" > /sys/kernel/debug/dynamic_debug/control
# 开启某个函数的调试信息
echo "func my_irq_handler +p" > /sys/kernel/debug/dynamic_debug/control
# 开启某一行
echo "file mydriver.c line 123 +p" > /sys/kernel/debug/dynamic_debug/control
# 查看当前所有动态调试点
cat /sys/kernel/debug/dynamic_debug/control
注意:使用动态调试前,内核必须开启 CONFIG_DYNAMIC_DEBUG。Tina Linux 默认是开启的,但如果你自己裁剪内核,记得加上。另外,调试信息用的是 pr_debug(),不是 printk(KERN_DEBUG)。
2.2.3 实战技巧
我在调试 USB 驱动时遇到过一个问题:设备插入后枚举失败,但不知道卡在哪一步。用动态调试把 USB 核心层的所有调试信息打开:
echo "file drivers/usb/core/* +p" > /sys/kernel/debug/dynamic_debug/control
然后插拔设备,串口刷刷刷地打印出每一步的调用链。一看日志,发现是在获取描述符时超时了。嗯,问题就出在硬件信号质量上。
2.3 devmem:直接操作硬件寄存器
调试硬件驱动时,最直接的手段就是读写寄存器。devmem 是一个用户态工具,可以读写物理内存地址。说白了,它就是一把"硬件调试的螺丝刀"。
2.3.1 基本用法
# 读 32 位寄存器
devmem 0x01C20800
# 写 32 位寄存器
devmem 0x01C20800 32 0x00000001
# 读 16 位
devmem 0x01C20800 16
# 读 8 位
devmem 0x01C20800 8
注意:devmem 操作的是物理地址,不是虚拟地址。在 Tina Linux 中,外设寄存器地址通常在 /sys/class/sunxi_info/sys_info 或芯片手册里可以查到。
2.3.2 典型调试场景
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 检查 GPIO 电平 | 读 GPIO 数据寄存器 | 0/1 对应高低电平 |
| 验证时钟是否使能 | 读 CCU 寄存器 | 对应 bit 为 1 |
| 确认中断状态 | 读中断状态寄存器 | 挂起位为 1 |
| 配置 DMA 通道 | 写 DMA 配置寄存器 | 启动后状态变化 |
避坑指南:我曾经在调试 I2C 控制器时,用 devmem 写了一个保留位,结果整个 I2C 模块挂死了。所以操作寄存器前,一定要仔细看芯片手册的寄存器描述,确认哪些位是可读写的,哪些是保留的。保留位写 0 是最安全的。
2.3.3 配合驱动调试
很多时候,驱动加载后寄存器值不对,但驱动代码看起来没问题。这时候我会:
- 先读寄存器,确认硬件初始状态
- 加载驱动,再读一次,看驱动是否写入了预期值
- 如果不对,用 devmem 手动写一次,看硬件是否响应
- 对比驱动代码和手动操作,找出差异
这个方法帮我解决过不少"驱动写不进寄存器"的诡异问题。有一次发现是驱动里地址偏移算错了,差了 0x10 字节。
2.4 strace:用户态系统调用追踪器
strace 是用户态调试的神器。它可以追踪一个进程发起的所有系统调用,包括参数、返回值、耗时。你想想看,当你的应用程序卡住、崩溃、或者行为异常时,strace 能告诉你它到底在干什么。
2.4.1 基本用法
# 追踪一个正在运行的进程
strace -p 1234
# 追踪一个新启动的程序
strace ls -l
# 只追踪特定的系统调用
strace -e open,read,write ./myapp
# 显示时间戳
strace -t ./myapp
# 显示相对时间(每个调用的耗时)
strace -r ./myapp
2.4.2 实战案例
我记得有一次,一个客户反馈说 Tina Linux 上的视频播放器启动很慢,要等好几秒。用 strace 一追:
strace -t -T ./video_player 2>&1 | head -50
输出显示,程序在 open 一个配置文件时卡了 2 秒多。仔细一看,那个配置文件路径写错了,程序在遍历整个文件系统查找。改对路径后,启动时间从 3 秒降到了 0.2 秒。
2.4.3 常用过滤选项
| 选项 | 作用 | 示例 |
|---|---|---|
| -e trace=file | 只追踪文件操作 | open, read, write, close |
| -e trace=network | 只追踪网络调用 | socket, connect, send, recv |
| -e trace=process | 只追踪进程管理 | fork, exec, exit |
| -e trace=signal | 只追踪信号 | kill, sigaction |
| -c | 统计汇总 | 显示每个调用的次数和耗时 |
核心思路:strace 的输出可能很长,别从头看到尾。先看错误返回值(负数),再看耗时长的调用。我一般用 strace -T -o trace.log ./app 把结果存文件,然后用 grep 搜 "= -1" 或 "ENOENT" 这类关键词。
2.4.4 与 printk 配合
有时候问题跨越用户态和内核态。比如应用程序调用 read() 返回 -1,但不知道内核驱动里发生了什么。这时候我会:
- 用 strace 确认是哪个系统调用失败,错误码是什么
- 在内核驱动的对应函数里加 printk,打印参数和返回值
- 重新运行,对比用户态和内核态的日志
这种"上下夹击"的方式,能快速定位到底是应用传参错了,还是驱动处理有问题。
性能影响:strace 会大幅降低程序运行速度,因为每个系统调用都要被拦截。生产环境不要用,调试时也要注意。如果追踪时间敏感的程序,结果可能不准确。
2.5 小结
这三个工具各有侧重:printk 和动态调试管内核,devmem 管硬件,strace 管用户态。把它们组合起来用,基本上能覆盖 90% 的日常调试场景。我个人习惯是:先 strace 看用户态行为,再 printk 看内核响应,最后 devmem 确认硬件状态。这套流程帮我解决过不少疑难杂症。