3. printk 格式化输出:常用格式说明符与内核专用格式
printk 的格式化输出是内核调试中最直接的信息呈现手段。与用户空间的 printf 类似,printk 使用格式说明符(format specifier)来指定数据的输出类型和格式。然而,内核提供了大量专用格式说明符,用于处理网络地址、设备树节点、资源大小等内核特有数据类型。掌握这些格式说明符,能让你在调试时一眼看穿数据本质。
调试原则: 优先使用专用格式说明符(如 %pI4、%pV),而非手动拼接十六进制数。专用格式说明符不仅输出更规范,还能自动处理字节序、空指针检查等细节,减少调试信息中的误导。
3.1 基础格式说明符(与用户空间通用)
以下格式说明符与标准 C 库 printf 行为一致,但在内核环境中使用时需注意数据类型长度(如 long 在 64 位系统为 8 字节)。
| 格式说明符 | 数据类型 | 输出示例 | 注意事项 |
|---|---|---|---|
%d / %i |
int | -123 |
有符号十进制 |
%u |
unsigned int | 255 |
无符号十进制 |
%x / %X |
unsigned int | ff / FF |
十六进制,小写/大写 |
%ld / %lx |
long / unsigned long | 123456 / 1e240 |
长度修饰符 l |
%lld / %llx |
long long | 9876543210 |
长度修饰符 ll |
%p |
void * | ffff888012345678 |
指针地址,默认十六进制 |
%s |
char * | hello world |
字符串,空指针输出 (null) |
%c |
char | A |
单个字符 |
// 基础格式示例
int irq = 42;
unsigned long phys_addr = 0x1fe00000;
char *dev_name = "i2c-0";
printk("IRQ number: %d\n", irq);
printk("Physical address: 0x%lx\n", phys_addr);
printk("Device name: %s\n", dev_name);
printk("Pointer value: %p\n", &irq);
避坑提示: 在 64 位内核中,
int 仍为 4 字节,long 和指针为 8 字节。使用 %x 打印指针会截断高 32 位,应始终使用 %p 或 %lx。
3.2 内核专用格式说明符(%p 扩展家族)
内核在 %p 基础上扩展了大量专用格式,通过 %p 后跟特定字符实现。这些扩展由 lib/vsprintf.c 中的 pointer() 函数处理,是内核调试的利器。
3.2.1 网络地址格式(%pI4, %pI6, %pM)
| 格式说明符 | 数据类型 | 输出示例 | 说明 |
|---|---|---|---|
%pI4 |
__be32 * |
192.168.1.1 |
IPv4 地址,点分十进制 |
%pI6 |
struct in6_addr * |
fe80::1 |
IPv6 地址,压缩格式 |
%pI6c |
struct in6_addr * |
fe80::1%eth0 |
IPv6 地址带 scope ID |
%pM |
u8 * |
00:11:22:33:44:55 |
MAC 地址,冒号分隔 |
%pMR |
u8 * |
00-11-22-33-44-55 |
MAC 地址,连字符分隔 |
// 网络地址格式示例
__be32 ipv4 = in_aton("10.0.0.1");
struct in6_addr ipv6 = IN6ADDR_LOOPBACK_INIT;
u8 mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
printk("IPv4: %pI4\n", &ipv4);
printk("IPv6: %pI6\n", &ipv6);
printk("MAC: %pM\n", mac);
字节序注意:
%pI4 期望参数为网络字节序(大端)的 __be32 指针。如果传入主机字节序的 u32,在小端系统上会输出错误的地址。使用 in_aton() 或 htonl() 确保字节序正确。
3.2.2 资源与设备格式(%pr, %pR, %pV)
| 格式说明符 | 数据类型 | 输出示例 | 说明 |
|---|---|---|---|
%pR |
struct resource * |
[mem 0x10000000-0x10000fff] |
资源范围,含类型标志 |
%pr |
struct resource * |
[mem 0x10000000-0x10000fff] |
资源范围,精简格式 |
%pV |
struct va_format * |
自定义格式化输出 |
可变参数格式化,用于延迟打印 |
%pOF |
struct device_node * |
/soc/i2c@12340000 |
设备树节点全路径 |
%pOFn |
struct device_node * |
i2c@12340000 |
设备树节点名称 |
// 资源与设备格式示例
struct resource res = {
.start = 0x10000000,
.end = 0x10000fff,
.flags = IORESOURCE_MEM,
};
struct device_node *np = of_find_node_by_path("/soc/i2c@12340000");
printk("Resource: %pR\n", &res);
printk("DT node: %pOF\n", np);
printk("DT name: %pOFn\n", np);
3.2.3 时间与计时格式(%pt, %pkt)
| 格式说明符 | 数据类型 | 输出示例 | 说明 |
|---|---|---|---|
%pt |
ktime_t * |
1234567890 |
纳秒时间戳 |
%pkt |
ktime_t * |
1.234567890 |
秒.纳秒格式 |
%pTV |
struct timeval * |
12345.678901 |
秒.微秒格式 |
// 时间格式示例
ktime_t now = ktime_get();
struct timeval tv;
do_gettimeofday(&tv);
printk("ktime: %pt\n", &now);
printk("ktime: %pkt\n", &now);
printk("timeval: %pTV\n", &tv);
3.3 内核专用格式说明符全景图
以下 SVG 图展示了内核 %p 扩展格式说明符的分类体系,帮助你在调试时快速定位所需的格式。
3.4 格式说明符组合与高级用法
内核格式说明符支持长度修饰符、标志位和精度控制,组合使用可以精确控制输出格式。
3.4.1 长度修饰符组合
| 组合 | 含义 | 示例 |
|---|---|---|
%ld |
long 十进制 | printk("%ld\n", (long)val); |
%lx |
unsigned long 十六进制 | printk("phys: 0x%lx\n", phys_addr); |
%08x |
宽度 8,前导零填充 | printk("reg: 0x%08x\n", reg_val); |
%-20s |
左对齐,宽度 20 | printk("%-20s %d\n", name, val); |
%*pE |
动态宽度 escape 字符串 | printk("%*pE\n", len, buf); |
3.4.2 %pV 可变参数格式化(延迟打印)
%pV 是内核特有的强大工具,它允许将格式化参数打包到 struct va_format 中,实现延迟打印。这在中断上下文或原子上下文中特别有用,因为你可以先构造格式化参数,稍后在安全上下文中再执行实际的 printk 调用。
// %pV 延迟打印示例
struct va_format vaf;
va_list args;
// 在原子上下文中只构造参数,不打印
vaf.fmt = "IRQ %d on CPU %d, count: %llu";
vaf.va = &args;
// 稍后在进程上下文中打印
printk("IRQ stats: %pV\n", &vaf);
调试技巧: 在中断处理函数中,如果直接调用 printk 可能导致递归死锁,可以使用
%pV 将格式化推迟到 tasklet 或 workqueue 中执行。这是内核中“延迟打印”的标准模式。
3.5 格式说明符选择速查表
以下速查表按调试场景分类,帮助你快速选择正确的格式说明符。
| 调试场景 | 推荐格式 | 示例代码 |
|---|---|---|
| 打印 IP 数据包源地址 | %pI4 |
printk("src: %pI4\n", &iph->saddr); |
| 打印网络设备 MAC | %pM |
printk("MAC: %pM\n", dev->dev_addr); |
| 打印内存资源范围 | %pR |
printk("mem: %pR\n", &res); |
| 打印设备树节点路径 | %pOF |
printk("node: %pOF\n", np); |
| 打印内核时间戳 | %pkt |
printk("time: %pkt\n", &kt); |
| 打印 UUID/GUID | %pU |
printk("uuid: %pU\n", uuid); |
| 打印转义字符串 | %pE |
printk("esc: %pE\n", str); |
| 打印受限内核指针 | %pK |
printk("ptr: %pK\n", ptr); |
安全提醒: 在生产内核中,
%pK 会隐藏内核指针(输出 0),除非进程具有 CAP_SYSLOG 权限。调试内核(CONFIG_DEBUG_KERNEL)则始终显示真实地址。不要在发布版本中遗留 %p 打印敏感地址。
3.6 实战:组合使用格式说明符调试驱动
以下是一个完整的驱动调试示例,展示如何组合使用多种格式说明符输出关键信息。
// 网络驱动调试示例
static void dump_packet_info(struct sk_buff *skb, struct net_device *dev)
{
struct iphdr *iph = ip_hdr(skb);
struct ethhdr *eth = eth_hdr(skb);
ktime_t now = ktime_get();
printk(KERN_DEBUG "=== Packet Debug ===\n");
printk(KERN_DEBUG "Device: %s\n", dev->name);
printk(KERN_DEBUG "MAC src: %pM dst: %pM\n",
eth->h_source, eth->h_dest);
printk(KERN_DEBUG "IP src: %pI4 dst: %pI4\n",
&iph->saddr, &iph->daddr);
printk(KERN_DEBUG "Protocol: %d Length: %u\n",
iph->protocol, ntohs(iph->tot_len));
printk(KERN_DEBUG "Timestamp: %pkt\n", &now);
printk(KERN_DEBUG "SKB addr: %pK\n", skb);
printk(KERN_DEBUG "====================\n");
}
输出示例:
=== Packet Debug ===
Device: eth0
MAC src: 00:11:22:33:44:55 dst: 66:77:88:99:aa:bb
IP src: 192.168.1.100 dst: 10.0.0.1
Protocol: 6 Length: 1500
Timestamp: 12345.678901234
SKB addr: 0000000000000000
====================
调试效率提升: 将上述
dump_packet_info() 函数与动态调试结合,通过 echo 'func dump_packet_info +p' > /sys/kernel/debug/dynamic_debug/control 按需开启,避免在生产环境中产生大量日志。