内核模块
实验:编写一个简单的内核模块
首先我们编写内核模块代码,内容如代码清单 1 所示,将其命名为 hello.c。
- #include <linux/init.h>
- #include <linux/module.h>
- static int __init my_test_init(void)
- {
- printk("my first kernel module init\n");
- return 0;
- }
- static void __exit my_test_exit(void)
- {
- printk("goodbye\n");
- }
- module_init(my_test_init);
- module_exit(my_test_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("rlk");
- MODULE_DESCRIPTION("my test kernel module");
- MODULE_ALIAS("mytest");
最简单的说明是:my_test_init 函数会在模块加载的时候被调用;my_test_exit 函数会在模块卸载的时候被调用。
但是具体牵扯到许多细节,主要和 __init、__exit、module_init、module_exit 这些宏有关系。我们不妨直接把这些宏展开后来看:
- static int __attribute__((__section__(".init.text"))) __attribute__((__cold__)) my_test_init(void)
- {
- ({ do {} while (0); _printk("my first kernel module init\n"); });
- return 0;
- }
- static void __attribute__((__section__(".exit.text"))) __attribute__((__cold__)) __attribute__((no_instrument_function)) my_test_exit(void)
- {
- ({ do {} while (0); _printk("goodbye\n"); });
- }
- static inline __attribute__((__gnu_inline__)) __attribute__((__unused__)) __attribute__((no_instrument_function)) initcall_t __attribute__((__unused__)) __inittest(void) { return my_test_init; } int init_module(void) __attribute__((__copy__(my_test_init))) __attribute__((alias("my_test_init"))); static void * __attribute__((__used__)) __attribute__((__section__(".init.data"))) __UNIQUE_ID___addressable_init_module186 = (void *)&init_module;;;
- static inline __attribute__((__gnu_inline__)) __attribute__((__unused__)) __attribute__((no_instrument_function)) exitcall_t __attribute__((__unused__)) __exittest(void) { return my_test_exit; } void cleanup_module(void) __attribute__((__copy__(my_test_exit))) __attribute__((alias("my_test_exit"))); static void * __attribute__((__used__)) __attribute__((__section__(".exit.data"))) __UNIQUE_ID___addressable_cleanup_module187 = (void *)&cleanup_module;;;
- static const char __UNIQUE_ID_license188[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "license" "=" "GPL";
- static const char __UNIQUE_ID_author189[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "author" "=" "rlk";
- static const char __UNIQUE_ID_description190[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "description" "=" "my test kernel module";
- static const char __UNIQUE_ID_alias191[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "alias" "=" "mytest";
从展开的内容来看,所有的宏都是涉及节的定义。虽然现在还不清楚具体的加载原理,但是能感受到模块是利用具体节加载的。
比如,module_init 会把 my_test_init 函数代码段关联到 init_module 函数,并把 init_module 函数放在指定节中。
展开的内容基于 "gcc -E" 获得。
"make --trace --debug" 可以找到具体规则调用的点。因为是单个文件,所以为了方便,我们可以把 gcc -E 命令写死。
接下来我们实验在本机环境编译内核模块,以及交叉编译内核模块。需要编写不同的 Makefile 文件。
本机环境编译
看到清单 1.2,我们对其中涉及到的内容进行逐一讲解。
- BASEINCLUDE ?= /lib/modules/`uname -r`/build
- myhello-objs := hello.o
- obj-m := myhello.o
- all:
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
- clean:
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
- rm -rf *.ko;
-C 选项是 make 命令的一个选项,它用于改变当前工作目录。当使用 -C 选项时,make 会在执行编译任务之前切换到指定的目录。
即 -C 会执行其指定目录下的 Makefile。
编译内核需要执行 linux 的顶层 Makefile。
M 是传递给 linux 顶层 Makefile 的变量,指定模块的目录。
linux 顶层 Makefile 中会用到这个变量,比如:
KBUILD_EXTMOD := $(M)
obj-m 指定要编译的模块。模块对应的源文件通过 $(模块)-objs 变量指定。在本例中,我们需要编译 myhello 模块,模块依赖的源文件通过 myhello-objs,即需要 hello.c 文件。
这边刚看有点绕,是因为之前看的例子都直接指定的 obj-m 变量,并没有定义 $(模块)-objs 变量。那是因为例子都比较简单,只有一个源文件。
像上述写法,如果模块对应多个源文件,只要再往 $(模块)-objs 变量里添加即可。
可以推测 Makefile 的逻辑:会有一条规则匹配各个模块对应的依赖源文件,比如 $(m)-objs。如果没有匹配到模块名,则模块名默认和 obj-m 里的依赖名一样。
最终编译的话,直接调用 make 命令即可。
- tim@tim:~$ make
交叉编译
交叉编译的 Makefile 和本机环境编译的基本一样,如代码清单 1.3 所示,需要改变 linux 顶层 Makefile 的路径。
- BASEINCLUDE ?= /home/tim/runninglinuxkernel_5.0/
- #BASEINCLUDE ?= /lib/modules/`uname -r`/build
- CONFIG_MODULE_SIG=n
- mytest-objs := my_test.o
- obj-m := mytest.o
- all :
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
- clean:
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
- rm -f *.ko;
最终编译的话,需要额外定义交叉编译的架构和交叉编译 gcc 前缀。
- tim@tim:~$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
ARCH 和 CROSS_COMPILE 变量也是在 linux 顶层 Makefile 中使用的变量。
所以第一次看到模块的 Makefile 时,感觉不知所云,我觉得是正常的。因为它本身就跟 linux 的编译逻辑紧密相关,没了解过肯定就不知道为什么要定义这些变量。
第一次接触的话,可以看一下我之前写的这篇文章 《[ARM Linux系统移植] U-Boot 顶层 Makefile 分析 - 基础知识》,会对 linux 的编译有一个最初步的认识。
验证
本机环境编译的模块可以在本机环境上运行,交叉编译的模块需要在 QEMU 上运行。为了方便,这边在本机环境上验证编译好的模块。
编译成功,会有 .ko 文件生成,我们通过 insmod 命令加载:
- tim@tim:~$ sudo insmod myhello.ko
可以使用 dmesg 命令查看内核的输出信息:
- tim@tim:~$ sudo dmesg
- [ 1795.690270] my first kernel module init
可以使用 lsmod 列出当前系统中的模块,可以看到我们的模块已经被加载。
- tim@tim:~$ lsmod
- Module Size Used by
- myhello 16384 0
加载完模块之后,系统会在 /sys/module 目录下针对模块新建一个目录。
- tim@tim:/sys/module/myhello$ tree -a
- .
- ├── coresize
- ├── holders
- ├── initsize
- ├── initstate
- ├── notes
- │ ├── .note.gnu.build-id
- │ └── .note.Linux
- ├── refcnt
- ├── sections
- │ ├── .exit.data
- │ ├── .exit.text
- │ ├── .gnu.linkonce.this_module
- │ ├── .init.data
- │ ├── .init.text
- │ ├── __mcount_loc
- │ ├── .note.gnu.build-id
- │ ├── .note.Linux
- │ ├── .return_sites
- │ ├── .rodata.str1.1
- │ ├── .strtab
- │ └── .symtab
- ├── srcversion
- ├── taint
- └── uevent
- 3 directories, 21 files
卸载模块,可以使用 rmmod 命令:
- tim@tim:~$ sudo rmmod myhello
- tim@tim:~$ sudo dmesg
- [ 2611.240187] goodbye
实验:向内核模块传递参数
这节我们实验如何向模块传递参数。如代码清单 2 所示,定义了两个内核参数,debug 和 mytest,其中 module_param 宏是关键。
- #include <linux/module.h>
- #include <linux/init.h>
- static int debug = 1;
- module_param(debug, int, 0644);
- MODULE_PARM_DESC(debug, "enable debugging information");
- #define dprintk(args...) \
- if (debug) { \
- printk(KERN_DEBUG args); \
- }
- static int mytest = 100;
- module_param(mytest, int, 0644);
- MODULE_PARM_DESC(mytest, "test for module parameter");
- static int __init my_test_init(void)
- {
- dprintk("my first kernel module init\n");
- dprintk("module parameter=%d\n", mytest);
- return 0;
- }
- static void __exit my_test_exit(void)
- {
- dprintk("goodbye module parameter=%d\n", mytest);
- }
- module_init(my_test_init);
- module_exit(my_test_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("rlk");
- MODULE_DESCRIPTION("kernel module parameter test");
- MODULE_ALIAS("module paramter test");
module_param 宏的定义如下:
- module_param(name, type, perm);
其中:
name : 参数的名称,即要在内核模块中使用的变量名。
type : 参数的数据类型,如 int, charp (字符指针), bool 等。注意,这里使用的是内核参数类型,而不是通常的 C 数据类型。
perm : 参数的访问权限。用于在 /sys/module/<module_name>/parameters/ 目录下创建一个文件,以便用户空间程序可以访问和修改这些参数。这里的权限是一个八进制数,表示文件的访问权限。
可以在加载模块的时候,通过命令行指定参数的值。
- tim@tim:~$ sudo insmod myparam.ko mytest=200
- tim@tim:~$ sudo dmesg
- [ 3728.323959] my first kernel module init
- [ 3728.323963] module parameter=200
也可以通过创建的模块文件对参数进行修改。
- tim@tim:~$ cd /sys/module/myparam/parameters
- tim@tim:/sys/module/myparam/parameters$ echo 300 | sudo tee mytest
- tim@tim:/sys/module/myparam/parameters$ sudo rmmod myparam
- tim@tim:/sys/module/myparam/parameters$ sudo dmesg
- [ 4291.678165] goodbye module parameter=300