背景
在调试内核时,最常用的方式就是添加打印。这个方法简单直接,但有个问题:若添加打印的函数会被频繁地调用执行,那这很可能使系统在启动或运行阶段触发海量的打印,反而将关键的信息淹没;或者由此导致系统运行卡顿甚至无法启动。
还有另一种常见的调试需求:我们需要在内核中模拟某种故障,并且是以我们所能掌控的时机和方式来触发。关于这点我们一般会想到内核故障注入,但其手段和所涉及的软件工具比较复杂,且其所能模拟的故障种类和故障位置一般是给定的,无法随意调整。
对于上述两个场景,我们其实可以用同一个方法解决:在内核中预埋一个“开关”,并在内核的用户态接口(如 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 # 打开开关