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() 调用在编译时会被完全移除,不会产生任何运行时开销。
3.2.2 配置与使用步骤
- 确保内核配置开启:
CONFIG_DYNAMIC_DEBUG=y且CONFIG_DEBUG_FS=y。 - 挂载 debugfs(如果未自动挂载):
mount -t debugfs none /sys/kernel/debug。 - 查看控制文件:
cat /sys/kernel/debug/dynamic_debug/control会列出所有可控制的调试点。 - 写入匹配规则:通过
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 等复杂数据的输出。同时,深入讲解了动态调试的原理和配置方法,它允许你在运行时精确控制调试信息的输出,是内核驱动调试中不可或缺的利器。掌握这些技巧,你将能从“盲目打印”升级到“精准控制”,大幅提升调试效率。