4. printk格式化输出:常用格式化说明符、打印指针和结构体、自定义格式化

printk 的格式化输出,说白了就是内核版的 printf。用法几乎一样,但坑更多。我刚开始写内核驱动时,觉得这玩意儿太简单了,结果一个指针打印错了,排查了整整一个下午。嗯,从那以后我再也不敢小看它了。

4.1 常用格式化说明符

先看最基础的部分。这些说明符你每天都会用到:

说明符 含义 示例
%d 或 %i 有符号十进制整数 printk(KERN_INFO "count = %d\n", count);
%u 无符号十进制整数 printk(KERN_DEBUG "size = %u\n", size);
%x 无符号十六进制(小写) printk("reg_val = 0x%x\n", reg);
%X 无符号十六进制(大写) printk("MASK = 0x%X\n", mask);
%ld / %lx long 类型 printk("offset = %ld\n", off);
%lld / %llx long long 类型 printk("ts = %lld\n", ts);
%s 字符串 printk("name = %s\n", dev_name);
%c 单个字符 printk("tag = %c\n", tag);
我的习惯:调试寄存器时一律用 %#x,这样会自动加 0x 前缀,省得自己拼字符串。比如 printk("reg = %#x\n", val) 输出 reg = 0xff,比 %x 更直观。

4.2 打印指针:%p 家族的玄机

打印指针是内核调试的重头戏。普通 printf 用 %p 就完事了,但内核里 %p 被玩出了花。为什么?因为安全考虑——直接打印真实地址可能泄露内核布局信息。

我个人建议,调试阶段用 %px,它能打印完整的真实地址。正式代码里用 %p,它会被哈希处理:

// 调试阶段:看真实地址
printk("buf = %px\n", buf);   // 输出: buf = 00000000f1234567

// 正式代码:哈希地址
printk("buf = %p\n", buf);    // 输出: buf = 000000001234abcd

除了基础用法,%p 还有很多扩展格式。我整理了一张表,你直接收藏就行:

格式 作用 示例输出
%p 哈希后的指针 000000001234abcd
%px 真实指针(需开启 debug) ffff888012345678
%pK 内核符号指针 0xffffffff81234567
%pS 打印符号名+偏移 do_fork+0x12/0x200
%pF 打印函数名 do_fork
%pI4 IPv4 地址 192.168.1.1
%pI6 IPv6 地址 2001:db8::1
%pM MAC 地址 00:11:22:33:44:55
%pD dentry 路径 /home/user/file.txt
避坑指南:我曾经在调试网络驱动时,用 %p 打印 skb 的指针,结果哈希值每次都不一样,根本没法跟踪。后来换成 %px 才看清真实地址。记住:调试阶段大胆用 %px,提交代码前再改回 %p。

4.3 打印结构体:从手动到自动

打印结构体是内核调试的日常。最笨的办法是逐个字段打印:

struct task_struct *p = current;
printk("pid = %d, comm = %s, state = %ld\n",
       p->pid, p->comm, p->state);

这样写多了你就烦了。内核其实提供了不少辅助宏,比如:

// 打印 task_struct 的关键信息
printk("task: %p, pid: %d, comm: %s\n",
       p, task_pid_nr(p), p->comm);

// 打印时间结构体
struct timespec64 ts;
printk("time: %lld.%09ld\n", ts.tv_sec, ts.tv_nsec);

对于自定义结构体,我建议你写个专门的 dump 函数。比如:

struct my_device {
    int id;
    char name[32];
    unsigned long base;
    int irq;
};

static void dump_my_device(struct my_device *dev)
{
    printk("my_device@%px:\n", dev);
    printk("  id   = %d\n", dev->id);
    printk("  name = %s\n", dev->name);
    printk("  base = 0x%lx\n", dev->base);
    printk("  irq  = %d\n", dev->irq);
}
我的经验:结构体 dump 函数里,我习惯用 %px 打印结构体自身地址,这样在日志里能快速定位是哪个实例。配合 %pS 打印调用者函数名,排查问题快得多。

4.4 自定义格式化:扩展 printk 的能力

内核允许你注册自定义的 %p 扩展。说白了,就是告诉 printk:遇到 %p 后面跟某个字符时,调用你的处理函数。

实现方式很简单,用 struct printf_specregister_printf_specifier

// 自定义 %pM 的处理器(其实内核已经内置了)
// 这里演示如何注册一个 %pX 来打印自定义结构

#include <linux/printk.h>

static int my_pointer_format(struct printf_spec *spec,
                             const char *fmt, va_list args)
{
    struct my_device *dev = va_arg(args, struct my_device *);
    return scnprintf(buf, size, "dev(id=%d, name=%s)",
                     dev->id, dev->name);
}

// 在模块初始化时注册
static int __init my_init(void)
{
    register_printf_specifier("X", my_pointer_format, NULL);
    return 0;
}

注册之后,你就可以这样用了:

struct my_device dev = { .id = 42, .name = "eth0" };
printk("device info: %pX\n", &dev);
// 输出: device info: dev(id=42, name=eth0)
注意:自定义格式化在内核主线版本中支持程度不同。老内核可能没有这个接口。我建议你先查一下当前内核的 lib/vsprintf.c,看看有哪些扩展可用。别自己造轮子,内核已经提供了很多现成的。

4.5 知识体系总览

下面这张图总结了 printk 格式化输出的核心脉络。你把它存下来,写代码时对照着看:

printk 格式化输出知识体系 printk 格式化输出 基础说明符 %d %u %x %ld %llx %s %c %p 整数 / 字符串 / 指针 %p 扩展家族 %px 真实地址 / %pK 内核符号 %pS 符号名+偏移 / %pF 函数名 %pI4 IPv4 / %pI6 IPv6 / %pM MAC 调试用 %px,正式用 %p 结构体 & 自定义 手动逐字段打印 编写 dump 函数 register_printf_specifier 自定义 %pX 扩展 核心原则:调试用 %px,提交用 %p 掌握这些,内核日志对你来说就是透明的

说到底,printk 格式化输出就是个熟练活。你多写几次,自然就记住了。我个人建议你把这些表格截图存手机里,写代码时瞄一眼。等用熟了,你闭着眼都能写出正确的格式化字符串。

最后提醒一句:我曾经在调试中断上下文时,用了 %s 打印一个可能为 NULL 的字符串指针,结果内核直接 panic。记住,内核里没有 printf 那么宽容,指针解引用前一定要判空。安全第一。