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_spec 和 register_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 格式化输出就是个熟练活。你多写几次,自然就记住了。我个人建议你把这些表格截图存手机里,写代码时瞄一眼。等用熟了,你闭着眼都能写出正确的格式化字符串。
最后提醒一句:我曾经在调试中断上下文时,用了 %s 打印一个可能为 NULL 的字符串指针,结果内核直接 panic。记住,内核里没有 printf 那么宽容,指针解引用前一定要判空。安全第一。