3. printk进阶

在 Linux 内核调试中,printk 是最基础也是最常用的调试手段。然而,简单的 printk("%s %d\n", str, val) 往往无法满足复杂数据结构的输出需求。本章将深入讲解 printk 的扩展格式化占位符,以及内核提供的强大动态调试(Dynamic Debug)机制,帮助你从“盲目打印”进阶到“精准控制”。

3.1 扩展格式化占位符:%p 家族

内核的 printk 实现了一套以 %p 开头的扩展格式化说明符,用于直接输出指针、网络地址、MAC 地址、UUID 等复杂数据,无需手动转换。这大大简化了调试代码,并提高了可读性。

3.1.1 常用 %p 扩展格式速查表

格式符 用途 输出示例
%p 普通指针(十六进制) 0xffff888123456789
%pK 内核指针(安全限制,非特权进程显示为 0) 0x0000000000000000 或真实地址
%px 始终显示完整指针(需谨慎使用) 0xffff888123456789
%pI4 IPv4 地址(__be32 类型) 192.168.1.1
%pI6 IPv6 地址(struct in6_addr *) 2001:db8::1
%pM MAC 地址(6 字节) 00:11:22:33:44:55
%pMF MAC 地址(FDDI 风格) 00-11-22-33-44-55
%pUb UUID/GUID(大端) 00000000-0000-0000-0000-000000000000
%pU UUID/GUID(小端) 00000000-0000-0000-0000-000000000000
%pE 转义字符串(处理不可打印字符) Hello\x0aWorld
%pD 打印 struct dentry * 名称 /path/to/file
最佳实践: 在驱动调试中,%pM 用于打印网卡 MAC 地址,%pI4 用于打印 IP 地址,%pD 用于打印文件路径。这些格式符能让你用一行代码替代之前需要 3-5 行手动转换的代码。

3.1.2 实战代码示例:网络驱动调试

假设你在调试一个网络驱动,需要打印接收到的数据包的源 MAC 和源 IP:

#include <linux/printk.h>
#include <linux/etherdevice.h>
#include <linux/inetdevice.h>

void dump_packet_info(struct sk_buff *skb) {
    struct ethhdr *eth = (struct ethhdr *)skb_mac_header(skb);
    struct iphdr *iph = (struct iphdr *)skb_network_header(skb);

    // 使用 %pM 打印 MAC 地址
    printk(KERN_INFO "my_driver: src MAC = %pM\n", eth->h_source);
    // 使用 %pI4 打印 IPv4 地址
    printk(KERN_INFO "my_driver: src IP  = %pI4\n", &iph->saddr);
    // 使用 %pI6 打印 IPv6 地址(如果存在)
    if (skb->protocol == htons(ETH_P_IPV6)) {
        struct ipv6hdr *ip6h = (struct ipv6hdr *)skb_network_header(skb);
        printk(KERN_INFO "my_driver: src IPv6 = %pI6\n", &ip6h->saddr);
    }
}

3.2 动态调试(Dynamic Debug)原理与配置

动态调试是内核提供的一种高级调试机制,允许你在不重新编译内核或模块的情况下,动态地启用或禁用特定的 pr_debug()、dev_dbg() 等调试打印语句。它基于 debugfs 文件系统,通过写入控制文件来精细控制打印行为。

3.2.1 核心原理

动态调试的核心思想是在编译时,将 pr_debug() 等宏调用中的调试信息(如文件名、函数名、行号、格式字符串)作为元数据嵌入到内核或模块的 .dyndbg 节中。运行时,内核通过 debugfs 暴露一个控制接口,用户可以通过写入匹配规则来动态修改这些调试点的状态。

关键点: 动态调试依赖于 CONFIG_DYNAMIC_DEBUG 内核配置选项。如果未开启,所有 pr_debug() 调用在编译时会被完全移除,不会产生任何运行时开销。
动态调试(Dynamic Debug)原理架构图 用户空间 echo 'file xxx.c +p' > control debugfs 控制接口 /sys/kernel/debug/dynamic_debug/control 内核空间 .dyndbg 节(元数据) 动态调试引擎 匹配 & 开关控制 驱动代码 pr_debug("...") 控制流说明 1. 用户写入匹配规则 2. debugfs 解析规则 3. 引擎遍历 .dyndbg 节 4. 匹配成功则修改标志位 5. pr_debug 根据标志位 决定是否输出

3.2.2 配置与使用步骤

  1. 确保内核配置开启:CONFIG_DYNAMIC_DEBUG=y 且 CONFIG_DEBUG_FS=y。
  2. 挂载 debugfs(如果未自动挂载):mount -t debugfs none /sys/kernel/debug。
  3. 查看控制文件:cat /sys/kernel/debug/dynamic_debug/control 会列出所有可控制的调试点。
  4. 写入匹配规则:通过 echo 命令向控制文件写入规则来启用或禁用调试打印。

3.2.3 匹配规则语法

匹配规则的基本格式为:match_spec flags。其中 match_spec 用于选择调试点,flags 用于设置操作。

匹配关键字 含义 示例
func 匹配函数名 func my_function +p
file 匹配源文件名(支持通配符 *) file drivers/net/ethernet/intel/* +p
module 匹配模块名 module my_driver +p
line 匹配行号或行号范围 line 123 +p 或 line 100-200 +p
format 匹配格式字符串中的子串 format "error" +p
标志位 含义
+p 启用打印(使 pr_debug() 生效)
-p 禁用打印
+f 在输出中添加函数名
+l 在输出中添加行号
+m 在输出中添加模块名
+t 在输出中添加线程 ID

3.2.4 实战示例:调试网卡驱动

假设你正在调试 igb 网卡驱动,只想看到 igb_main.c 文件中 igb_xmit_frame 函数的调试信息:

# 启用特定函数和文件的调试打印,并显示函数名和行号
echo 'func igb_xmit_frame file igb_main.c +pfl' > /sys/kernel/debug/dynamic_debug/control

# 查看是否生效
cat /sys/kernel/debug/dynamic_debug/control | grep igb_xmit_frame

# 输出示例(在 dmesg 中):
# igb: igb_xmit_frame:123: my_debug_message
注意事项:
  • 动态调试规则在系统重启后会丢失。如需持久化,可将规则写入启动脚本(如 /etc/rc.local)。
  • 对于模块,需要在模块加载后(而非加载前)写入规则。可以在 modprobe 之后执行 echo 命令。
  • 过度启用动态调试会显著增加内核日志输出,影响系统性能。建议只针对特定调试点启用。

3.3 本章小结

本章介绍了 printk 的 %p 扩展格式化家族,它们能极大简化网络地址、MAC 地址、UUID 等复杂数据的输出。同时,深入讲解了动态调试的原理和配置方法,它允许你在运行时精确控制调试信息的输出,是内核驱动调试中不可或缺的利器。掌握这些技巧,你将能从“盲目打印”升级到“精准控制”,大幅提升调试效率。