3、动态调试( dyndbg ):原理、编译配置、运行时控制
动态调试,英文叫 dyndbg,是我在调试驱动时最常用的工具之一。说实话,没有它之前,调试内核驱动就像在黑夜里摸路——你只能靠 printk 到处插桩,然后重新编译、重启、看日志,再改代码……循环往复,效率极低。
而 dyndbg 的出现,彻底改变了这个局面。它允许你在系统运行时,动态地开启或关闭某个模块、某个文件、甚至某一行代码的调试日志。不需要重新编译内核,不需要重启系统。嗯,这才是工程师该有的调试方式。
3.1 原理:它到底是怎么工作的?
先说说原理。你想想看,传统的 printk 是编译时就决定好的——你写了就打印,不写就不打印。而 dyndbg 的思路是:把调试信息做成可开关的。
具体来说,内核在编译时,会在每个 pr_debug()、dev_dbg() 等调试宏调用点,嵌入一个 动态调试描述符。这个描述符记录了:
- 所在的文件名
- 函数名
- 行号
- 调试信息的格式字符串
- 当前的启用/禁用状态
这些描述符被组织成一个全局的链表。运行时,你可以通过 debugfs 文件系统(挂载在 /sys/kernel/debug/dynamic_debug/control)来修改这些描述符的状态。说白了,就是改一个文件里的几个字节,就能控制成千上万个调试点的开关。
核心思想:编译时埋点,运行时控制。把调试开关从编译期延迟到运行期。
我在项目中遇到过一个问题:一个 USB 驱动在特定硬件上偶尔会挂死,但 printk 加多了会影响时序,加少了又抓不到关键信息。用 dyndbg 就完美解决了——平时全部关闭,复现问题时只打开那几行关键代码的调试日志。
3.2 编译配置:怎么把它编进内核?
要让 dyndbg 工作,内核编译时得打开几个配置项。我建议你直接检查你的内核配置:
# 检查是否已启用
grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)
grep CONFIG_DEBUG_FS /boot/config-$(uname -r)
需要的配置项如下:
| 配置项 | 说明 | 建议值 |
|---|---|---|
CONFIG_DYNAMIC_DEBUG |
动态调试核心功能 | y |
CONFIG_DEBUG_FS |
debugfs 文件系统,用于运行时控制接口 | y |
CONFIG_DYNAMIC_DEBUG_CORE |
动态调试核心支持(较新内核才需要) | y |
如果你是自己编译内核,在 make menuconfig 里这样找:
Kernel hacking --->
[*] Debug filesystem
[*] Dynamic debugging
[*] Dynamic debug core support (可选,看内核版本)
小提示:我习惯在开发板的内核里直接把这些选项都打开。反正调试版内核,多点功能没坏处。但产品发布时,记得关掉 CONFIG_DEBUG_FS,避免暴露调试接口。
另外要注意,你的驱动代码里必须使用 pr_debug()、dev_dbg() 这类宏,而不是 printk(KERN_DEBUG ...)。因为只有这些宏才会被 dyndbg 框架识别。我曾经接手过一个项目,里面全是 printk 写的调试信息,想用 dyndbg 控制?没门。只能一个个改成 pr_debug。
3.3 运行时控制:怎么用?
编译好了,系统跑起来了。现在来看看怎么在运行时控制调试日志。
首先,挂载 debugfs:
mount -t debugfs none /sys/kernel/debug
然后,控制接口就在这个文件里:
/sys/kernel/debug/dynamic_debug/control
你可以 cat 它看看当前所有调试点的状态:
cat /sys/kernel/debug/dynamic_debug/control | head -20
输出大概长这样:
drivers/usb/core/hub.c:1023 [hub]usb_hub_init =_ "hub: init hub\012"
drivers/usb/core/hub.c:1045 [hub]usb_hub_init =_ "hub: init hub done\012"
drivers/net/eth.c:256 [eth]eth_open =_ "eth: open device\012"
每一行就是一个调试点。格式是:文件名:行号 [模块名]函数名 =标志位 "格式字符串"。标志位 =_ 表示禁用,=p 表示启用。
现在,重点来了——怎么控制它?
3.3.1 按模块控制
打开整个 USB hub 模块的调试日志:
echo 'module hub +p' > /sys/kernel/debug/dynamic_debug/control
3.3.2 按文件控制
只打开某个文件的调试:
echo 'file drivers/usb/core/hub.c +p' > /sys/kernel/debug/dynamic_debug/control
3.3.3 按函数控制
只打开某个函数的调试:
echo 'func usb_hub_init +p' > /sys/kernel/debug/dynamic_debug/control
3.3.4 按行号控制
精确到某一行:
echo 'file hub.c line 1023 +p' > /sys/kernel/debug/dynamic_debug/control
3.3.5 组合条件
还可以组合使用:
echo 'file hub.c line 1000-2000 +p' > /sys/kernel/debug/dynamic_debug/control
关闭调试就是把 +p 换成 -p:
echo 'module hub -p' > /sys/kernel/debug/dynamic_debug/control
注意:写控制文件时,一次只能写一条规则。多条规则要分多次 echo。我曾经写了个脚本批量操作,结果格式写错了,整个控制文件被清空——所有调试点都关了。排查了半天才发现。
3.4 实战技巧:我的一些经验
最后分享几个我常用的技巧:
- 先查再开:在打开调试之前,先用
grep确认你的调试点确实存在。比如grep 'my_driver' /sys/kernel/debug/dynamic_debug/control。如果找不到,说明你的代码没用pr_debug,或者模块没加载。 - 配合 dmesg 级别:
pr_debug默认输出到dmesg,但可能被日志级别过滤掉。我习惯先echo 8 > /proc/sys/kernel/printk,确保所有级别的日志都能看到。 - 写个脚本:对于经常调试的模块,我写了个小脚本,一键打开/关闭特定调试点。比如:
#!/bin/bash
# 打开我的驱动调试
echo 'module my_driver +p' > /sys/kernel/debug/dynamic_debug/control
# 但关掉过于频繁的日志
echo 'file my_driver.c line 500-600 -p' > /sys/kernel/debug/dynamic_debug/control
dmesg -C # 清空旧日志
tail -f /var/log/kern.log
嗯,差不多就这些。动态调试这个东西,说白了就是给你一把手术刀,让你能精准地切开驱动运行的每一个瞬间。用好了,调试效率能提升一个数量级。我建议你拿到一块新开发板时,第一件事就是确认 dyndbg 能不能用——这能省下你后面无数次的编译重启时间。