4. printk 格式说明符:%d、%x、%p、%s、%zu 等常用格式、%p 扩展(%pK、%pI4、%pM)、自定义格式

在嵌入式 Linux 驱动调试中,printk 是最直接、最常用的日志输出手段。其格式化字符串的掌握程度,直接决定了调试信息的可读性与分析效率。本章将系统梳理 printk 的常用格式说明符、内核特有的 %p 扩展,以及如何通过自定义格式实现更高效的日志输出。

4.1 基础格式说明符速查

下表列出了驱动调试中最常用的格式说明符及其典型应用场景:

格式符 数据类型 典型用途 示例输出
%d / %i int 打印有符号整数(状态码、计数器) ret = -22
%u unsigned int 打印无符号整数(长度、索引) len = 1024
%x / %X unsigned int 打印十六进制值(寄存器、标志位) reg = 0xff00
%p void * 打印指针地址 ptr = ffff800012345678
%s char * 打印字符串(设备名、错误描述) name = i2c-0
%zu size_t 打印 sizeof 返回值、内存大小 size = 64
%ld / %lu long / unsigned long 打印长整型(物理地址、时间戳) phys = 0x10000000
%lld / %llu long long 打印 64 位整数(大计数器、纳秒值) ns = 1234567890123
调试建议:在打印指针时,优先使用 %p 而非 %x 或 %lx。%p 会根据架构自动适配地址宽度(32位输出8位十六进制,64位输出16位),且内核对其有特殊的哈希处理,避免泄露内核地址。

4.2 内核专用 %p 扩展:安全与可读性并重

Linux 内核在标准 C 的 %p 基础上,扩展了一套功能强大的子格式说明符。这些扩展通过 %p[扩展符] 的语法调用,极大简化了常见数据结构的打印逻辑。

4.2.1 安全控制:%pK

%pK 用于控制内核指针的可见性。当 /proc/sys/kernel/kptr_restrict 设置为 1 或 2 时,非特权用户看到的指针会被替换为全零,有效防止内核地址泄露。

// 安全打印指针,非 root 用户看到 0000000000000000
printk(KERN_INFO "Device at %pK\n", dev_ptr);

4.2.2 网络地址:%pI4 与 %pI6

打印 IP 地址时,无需手动调用 sprintf 拼接点分十进制字符串。%pI4 直接接受 __be32 指针,自动完成网络字节序到点分十进制的转换。

__be32 ip = cpu_to_be32(0xC0A80001); // 192.168.0.1
printk(KERN_INFO "Server IP: %pI4\n", &ip);
// 输出: Server IP: 192.168.0.1

4.2.3 MAC 地址:%pM

以太网 MAC 地址的打印变得极其简洁,无需逐字节格式化。

u8 mac[6] = {0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E};
printk(KERN_INFO "MAC: %pM\n", mac);
// 输出: MAC: 00:1a:2b:3c:4d:5e
扩展对比表:以下表格汇总了最常用的 %p 扩展及其输出格式:
格式符 输入类型 输出示例 典型场景
%pK void * ffff800012345678 或 0000000000000000 安全日志、用户态可见的调试信息
%pI4 __be32 * 192.168.1.100 网络驱动、IP 地址记录
%pI6 struct in6_addr * fe80::1 IPv6 网络调试
%pM u8 * (6字节) 00:1a:2b:3c:4d:5e 以太网驱动、MAC 地址显示
%pMF u8 * (6字节) 001a.2b3c.4d5e Cisco 格式 MAC 地址
%p4 u8 * (4字节) 0a 0b 0c 0d 原始 4 字节十六进制转储
%pUn struct dentry * /sys/class/leds VFS 路径名打印

4.3 自定义格式:使用 printk 的 %p 扩展机制

内核允许驱动开发者注册自己的 %p 扩展,这对于打印自定义结构体(如设备私有数据、硬件寄存器集合)极为有用。核心机制是通过 struct printf_spec 和 set_printf_spec 注册格式化回调函数。

4.3.1 实现步骤

  1. 定义格式化回调函数:函数原型为 int my_formatter(struct printf_spec *spec, const char *fmt, ...)。
  2. 注册扩展:在模块初始化时调用 set_printf_spec('X', my_formatter),其中 'X' 是自定义扩展字符。
  3. 使用:在 printk 中通过 %pX 调用自定义格式化逻辑。
// 示例:自定义 %pD 打印 struct my_device 的关键字段
static int my_dev_printf(struct printf_spec *spec, const char *fmt, ...)
{
    struct my_device *dev;
    va_list args;

    va_start(args, fmt);
    dev = va_arg(args, struct my_device *);
    va_end(args);

    return seq_printf(spec->seq, "{irq=%d, base=0x%lx, name=%s}",
                      dev->irq, dev->base, dev->name);
}

// 模块初始化时注册
static int __init my_driver_init(void)
{
    set_printf_spec('D', my_dev_printf);
    // ...
}

// 使用自定义格式
struct my_device *dev = get_device();
printk(KERN_INFO "Device info: %pD\n", dev);
// 输出: Device info: {irq=42, base=0x10002000, name=my_i2c}
注意事项:自定义 %p 扩展是全局性的,注册后会影响整个内核的 printk 行为。务必在模块卸载时调用 unset_printf_spec('D') 清理,避免内核崩溃或格式化混乱。

4.4 格式说明符选择流程图

下面的 SVG 流程图帮助你在调试时快速选择合适的格式说明符:

printk 格式说明符选择流程 printk 格式说明符选择流程 需要打印什么? 数值 地址/结构 整数 / 十六进制 有符号? → %d / %i 无符号? → %u / %x 指针 / 结构体 普通指针 → %p 安全控制 → %pK IP/MAC → %pI4 / %pM 自定义结构 → %pX 提示:%zu 专用于 size_t,%ld/%lu 用于 long 类型,避免类型不匹配

4.5 常见陷阱与最佳实践

  • 类型不匹配:使用 %d 打印 size_t 会导致 64 位系统上截断,应使用 %zu。
  • 指针哈希:普通 %p 在内核 5.0+ 默认进行地址哈希,如需原始地址请使用 %px(仅限调试内核)。
  • 网络字节序:%pI4 期望输入为网络字节序(大端),传入主机字节序会导致 IP 地址反转。
  • 缓冲区溢出:%s 打印字符串时,确保指针非空且以 \0 结尾,否则可能引发崩溃。
调试口诀:整数用 %d/%u,指针用 %p,大小用 %zu,网络地址用 %pI4,安全打印用 %pK,自定义结构用 %pX。