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 实战技巧:我的一些经验

最后分享几个我常用的技巧:

  1. 先查再开:在打开调试之前,先用 grep 确认你的调试点确实存在。比如 grep 'my_driver' /sys/kernel/debug/dynamic_debug/control。如果找不到,说明你的代码没用 pr_debug,或者模块没加载。
  2. 配合 dmesg 级别pr_debug 默认输出到 dmesg,但可能被日志级别过滤掉。我习惯先 echo 8 > /proc/sys/kernel/printk,确保所有级别的日志都能看到。
  3. 写个脚本:对于经常调试的模块,我写了个小脚本,一键打开/关闭特定调试点。比如:
#!/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 能不能用——这能省下你后面无数次的编译重启时间。