Release log:
2021-04-24 六: 完成初版
背景介绍
最近在跟进一个网络相关的问题,需要查看经过 __netif_receive_skb 的报文是否有异常。如果打印所有的包,由于打印太多会影响性能,并且打印的内容也不会太详细。所以决定,把监听到的报文全部保存到 pcap 文件中,然后通过 wireshark 查看
这里对需要用到的知识做一个整理
知识点分析
- 怎么在内核模块中读写文件
- pcap 的文件格式是怎样的
怎么在内核中读写文件
一段简单的实例代码如下:
// main.c
#include <linux/module.h> // 包含了对模块的结构定义以及模块的版本控制
#include <linux/kernel.h> // 包含常用的内核函数
#include <linux/fs.h>
MODULE_LICENSE("Dual BSD/GPL");
static int __init skb2pcap_init(void)
{
struct file *fp = filp_open("/tmp/kernel_rw", O_CREAT | O_WRONLY | O_TRUNC, 0644);
const char *wstr = "write from kernel";
int len = 0;
mm_segment_t old_fs;
if (IS_ERR(fp))
{
printk("[%s] open file failed\n", __func__);
return -1;
}
old_fs = get_fs();
/* 改变 kernel 对内存地址检查的处理方式, 如果此处被抢占是否会有风险 */
set_fs(KERNEL_DS);
len = vfs_write(fp, wstr, strlen(wstr), &fp->f_pos);
set_fs(old_fs);
filp_close(fp, NULL);
printk("[%s] write len: %d\n", __func__, len);
return 0;
}
static void __exit skb2pcap_exit(void)
{
struct file *fp = filp_open("/tmp/kernel_rw", O_RDONLY, 0);
int len = 0;
char buf[128] = {0};
mm_segment_t old_fs;
if (IS_ERR(fp))
{
printk("[%s] open file failed\n", __func__);
return;
}
old_fs = get_fs();
set_fs(KERNEL_DS);
len = vfs_read(fp, buf, sizeof(buf) - 1, &fp->f_pos);
set_fs(old_fs);
filp_close(fp, NULL);
printk("[%s] get str[%d]: %s\n", __func__, len, buf);
}
module_init(skb2pcap_init);
module_exit(skb2pcap_exit);
# Makefile
# 如果已定义 KERNELRELEASE,则说明是从内核构造系统调用的,因此可利用其内建语句
ifneq ($(KERNELRELEASE), )
obj-m := skb2pcap.o
skb2pcap-objs := main.o
# 否则,是直接从命令行调用的,这时要用内核构造系统
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
# rm -f *.o *.cmd .*.cmd *.mod modules.order *.mod.c
endif
需要注意,vfs_read 和 vfs_write 原型中,buffer 指针是用 __user
修饰的,如果直接使用内核地址作为参数会返回失败。所以在使用前需要调用 set_fs 改变 kernel 对内存地址的检查方式
遇到问题
insmode 的时候提示错误
$ sudo insmod ./skb2pcap.ko
insmod: ERROR: could not insert module ./skb2pcap.ko: Operation not permitted
使用 dmesg 查看,发现如下提示
Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
解决办法: 重启电脑,在 BIOS 中关闭 secure boot 功能,保存并重启。这个时候 insmod 就没问题了
但 insmode 时会发现,终端没有 printk 内容输出。其实输出内容可以在 dmesg 中看到
pcap 的文件格式是怎样的
pcap 文件的总体结构是: 文件头–数据包头1–数据包1–数据包头2–数据包2…
其中文件头的格式如下:
struct pcap_file_header {
uint32_t magic; // 为 0xa1b2c3d4 或者 0xd4c3b2a1,用来识别字节顺序,前者为大端模式,后者为小端模式
uint16_t version_major; // 主版本号,一般为 0x0200
uint16_t version_minor; // 次版本号,一般为 0x0400
uint32_t thiszone; // GMT 和本地时间的差值,单位秒。一般为零
uint32_t sigfigs; // 时间戳的精度,一般为全零
uint32_t snaplen; // 设置所抓获的数据包的最大长度,如果所有数据包都要抓获,将该值设置为65535
uint32_t linktype; // 链路类型
};
/* 链路类型对应关系如下
0 BSD loopback devices, except for later OpenBSD
1 Ethernet, and Linux loopback devices
6 802.5 Token Ring
7 ARCnet
8 SLIP
9 PPP
10 FDDI
100 LLC/SNAP-encapsulated ATM
101 "raw IP", with no link
102 BSD/OS SLIP
103 BSD/OS PPP
104 Cisco HDLC
105 802.11
108 later OpenBSD loopback devices (with the AF_value in network byte order)
113 special Linux "cooked" capture
114 LocalTalk
*/
数据包头的格式如下:
struct pcap_pkt_header {
uint32_t ts_s; // 时间戳高位,精确到秒,从 GMT 开始记
uint32_t ts_ms; // 时间戳低位,精确到毫秒
uint32_t caplen; // 抓获到数据帧的长度
uint32_t len; // 离线数据长度,网路中实际数据帧的长度,一般不大于Caplen,多数情况下和Caplen值一样
};
一段简单的解析 pcap 报文的代码如下:
int main(int argc, char *argv[])
{
int fd = -1;
struct pcap_file_header fhdr = {0};
struct pcap_pkt_header phdr = {0};
uint8_t databuf[2048] = {0};
int datalen = 0;
if (2 != argc)
{
printf("usage: %s pcap_file_name\n", argv[0]);
return 0;
}
if ((fd = open(argv[1], O_RDONLY)) < 0)
{
printf("open %s failed: %s\n", argv[1], strerror(errno));
goto leave;
}
if (sizeof(fhdr) != read(fd, &fhdr, sizeof(fhdr)))
{
printf("read file failed: %s\n", strerror(errno));
goto leave;
}
printf("file header info: magic(0x%08x), major_version(0x%04x), "
"minor_version(0x%04x), thiszone(0x%08x), sigfigs(0x%08x), "
"snaplen(0x%08x), linktype(0x%08x)\n",
fhdr.magic, fhdr.version_major, fhdr.version_minor,
fhdr.thiszone, fhdr.sigfigs, fhdr.snaplen, fhdr.linktype);
while (sizeof(phdr) == read(fd, &phdr, sizeof(phdr)))
{
int i = 0;
datalen = read(fd, databuf, phdr.caplen);
if (phdr.caplen != datalen)
{
printf("read data len err(%d - %d): %s\n", phdr.caplen, datalen, strerror(errno));
break;
}
printf("packet header info: ts_s(%d), ts_ms(%d), caplen(%d), len(%d):",
phdr.ts_s, phdr.ts_ms, phdr.caplen, phdr.len);
for (i = 0; i < phdr.caplen; i ++)
{
if (0 == i % 16) printf("\n");
else if (0 == i % 8) printf(" ");
printf("%02x ", databuf[i]);
}
printf("\n");
}
leave:
if (fd >= 0) close(fd);
return 0;
}