在 Linux 内核中,kernel module 是一些可以让内核在需要时载入和执行的代码,并且可以在不需要时由内核卸载。在很早以前,每编写一个设备驱动都需要重新编译一次整个内核镜像,kernel module 机制的出现避免了每次给内核添加功能时都需要重新编译,极大的提升了内核的可扩展性。本文将介绍如何基于 kernel module 机制编写自己的 module,并介绍 kernel module 的基层实现原理,文中所有演示代码可以在 Github 中找到。

编写 Module

工作环境为 Ubuntu 20.04 内核版本为 5.4.0

1
2
$uname -a
Linux VM-0-29-ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

下面是一个简单的 hello worldkernel module

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>

static int hello_init(void)
{
    printk(KERN_ALERT "Hello, world!\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "GoodBye, cruel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("houmin.wei@outlook.com");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("A Hello, world Module");
  • module_init 为模块入口函数,在模块加载时被调用执行
  • module_exit 为模块出口函数,在模块卸载被调用执行

为了将上述代码编译成内核模块,使用 make 进行编译,下面是使用的 Makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 如果已定义KERNELRELEASE,则说明是从内核构造系统调用的,
# 因此可利用其内建语句
ifneq ($(KERNELRELEASE), )
    obj-m := hello.o
    # module-objs := file1.o file2.o 如果模块依赖多个源文件,请添加这一句并相应地修改目标文件列表
# 否则,是直接从命令行调用的,
# 这时要调用内核构造系统
else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
endif

其中,obj-m 指定了目标文件的名称,文件名需要和源文件名相同(扩展名除外),以便于 make 自动推导。

使用 make 命令编译模块,得到模块文件 hello.ko 和一些中间文件。

1
2
3
4
5
6
7
8
9
$ make
make -C /lib/modules/5.4.0-42-generic/build M=/home/ubuntu/houmin/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-42-generic'
  CC [M]  /home/ubuntu/houmin/hello/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/ubuntu/houmin/hello/hello.mod.o
  LD [M]  /home/ubuntu/houmin/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-42-generic'

Linking a module into the kernel
Linking a module into the kernel

  • 加载模块:执行命令 insmod hello.ko加载模块。注意 insmod命令不会自动加载依赖项,如果你编写的驱动模块依赖了其他模块,则可以使用 modprobe命令自动加载依赖项。
  • 卸载模块:执行命令 rmmod hello卸载模块
  • 验证输出:可以用 dmesg 查看内核日志:
[12517.215951] Hello, world!
[14157.446937] GoodBye, cruel world!

Module 原理

内核链接

查看内核模块的 ko 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h hello.ko
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          2904 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         19
  Section header string table index: 18

可以看到,hello.ko 的文件类型为可重定位目标文件,这和一般的目标文件格式没有任何区别。我们知道,目标文件是不能直接执行的,它需要经过链接器的地址空间分配、符号解析和重定位的过程,转化为可执行文件才能执行。实际上,内核将 hello.ko 加载后对其进行了链接

模块加载

模块数据结构的 initexit 函数指针记录了我们定义的模块入口函数和出口函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct module
{
    /* Startup function. */
    int (*init)(void);

    /* Destruction function. */
    void (*exit)(void);

    //...
};

模块加载由内核的系统调用 init_module 完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* This is where the real work happens */
SYSCALL_DEFINE3(init_module, void __user *, umod,
       unsigned long, len, const char __user *, uargs)
{
    struct module *mod;
    int ret = 0;

    /* Do all the hard work */
    mod = load_module(umod, len, uargs); //模块加载

    /* Start the module */
    if (mod->init != NULL)
       ret = do_one_initcall(mod->init);//模块init函数调用

    //...
    return 0;

}

系统调用 init_moduleSYSCALL_DEFINE3(init_module...) 实现,其中有两个关键的函数调用,load_module 用于模块加载,do_one_initcall 用于回调模块的 init 函数。

函数 load_module 的实现为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* Allocate and load the module: note that size of section 0 is always
   zero, and we rely on this for optional sections. */
static struct module *load_module(void __user *umod,
                unsigned long len,
                const char __user *uargs)
{
    struct load_info info = { NULL, };
    struct module *mod;
    long err;

    /* Copy in the blobs from userspace, check they are vaguely sane. */
    err = copy_and_check(&info, umod, len, uargs); // 拷贝到内核
    if (err)
       return ERR_PTR(err);

    /* Figure out module layout, and allocate all the memory. */
    mod = layout_and_allocate(&info); // 地址空间分配
    if (IS_ERR(mod)) {
       err = PTR_ERR(mod);
       goto free_copy;
    }

    /* Fix up syms, so that st_value is a pointer to location. */
    err = simplify_symbols(mod, &info); // 符号解析

    if (err < 0)
       goto free_modinfo;

    err = apply_relocations(mod, &info); // 重定位
    if (err < 0)
       goto free_modinfo;

  //...
}

函数 load_module 内有四个关键的函数调用:

  • copy_and_check 将模块从用户空间拷贝到内核空间
  • layout_and_allocate 为模块进行地址空间分配
  • simplify_symbols 为模块进行符号解析
  • apply_relocations 为模块进行重定位

由此可见,模块加载时,内核为模块文件 hello.ko 进行了链接的过程。

至于函数 do_one_initcall 的实现就比较简单了,即调用了模块的入口函数 init

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int __init_or_module do_one_initcall(initcall_t fn)
{
    int count = preempt_count();
    int ret;

    if (initcall_debug)
       ret = do_one_initcall_debug(fn);
    else
       ret = fn(); //调用init module

    //...
    return ret;
}

模块卸载

模块卸载由内核的系统调用 delete_module 完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
        unsigned int, flags)
{
    struct module *mod;
    char name[MODULE_NAME_LEN];
    int ret, forced = 0;

    //...

    /* Final destruction now no one is using it. */
    if (mod->exit != NULL)
      mod->exit(); //调用exit module
      free_module(mod);//卸载模块

    //...
}

通过回调 exit 完成模块的出口函数功能,最后调用 free_module将模块卸载。

参考资料