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

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

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

  1. 环境准备

  2. 编写模块代码

  3. 编写 Makefile

  4. 编译模块

  5. 加载、卸载测试

环境准备

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

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

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

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

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

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

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

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

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

make modules_prepare

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

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

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

它的基本格式如下:

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

这是我的 Module.symvers 文件

编写模块代码

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

mkdir my_module_demo
cd my_module_demo

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

创建 my_module.c touch my_module.c

写入以下内容。

#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 内核代码的根路径。

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 目录下的内容如下:

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 命令。

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

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 目录下。

cp my_module.ko ../initramfs/

然后重新制作 initramfs

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

启动 QEMU

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

安装模块

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

暂时忽略这些信息。

使用 lsmod 来查看模块。

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

使用 rmmod 来卸载模块。

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

The End…