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 时如何根据数据类型选择正确的格式符:
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。建议将此文件加入书签,作为日常调试的速查手册。