网络地址快速打印:printk扩展格式说明符
内核中最便捷的网络地址打印方式------printk扩展格式说明符 。这些是内核printk函数专门为网络地址提供的格式化输出功能。
一、IPv4地址打印
1.1 基本格式
c
#include <linux/printk.h>
__be32 ipaddr = htonl(0xC0A80101); // 192.168.1.1
// %pI4 - 打印点分十进制,不带前导零(标准格式)
printk(KERN_INFO "IP: %pI4\n", &ipaddr);
// 输出: IP: 192.168.1.1
// %pi4 - 打印点分十进制,带前导零(固定宽度)
printk(KERN_INFO "IP: %pi4\n", &ipaddr);
// 输出: IP: 192.168.001.001
1.2 字节序说明
c
struct iphdr *iph = ip_hdr(skb);
// 注意:必须传递指针!不能直接传值
printk(KERN_DEBUG "src IP: %pI4, dst IP: %pI4\n",
&iph->saddr, &iph->daddr);
// iph->saddr 已经是网络字节序(__be32),无需转换
二、IPv6地址打印
2.1 基本格式
c
#include <net/ipv6.h>
struct in6_addr ipv6_addr;
// 假设已填充地址数据
// %pI6 - 完整格式,冒号分隔,16位字使用网络字节序
printk(KERN_INFO "IPv6: %pI6\n", &ipv6_addr);
// 输出: 2001:0db8:0000:0000:0000:ff00:0042:8329
// %pi6 - 紧凑格式,不带冒号
printk(KERN_INFO "IPv6: %pi6\n", &ipv6_addr);
// 输出: 20010db8000000000000ff0000428329
// %pI6c - 压缩格式(RFC 5952标准)★ 最常用
printk(KERN_INFO "IPv6: %pI6c\n", &ipv6_addr);
// 输出: 2001:db8::ff00:42:8329
2.2 IPv6格式对比
| 格式符 | 输出示例 | 说明 |
|---|---|---|
%pI6 |
2001:0db8:0000:0000:0000:ff00:0042:8329 |
完整格式,前导零保留 |
%pi6 |
20010db8000000000000ff0000428329 |
无冒号,完整32位十六进制 |
%pI6c |
2001:db8::ff00:42:8329 |
压缩格式,符合RFC 5952 |
三、MAC地址打印
c
#include <linux/netdevice.h>
// MAC地址通常以 u8[6] 或 unsigned char[6] 存储
u8 mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
// %pM - 标准格式,冒号分隔
printk(KERN_INFO "MAC: %pM\n", mac);
// 输出: MAC: 00:11:22:33:44:55
// %pMR - 反转顺序(某些协议需要)
printk(KERN_INFO "MAC (reversed): %pMR\n", mac);
// 输出: MAC (reversed): 55:44:33:22:11:00
// %pMF - 不带冒号的格式
printk(KERN_INFO "MAC: %pMF\n", mac);
// 输出: MAC: 00-11-22-33-44-55
四、sockaddr结构体打印(最强大)
4.1 基本用法
c
#include <linux/socket.h>
#include <net/sock.h>
struct sockaddr_in *sin;
struct sockaddr_in6 *sin6;
struct sockaddr *sa;
// %pIS - 打印sockaddr中的IP地址和端口
printk(KERN_INFO "Address: %pIS\n", sa);
// IPv4输出: 192.168.1.1:8080
// IPv6输出: [2001:db8::1]:8080
// %pISc - 压缩IPv6格式的sockaddr
printk(KERN_INFO "Address: %pISc\n", sa);
// IPv6输出: [2001:db8::1]:8080(已压缩)
4.2 完整格式选项
c
// %pIS - 包含端口(IPv6地址用方括号括起)
printk(KERN_DEBUG "%pIS\n", sa);
// IPv4: 192.168.1.1:80
// IPv6: [2001:db8::1]:80
// %pISp - 仅打印IP地址(不含端口)
printk(KERN_DEBUG "%pISp\n", sa);
// IPv4: 192.168.1.1
// IPv6: 2001:db8::1
// %pISpc - 压缩IPv6格式,仅IP
printk(KERN_DEBUG "%pISpc\n", sa);
// IPv6: 2001:db8::1
五、实际使用示例
5.1 网络数据包调试
c
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <net/ipv6.h>
void dump_packet_info(struct sk_buff *skb)
{
struct iphdr *ip4;
struct ipv6hdr *ip6;
switch (skb->protocol) {
case htons(ETH_P_IP):
ip4 = ip_hdr(skb);
// 简洁的地址打印
printk(KERN_INFO "IPv4: %pI4 -> %pI4, len=%u\n",
&ip4->saddr, &ip4->daddr, ntohs(ip4->tot_len));
break;
case htons(ETH_P_IPV6):
ip6 = ipv6_hdr(skb);
// 使用压缩格式
printk(KERN_INFO "IPv6: %pI6c -> %pI6c, len=%u\n",
&ip6->saddr, &ip6->daddr, ntohs(ip6->payload_len));
break;
}
}
5.2 网络设备地址打印
c
#include <linux/netdevice.h>
void show_netdev_info(struct net_device *dev)
{
const struct net_device_ops *ops = dev->netdev_ops;
// MAC地址
printk(KERN_INFO "Device: %s\n", dev->name);
printk(KERN_INFO " MAC: %pM\n", dev->dev_addr);
// IPv4地址(需要遍历地址列表)
if (dev->ip_ptr) {
struct in_device *in_dev = __in_dev_get_rtnl(dev);
if (in_dev && in_dev->ifa_list) {
struct in_ifaddr *ifa = in_dev->ifa_list;
printk(KERN_INFO " IPv4: %pI4\n", &ifa->ifa_local);
}
}
}
5.3 套接字地址打印
c
#include <net/sock.h>
void print_socket_address(struct sock *sk)
{
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
int len;
// 获取本地地址
len = sizeof(sin);
if (kernel_getsockname(sk, (struct sockaddr *)&sin, &len) == 0) {
printk(KERN_INFO "Local: %pIS\n", &sin);
}
// 获取对端地址
len = sizeof(sin6);
if (kernel_getpeername(sk, (struct sockaddr *)&sin6, &len) == 0) {
printk(KERN_INFO "Remote: %pISc\n", &sin6);
}
}
六、格式说明符完整参考
6.1 汇总表
| 格式符 | 数据类型 | 输出示例 | 说明 |
|---|---|---|---|
%pI4 |
__be32* |
192.168.1.1 |
IPv4,无前导零 |
%pi4 |
__be32* |
192.168.001.001 |
IPv4,带前导零 |
%pI6 |
struct in6_addr* |
2001:0db8:0000:0000:0000:ff00:0042:8329 |
IPv6完整格式 |
%pi6 |
struct in6_addr* |
20010db8000000000000ff0000428329 |
IPv6无冒号 |
%pI6c |
struct in6_addr* |
2001:db8::ff00:42:8329 |
IPv6压缩格式 |
%pM |
u8[6] |
00:11:22:33:44:55 |
MAC地址,冒号分隔 |
%pMR |
u8[6] |
55:44:33:22:11:00 |
MAC地址,反转 |
%pMF |
u8[6] |
00-11-22-33-44-55 |
MAC地址,横线分隔 |
%pIS |
struct sockaddr* |
192.168.1.1:80 或 [2001:db8::1]:80 |
带端口 |
%pISp |
struct sockaddr* |
192.168.1.1 或 2001:db8::1 |
仅地址 |
%pISpc |
struct sockaddr* |
2001:db8::1 |
IPv6压缩+仅地址 |
6.2 IPv6压缩格式的特殊说明
c
// 压缩格式遵循 RFC 5952 标准
// 规则1: 省略前导零
// 规则2: 用 :: 表示最长的连续零段
// 规则3: 字母使用小写
struct in6_addr addr;
// 地址: 2001:0db8:0000:0000:0000:0000:0000:0001
// %pI6c 输出: 2001:db8::1
七、与传统方式的对比
7.1 旧式方法(已过时)
c
// 旧式宏(仍在部分代码中存在,但不推荐)
#include <linux/ip.h>
// 旧式IPv4打印
printk("IP: %d.%d.%d.%d\n",
NIPQUAD(iph->saddr)); // 繁琐,需要多个参数
// 旧式IPv6打印(已基本废弃)
printk("IPv6: " NIP6_FMT "\n",
NIP6(ipv6_hdr(skb)->saddr)); // 复杂,容易出错
7.2 现代方法(推荐)
c
// 现代方法:简洁、安全、类型检查
printk("IP: %pI4\n", &iph->saddr);
printk("IPv6: %pI6c\n", &ipv6_hdr(skb)->saddr);
八、注意事项
8.1 必须传递指针
c
__be32 ip = htonl(0xC0A80101);
// 错误:直接传值
printk("%pI4\n", ip); // 错误!
// 正确:传递地址
printk("%pI4\n", &ip); // 正确
8.2 字节序自动处理
c
// IPv4地址在网络协议栈中已经是网络字节序(__be32)
// 格式说明符会自动按网络字节序解释
struct iphdr *iph = ip_hdr(skb);
printk("%pI4\n", &iph->saddr); // 直接使用,无需 ntohl
8.3 RCU/锁保护
c
// 访问网络设备地址时可能需要锁保护
rcu_read_lock();
struct net_device *dev = dev_get_by_name(&init_net, "eth0");
if (dev) {
printk(KERN_INFO "MAC: %pM\n", dev->dev_addr);
dev_put(dev);
}
rcu_read_unlock();
8.4 格式符与内核版本
c
// %pI6c(IPv6压缩格式)需要较新的内核(3.x+)
// 如果需要兼容老内核,可以使用:
#ifdef CONFIG_PRINTK
printk("%pI6c\n", &addr);
#else
// 备选方案
#endif
九、完整调试示例
c
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <net/ipv6.h>
static void dump_skb_info(struct sk_buff *skb)
{
struct iphdr *ip4;
struct ipv6hdr *ip6;
struct ethhdr *eth = eth_hdr(skb);
printk(KERN_INFO "=== Packet Info ===\n");
printk(KERN_INFO "MAC: %pM -> %pM\n", eth->h_source, eth->h_dest);
switch (skb->protocol) {
case htons(ETH_P_IP):
ip4 = ip_hdr(skb);
printk(KERN_INFO "IPv4: %pI4:%d -> %pI4:%d\n",
&ip4->saddr, ntohs(tcp_hdr(skb)->source),
&ip4->daddr, ntohs(tcp_hdr(skb)->dest));
break;
case htons(ETH_P_IPV6):
ip6 = ipv6_hdr(skb);
printk(KERN_INFO "IPv6: %pI6c -> %pI6c\n",
&ip6->saddr, &ip6->daddr);
break;
}
}
// 注册到netfilter或驱动中使用
十、总结
| 需求 | 推荐格式符 | 示例 |
|---|---|---|
| IPv4地址快速打印 | %pI4 |
printk("%pI4", &addr) |
| IPv6地址打印(首选) | %pI6c |
printk("%pI6c", &addr6) |
| MAC地址打印 | %pM |
printk("%pM", mac) |
| 带端口的地址打印 | %pIS |
printk("%pIS", &sockaddr) |
核心要点:
- 所有格式符都必须传递指针,不能直接传值
- 地址数据应保持内核原有的字节序(IPv4为网络字节序)
- IPv6优先使用
%pI6c(压缩格式) %pIS系列可直接打印sockaddr结构,最为方便
这些扩展格式说明符是Linux内核网络调试中最常用的工具,比手动调用 in4_ntop/in6_ntop 要简洁得多。