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 实现步骤
- 定义格式化回调函数:函数原型为
int my_formatter(struct printf_spec *spec, const char *fmt, ...)。 - 注册扩展:在模块初始化时调用
set_printf_spec('X', my_formatter),其中'X'是自定义扩展字符。 - 使用:在
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 流程图帮助你在调试时快速选择合适的格式说明符:
4.5 常见陷阱与最佳实践
- 类型不匹配:使用
%d打印size_t会导致 64 位系统上截断,应使用%zu。 - 指针哈希:普通
%p在内核 5.0+ 默认进行地址哈希,如需原始地址请使用%px(仅限调试内核)。 - 网络字节序:
%pI4期望输入为网络字节序(大端),传入主机字节序会导致 IP 地址反转。 - 缓冲区溢出:
%s打印字符串时,确保指针非空且以\0结尾,否则可能引发崩溃。
调试口诀:整数用
%d/%u,指针用 %p,大小用 %zu,网络地址用 %pI4,安全打印用 %pK,自定义结构用 %pX。