内核模块

实验:编写一个简单的内核模块

首先我们编写内核模块代码,内容如代码清单 1 所示,将其命名为 hello.c

代码清单 1.1 内核模块 hello.c
  1. #include <linux/init.h>
  2. #include <linux/module.h>
  3.  
  4. static int __init my_test_init(void)
  5. {
  6.     printk("my first kernel module init\n");
  7.     return 0;
  8. }
  9.  
  10. static void __exit my_test_exit(void)
  11. {
  12.     printk("goodbye\n");
  13. }
  14.  
  15. module_init(my_test_init);
  16. module_exit(my_test_exit);
  17.  
  18. MODULE_LICENSE("GPL");
  19. MODULE_AUTHOR("rlk");
  20. MODULE_DESCRIPTION("my test kernel module");
  21. MODULE_ALIAS("mytest");

最简单的说明是:my_test_init 函数会在模块加载的时候被调用;my_test_exit 函数会在模块卸载的时候被调用。

但是具体牵扯到许多细节,主要和 __init__exitmodule_initmodule_exit 这些宏有关系。我们不妨直接把这些宏展开后来看:

  1. static int __attribute__((__section__(".init.text"))) __attribute__((__cold__)) my_test_init(void)
  2. {
  3.  ({ do {} while (0); _printk("my first kernel module init\n"); });
  4.  return 0;
  5. }
  6.  
  7. static void __attribute__((__section__(".exit.text"))) __attribute__((__cold__)) __attribute__((no_instrument_function)) my_test_exit(void)
  8. {
  9.  ({ do {} while (0); _printk("goodbye\n"); });
  10. }
  11.  
  12. 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;;;
  13. 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;;;
  14.  
  15.  static const char __UNIQUE_ID_license188[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "license" "=" "GPL";
  16. static const char __UNIQUE_ID_author189[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "author" "=" "rlk";
  17. static const char __UNIQUE_ID_description190[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = "description" "=" "my test kernel module";
  18. 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,我们对其中涉及到的内容进行逐一讲解。

代码清单 1.2 本机编译 Makefile
  1. BASEINCLUDE ?= /lib/modules/`uname -r`/build
  2.  
  3. myhello-objs := hello.o
  4. obj-m := myhello.o
  5.  
  6. all:
  7.     $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
  8.  
  9. clean:
  10.     $(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
  11.     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 的路径。

代码清单 1.3 交叉编译 Makefile
  1. BASEINCLUDE ?= /home/tim/runninglinuxkernel_5.0/
  2. #BASEINCLUDE ?= /lib/modules/`uname -r`/build
  3.  
  4. CONFIG_MODULE_SIG=n
  5.  
  6. mytest-objs := my_test.o
  7.  
  8. obj-m   :=   mytest.o
  9. all :
  10.     $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
  11.  
  12. clean:
  13.     $(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
  14.     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 宏是关键。

代码清单 2 模块参数
  1. #include <linux/module.h>
  2. #include <linux/init.h>
  3.  
  4. static int debug = 1;
  5. module_param(debug, int, 0644);
  6. MODULE_PARM_DESC(debug, "enable debugging information");
  7.  
  8. #define dprintk(args...) \
  9.     if (debug) { \
  10.         printk(KERN_DEBUG args); \
  11.     }
  12.  
  13. static int mytest = 100;
  14. module_param(mytest, int, 0644);
  15. MODULE_PARM_DESC(mytest, "test for module parameter");
  16.  
  17. static int __init my_test_init(void)
  18. {
  19.     dprintk("my first kernel module init\n");
  20.     dprintk("module parameter=%d\n", mytest);
  21.     return 0;
  22. }
  23.  
  24. static void __exit my_test_exit(void)
  25. {
  26.     dprintk("goodbye module parameter=%d\n", mytest);
  27. }
  28.  
  29. module_init(my_test_init);
  30. module_exit(my_test_exit);
  31.  
  32. MODULE_LICENSE("GPL");
  33. MODULE_AUTHOR("rlk");
  34. MODULE_DESCRIPTION("kernel module parameter test");
  35. 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