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 扩展格式说明符的分类体系,帮助你在调试时快速定位所需的格式。

内核 %p 扩展格式说明符分类体系 %p 扩展格式 网络地址类 资源设备类 时间计时类 其他专用类 %pI4 (IPv4) %pI6 (IPv6) %pM (MAC) %pR (资源) %pV (va_format) %pOF (DT节点) %pt (ktime) %pkt (秒.纳秒) %pTV (timeval) %pK (kptr) %pE (escape) %pU (UUID) 使用建议 • 网络调试:优先使用 %pI4/%pI6 打印 IP 地址,避免手动 ntohl() 转换 • 资源调试:使用 %pR 打印 resource 结构,自动显示类型(mem/io/irq) • 设备树调试:使用 %pOF 打印节点全路径,快速定位 DT 节点 • 安全考虑:使用 %pK 打印内核指针,在非特权上下文中会被隐藏为 0

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 按需开启,避免在生产环境中产生大量日志。