本文编写一个简单的 Linux 内核模块,然后在 QEMU 中使用 insmod 加载模块, lsmod 查看模块信息, rmmod 卸载模块来进行测试工作。

实验环境在前文Linux Kernel 从编译到运行为 Linux Kernel 添加系统调用 中已经准备好了,这里不再赘述。

编译模块的工作流程如下:

  1. 环境准备

  2. 编写模块代码

  3. 编写 Makefile

  4. 编译模块

  5. 加载、卸载测试

This article writes a simple Linux kernel module, then tests it in QEMU by loading it with insmod, checking its info with lsmod, and unloading it with rmmod.

The experimental environment was already prepared in Linux Kernel: From Compilation to Running and Adding a System Call to the Linux Kernel, so I won’t repeat it here.

The module compilation workflow is as follows:

  1. Environment preparation

  2. Writing the module code

  3. Writing the Makefile

  4. Compiling the module

  5. Load/unload testing

环境准备

在前文 为 Linux Kernel 添加系统调用 中我们使用 make bzImage 编译内核,这个命令不会编译模块,也不会生成模块导出的符号。

所谓模块导出的符号,就是模块中的函数或者变量,如果其他模块或者内核代码需要调用模块中的函数或者变量,就需要这些符号。

在 Linux Kernel 中,模块导出的符号是通过 EXPORT_SYMBOLEXPORT_SYMBOL_GPL 来导出的。

编译模块需要使用 make modules 命令,这个命令会编译模块,并且生成模块导出的符号。对应的文件是 Module.symvers

在编写我们自己的模块之前,我们需要先编译内核,然后生成 Module.symvers 文件。

编译内核在前文中已经完成,这里不再重复编译,我们需要编译模块,并生成 Module.symvers 文件。

1
2
3
# 切换到内核源码目录
cd ~/Desktop/kernel/linux
make modules -j8

这个过程比 make bzImage 要慢一些。

编译完成后,如果我们在 ~/Desktop/kernel/linux 还看不到 Module.symvers 文件,需要执行 make modules_prepare 命令。

1
make modules_prepare

执行完成后,我们就可以在 ~/Desktop/kernel/linux 目录下看到 Module.symvers 文件了。

1
2
wgxls@server:~/Desktop/kernel/linux$ file Module.symvers 
Module.symvers: ASCII text

Module.symvers 文件是一个文本文件,里面记录了模块导出的符号。

它的基本格式如下:

1
<符号地址> <符号名称> <所在模块> <导出类型>

这是我的 Module.symvers 文件

编写模块代码

来到上文为 Linux Kernel 添加系统调用中的 workspace 目录,创建一个新的目录。

1
2
mkdir my_module_demo
cd my_module_demo

先写一个最简单的模块,只包含加载和退出函数。

创建 my_module.c touch my_module.c

写入以下内容。

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wgxls");
MODULE_DESCRIPTION("A simple Linux kernel module.");
MODULE_VERSION("0.1");

static int __init my_module_init(void) {
printk(KERN_INFO "Hello, world!\n");
return 0;
}

static void __exit my_module_exit(void) {
printk(KERN_INFO "Goodbye, world!\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

编写 Makefile

为 my_module 编写 Makefile 文件,其中 -C 后面跟 linux 内核代码的根路径。

1
2
3
4
5
6
7
obj-m += my_module.o

all:
make -C /home/wgxls/Desktop/kernel/linux M=$(PWD) modules

clean:
make -C /home/wgxls/Desktop/kernel/linux M=$(PWD) clean

编译模块

截至目前,workspace 目录下的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wgxls@server:~/Desktop/kernel/workspace$ tree -L 2
.
├── busybox
├── bzImage
├── initramfs
│   ├── bin
│   ├── etc
│   ├── init
│   ├── proc
│   ├── sbin
│   ├── sys
│   ├── test_sys_call
│   └── test_sys_call.c
├── initramfs.img
├── Makefile
└── my_module_demo
├── Makefile
└── my_module.c

8 directories, 10 files

进入到 my_module_demo 目录下,执行 make 命令。

1
2
3
4
5
6
7
8
9
10
wgxls@server:~/Desktop/kernel/workspace/my_module_demo$ make 
make -C /home/wgxls/Desktop/kernel/linux M=/home/wgxls/Desktop/kernel/workspace/my_module_demo modules
make[1]: 进入目录“/home/wgxls/Desktop/kernel/linux”
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.o
MODPOST /home/wgxls/Desktop/kernel/workspace/my_module_demo/Module.symvers
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.mod.o
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/.module-common.o
LD [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.ko
BTF [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.ko
make[1]: 离开目录“/home/wgxls/Desktop/kernel/linux”

编译完成后,在当前目录下会生成 my_module.ko

1
2
wgxls@server:~/Desktop/kernel/workspace/my_module_demo$ file my_module.ko 
my_module.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=c6f883b75116f6157daacfa6287799c1b0540009, with debug_info, not stripped

测试运行

最后,我们在 QEMU 的环境下测试这个模块的安装和卸载。

首先把 my_module.ko 拷贝到 initramfs 目录下。

1
cp my_module.ko ../initramfs/

然后重新制作 initramfs

1
2
wgxls@server:~/Desktop/kernel/workspace$ make clean
wgxls@server:~/Desktop/kernel/workspace$ make initramfs

启动 QEMU

1
wgxls@server:~/Desktop/kernel/workspace$ make run

安装模块

这里除了打印了 Hello, world! 之外,还报了一个模块签名验证失败的信息,同时将内核标记为污染。

暂时忽略这些信息。

使用 lsmod 来查看模块。

1
2
~ # lsmod | grep -ie "my"
my_module 12288 0 - Live 0xffffffffc050c000 (OE)

使用 rmmod 来卸载模块。

当我们第二次安装模块时,就不会再次打印模块验证失败的消息了。

The End…

Environment Preparation

In Adding a System Call to the Linux Kernel, we used make bzImage to compile the kernel. This command does not compile modules, nor does it generate the symbols exported by modules.

The so-called exported symbols are functions or variables in modules. If other modules or kernel code need to call a module’s functions or variables, they need these symbols.

In the Linux kernel, exported module symbols are exported via EXPORT_SYMBOL and EXPORT_SYMBOL_GPL.

To compile modules, you need the make modules command, which compiles modules and generates the exported module symbols. The corresponding file is Module.symvers.

Before writing our own module, we need to compile the kernel first, then generate the Module.symvers file.

The kernel compilation was already done in the previous article, so we won’t repeat it. We need to compile the modules and generate the Module.symvers file.

1
2
3
# Switch to the kernel source directory
cd ~/Desktop/kernel/linux
make modules -j8

This process is a bit slower than make bzImage.

After compilation, if you still can’t see the Module.symvers file in ~/Desktop/kernel/linux, run the make modules_prepare command.

1
make modules_prepare

After that, you can see the Module.symvers file in the ~/Desktop/kernel/linux directory.

1
2
wgxls@server:~/Desktop/kernel/linux$ file Module.symvers 
Module.symvers: ASCII text

The Module.symvers file is a text file that records the symbols exported by modules.

Its basic format is as follows:

1
<symbol address> <symbol name> <module> <export type>

This is my Module.symvers file

Writing the Module Code

Go to the workspace directory from Adding a System Call to the Linux Kernel, and create a new directory.

1
2
mkdir my_module_demo
cd my_module_demo

First write the simplest module, containing only init and exit functions.

Create my_module.c touch my_module.c

Write the following content.

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wgxls");
MODULE_DESCRIPTION("A simple Linux kernel module.");
MODULE_VERSION("0.1");

static int __init my_module_init(void) {
printk(KERN_INFO "Hello, world!\n");
return 0;
}

static void __exit my_module_exit(void) {
printk(KERN_INFO "Goodbye, world!\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

Writing the Makefile

Write a Makefile for my_module, where the path after -C is the root path of the linux kernel source code.

1
2
3
4
5
6
7
obj-m += my_module.o

all:
make -C /home/wgxls/Desktop/kernel/linux M=$(PWD) modules

clean:
make -C /home/wgxls/Desktop/kernel/linux M=$(PWD) clean

Compiling the Module

So far, the contents of the workspace directory are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wgxls@server:~/Desktop/kernel/workspace$ tree -L 2
.
├── busybox
├── bzImage
├── initramfs
│   ├── bin
│   ├── etc
│   ├── init
│   ├── proc
│   ├── sbin
│   ├── sys
│   ├── test_sys_call
│   └── test_sys_call.c
├── initramfs.img
├── Makefile
└── my_module_demo
├── Makefile
└── my_module.c

8 directories, 10 files

Enter the my_module_demo directory and run the make command.

1
2
3
4
5
6
7
8
9
10
wgxls@server:~/Desktop/kernel/workspace/my_module_demo$ make 
make -C /home/wgxls/Desktop/kernel/linux M=/home/wgxls/Desktop/kernel/workspace/my_module_demo modules
make[1]: 进入目录“/home/wgxls/Desktop/kernel/linux”
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.o
MODPOST /home/wgxls/Desktop/kernel/workspace/my_module_demo/Module.symvers
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.mod.o
CC [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/.module-common.o
LD [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.ko
BTF [M] /home/wgxls/Desktop/kernel/workspace/my_module_demo/my_module.ko
make[1]: 离开目录“/home/wgxls/Desktop/kernel/linux”

After compilation, my_module.ko will be generated in the current directory.

1
2
wgxls@server:~/Desktop/kernel/workspace/my_module_demo$ file my_module.ko 
my_module.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=c6f883b75116f6157daacfa6287799c1b0540009, with debug_info, not stripped

Test Run

Finally, we test the installation and removal of this module in the QEMU environment.

First, copy my_module.ko to the initramfs directory.

1
cp my_module.ko ../initramfs/

Then rebuild the initramfs

1
2
wgxls@server:~/Desktop/kernel/workspace$ make clean
wgxls@server:~/Desktop/kernel/workspace$ make initramfs

Boot QEMU

1
wgxls@server:~/Desktop/kernel/workspace$ make run

Install the module

Besides printing Hello, world!, there’s also a module signature verification failure message, and the kernel is marked as tainted.

Ignore these messages for now.

Use lsmod to view the module.

1
2
~ # lsmod | grep -ie "my"
my_module 12288 0 - Live 0xffffffffc050c000 (OE)

Use rmmod to unload the module.

When we install the module a second time, the module verification failure message won’t be printed again.

The End…