用于内核测试的内核模块

背景

在调试内核时,最常用的方式就是添加打印。这个方法简单直接,但有个问题:若添加打印的函数会被频繁地调用执行,那这很可能使系统在启动或运行阶段触发海量的打印,反而将关键的信息淹没;或者由此导致系统运行卡顿甚至无法启动。

还有另一种常见的调试需求:我们需要在内核中模拟某种故障,并且是以我们所能掌控的时机和方式来触发。关于这点我们一般会想到内核故障注入,但其手段和所涉及的软件工具比较复杂,且其所能模拟的故障种类和故障位置一般是给定的,无法随意调整。

对于上述两个场景,我们其实可以用同一个方法解决:在内核中预埋一个“开关”,并在内核的用户态接口(如 proc 文件系统)上创建一个专门的接口来控制这个开关。对于第一个场景,我们可以将那些打印改为由开关控制,只有开关打开时才打印。这样我们就可以对开启打印的时机进行控制。对于第二个场景,我们可以让开关来触发故障。通过这种方式,相较于正式的故障注入,在故障触发时机、故障位置和故障实现三者上,开发者都能拥有较高的自由度。

下面就来介绍这种开关及其接口的实现方式。

实现

首先,要在内核代码中添加“开关”,并导出符号,供外部模块使用:

1int test_switch;
2EXPORT_SYMBOL(test_switch);

其次,要实现一个内核模块,提供一个用户态接口来控制“开关”。示例代码中的接口读写处理函数,是参考 security/selinux/selinuxfs.c 中的 sel_{read,write}_enforce() 函数:

 1#include <linux/module.h>
 2#include <linux/moduleparam.h>
 3#include <linux/init.h>
 4#include <linux/kernel.h>
 5#include <linux/proc_fs.h>
 6#include <linux/slab.h>
 7
 8MODULE_LICENSE("Dual BSD/GPL");
 9MODULE_AUTHOR("GRQ");
10
11static struct proc_dir_entry *ent;
12extern int test_switch;
13
14static ssize_t mywrite(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
15{
16	char *page = NULL;
17	ssize_t length;
18	int new_value;
19
20	page = memdup_user_nul(ubuf, count);
21
22	length = -EINVAL;
23	if (sscanf(page, "%d", &new_value) != 1)
24        goto out;
25
26	test_switch = !!new_value;
27	length = count;
28out:
29	kfree(page);
30	return length;
31}
32
33#define TMPBUFLEN 12
34static ssize_t myread(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
35{
36    char tmpbuf[TMPBUFLEN];
37	ssize_t length;
38
39	length = scnprintf(tmpbuf, TMPBUFLEN, "%d", test_switch);
40	return simple_read_from_buffer(ubuf, count, ppos, tmpbuf, length);
41}
42
43static struct proc_ops myops =
44{
45	.proc_read = myread,
46	.proc_write = mywrite,
47};
48
49static int simple_init(void)
50{
51	ent = proc_create("mydev", 0660, NULL, &myops);
52	return 0;
53}
54
55static void simple_cleanup(void)
56{
57	proc_remove(ent);
58}
59
60module_init(simple_init);
61module_exit(simple_cleanup);

最后,需要为内核模块弄一个构建用的 Makefile。由于该模块的结构很简单,因此就参考内核文档 搞了个极简 Makefile,如下:

 1ifneq ($(KERNELRELEASE),)
 2# kbuild part of makefile
 3obj-m := main.o
 4
 5else
 6# normal makefile
 7KERNEL_DIR=/your/linux/kernel/code
 8
 9all:
10	make -C ${KERNEL_DIR} M=$(PWD) modules
11
12clean:
13	make -C ${KERNEL_DIR} M=$(PWD) clean
14
15endif

使用

获取要测试的内核的源码,修改上述 Makefile 中的 KERNEL_DIR 变量为源码路径。随后:

1# In module's dir
2make                  # 构建模块
3
4insmod main.ko        # 插入模块
5echo 1 > /proc/mydev  # 打开开关

Spinlock 代码及故障模型分析
SLUB 的类三级缓存结构