0%

Linux Kernel Module 原理与实践

在 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

  • 加载模块:执行命令insmod hello.ko加载模块。注意insmod命令不会自动加载依赖项,如果你编写的驱动模块依赖了其他模块,则可以使用modprobe命令自动加载依赖项。
  • 卸载模块:执行命令rmmod hello卸载模块
  • 验证输出:可以用 dmesg 查看内核日志:
1
2
[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 函数指针记录了我们定义的模块入口函数和出口函数。

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

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

//...
};

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

kernel/module.c
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的实现为:

kernel/module.c
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完成。

kernel/module.c
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将模块卸载。

参考资料