4. printk 格式说明符:从基础到内核专用扩展

printk 的格式化语法与标准 C 库 printf 高度兼容,但内核环境增加了大量专用扩展,用于安全、网络地址、设备标识等场景。掌握这些格式符是准确解读内核日志的前提。

4.1 基础整数与指针格式符

下表列出最常用的 printk 格式符及其典型输出场景:

格式符 类型 示例输出 典型用途
%d 有符号十进制 int -123 错误码、计数器、差值
%u 无符号十进制 int 255 索引、标志位、长度
%x / %X 无符号十六进制 int ff / FF 寄存器值、状态掩码
%ld / %lu long 有/无符号 4294967295 大范围计数、时间戳
%lld / %llu long long 有/无符号 18446744073709551615 64 位硬件寄存器、纳秒时间
%p 指针(十六进制地址) ffff888012345678 内存地址、结构体指针
%s 以 \0 结尾的字符串 hello 设备名、函数名、消息文本
%zu size_t 无符号 1024 sizeof()、内存分配大小
避坑提示: 在 64 位内核中,int 仍是 32 位,long 是 64 位。用 %d 打印 size_t 会导致截断,必须使用 %zu。

4.2 内核专用 %p 扩展家族

Linux 内核为 %p 定义了丰富的子格式符,用于安全隐藏、网络地址、设备标识等场景。这些扩展由 lib/vsprintf.c 中的 pointer() 函数解析。

4.2.1 安全与访问控制:%pK

%pK 是内核安全审计的核心格式符。它根据 /proc/sys/kernel/kptr_restrict 的值决定是否暴露真实地址:

kptr_restrict 值 行为 典型输出
0 所有进程均可读取真实地址 ffff888012345678
1 仅特权进程(CAP_SYSLOG)可读 非特权:0000000000000000
2 所有进程均被隐藏 0000000000000000
安全实践: 在驱动调试中,永远不要直接使用 %p 打印内核指针。使用 %pK 确保生产环境下的地址隐藏。调试阶段可将 kptr_restrict 临时设为 0,但上线前必须恢复。

4.2.2 网络地址格式化:%pI4 / %pI6

网络协议栈中频繁需要打印 IP 地址,%pI4 和 %pI6 提供了标准格式化:

// IPv4 地址
__be32 addr = in_aton("192.168.1.1");
printk(KERN_INFO "IPv4: %pI4\n", &addr);
// 输出: IPv4: 192.168.1.1

// IPv6 地址
struct in6_addr addr6 = IN6ADDR_LOOPBACK_INIT;
printk(KERN_INFO "IPv6: %pI6\n", &addr6);
// 输出: IPv6: ::1

// 带端口号的 IPv4
printk(KERN_INFO "Endpoint: %pI4:%d\n", &addr, 8080);
// 输出: Endpoint: 192.168.1.1:8080

4.2.3 设备标识:%pdev / %pOF

设备驱动开发中,%pdev 和 %pOF 能直接打印设备名称和设备树节点路径:

struct platform_device *pdev = ...;
struct device_node *np = pdev->dev.of_node;

printk(KERN_INFO "Device: %pdev\n", pdev);
// 输出: Device: my-i2c-device.0

printk(KERN_INFO "DT node: %pOF\n", np);
// 输出: DT node: /soc/i2c@7e804000/my-i2c-device

4.2.4 其他常用 %p 扩展

格式符 用途 示例输出
%pM MAC 地址(6 字节) 00:11:22:33:44:55
%pMF MAC 地址(无冒号) 001122334455
%pE 转义字符串(不可见字符转义) hello\tworld
%pV 递归格式化 va_format 嵌套 printk 格式
%pS 打印符号名 + 偏移(内核符号表) do_fork+0x10/0x200
%pB 打印回溯符号(栈回溯) dump_stack+0x24/0x30
调试利器: %pS 和 %pB 在 oops 分析和函数调用跟踪中极其有用。它们自动解析内核符号表,将地址转换为可读的函数名和偏移量。

4.3 格式符选择流程

下面的 SVG 流程图展示了在编写 printk 时如何根据数据类型选择正确的格式符:

printk 格式符选择决策流程 数据类型是什么? 整数类型 指针/地址类型 有符号 → %d / %ld / %lld 无符号 → %u / %lu / %llu 十六进制 → %x / %X size_t → %zu 普通地址 → %pK(安全优先) IPv4 地址 → %pI4 IPv6 地址 → %pI6 MAC 地址 → %pM 符号/回溯 → %pS / %pB 设备/DT 节点 → %pdev / %pOF

4.4 实战案例:驱动调试中的格式符组合

以下是一个 I2C 驱动调试片段的真实示例,展示了多种格式符的混合使用:

#include <linux/i2c.h>
#include <linux/device.h>

static int my_i2c_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct device *dev = &client->dev;
    u8 reg_val;
    int ret;
    size_t buf_size = 256;

    // 1. 设备标识:使用 %pdev 和 %pOF
    dev_info(dev, "Probing device: %pdev\n", client);
    dev_info(dev, "DT node: %pOF\n", dev->of_node);

    // 2. 地址与安全:使用 %pK 隐藏内核指针
    dev_dbg(dev, "client struct at %pK\n", client);

    // 3. 整数与十六进制:寄存器值
    ret = i2c_smbus_read_byte_data(client, 0x10);
    if (ret < 0) {
        dev_err(dev, "read reg 0x10 failed: %d\n", ret);
        return ret;
    }
    reg_val = (u8)ret;
    dev_info(dev, "REG[0x10] = 0x%02x (%u)\n", reg_val, reg_val);

    // 4. 大小与计数:使用 %zu
    dev_info(dev, "Allocated buffer: %zu bytes\n", buf_size);

    // 5. 网络地址(如果设备涉及网络)
    // __be32 ip = in_aton("10.0.0.1");
    // dev_info(dev, "Server IP: %pI4\n", &ip);

    return 0;
}

4.5 格式符常见错误与排查

  • 类型不匹配: 用 %d 打印 size_t 导致高 32 位截断。修复:使用 %zu。
  • 指针解引用: %p 期望指针值,传入 %p 时误传 *ptr 会导致崩溃。修复:传入地址 &var 或 ptr。
  • 网络地址字节序: %pI4 期望网络字节序(big-endian)的 __be32,传入主机字节序会打印乱序地址。修复:使用 htonl() 转换。
  • 安全泄露: 生产环境使用 %p 暴露内核地址。修复:全局替换为 %pK。
内核文档参考: 完整的格式符列表和规范定义在 Documentation/core-api/printk-formats.rst。建议将此文件加入书签,作为日常调试的速查手册。

4.6 本章小结