3. 最小内核模块:编写一个只包含init和exit的空模块并编译
在深入调试 init 与 exit 函数故障之前,必须掌握内核模块的“最小骨架”。一个可加载的内核模块(Loadable Kernel Module, LKM)至少需要两个宏:module_init() 和 module_exit(),它们分别注册模块的入口和出口函数。本节将带领你从零构建、编译并验证一个空模块,为后续的故障注入与调试打下坚实基础。
3.1 模块的“三要素”
任何内核模块都必须包含以下三个部分,缺一不可:
| 要素 | 宏/声明 | 作用 |
|---|---|---|
| 入口函数 | module_init(init_func) |
模块加载时被内核调用,返回 0 表示成功,负值表示失败 |
| 出口函数 | module_exit(exit_func) |
模块卸载时被内核调用,执行清理工作 |
| 许可证声明 | MODULE_LICENSE("GPL") |
必须声明许可证,否则内核会标记模块为“污染” |
调试提示: 即使函数体为空,
module_init 和 module_exit 也必须显式声明。缺少任何一个宏,insmod 或 rmmod 操作都会失败,这是初学者最常见的故障之一。
3.2 编写空模块代码
创建一个名为 empty_module.c 的文件,内容如下:
#include <linux/module.h> // 包含 MODULE_* 宏
#include <linux/kernel.h> // 包含 printk 等内核函数
#include <linux/init.h> // 包含 __init 和 __exit 宏
static int __init empty_init(void)
{
// 空函数体,仅返回成功
return 0;
}
static void __exit empty_exit(void)
{
// 空函数体,无清理操作
}
module_init(empty_init);
module_exit(empty_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Course");
MODULE_DESCRIPTION("A minimal empty kernel module for init/exit debugging");
关键点解析:
__init和__exit是内核属性标记,告诉内核将函数放入特定的内存段,模块加载/卸载后可释放该段内存。static限定符确保函数符号不会暴露到全局内核符号表中,避免命名冲突。return 0是init函数成功的唯一标志;返回负值(如-ENOMEM)会导致加载失败。
3.3 编译模块:Makefile 编写
内核模块的编译必须依赖内核源码树(或内核头文件包)。以下是最小化的 Makefile:
obj-m := empty_module.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
| 变量 | 含义 |
|---|---|
obj-m |
指定要编译成模块的目标文件(不含路径) |
KDIR |
当前运行内核的构建树路径,由 uname -r 动态获取 |
M=$(PWD) |
告诉内核构建系统在指定目录下查找模块源码 |
常见编译故障:
- 如果
/lib/modules/$(uname -r)/build不存在,说明未安装内核头文件(linux-headers-*包)。 - 必须使用
tab缩进 Makefile 中的命令,不能使用空格。 - 编译时若出现
implicit declaration of function错误,检查是否遗漏了#include <linux/init.h>。
3.4 编译与验证流程
执行以下命令完成编译和加载测试:
$ make
$ sudo insmod empty_module.ko
$ lsmod | grep empty_module
$ sudo rmmod empty_module
$ dmesg | tail -5
由于我们的 init 和 exit 函数体为空,dmesg 中不会出现任何自定义打印信息。但 lsmod 能确认模块已成功加载,rmmod 无报错即表示卸载成功。
3.5 模块生命周期与 init/exit 调用关系
下面的 SVG 图展示了从用户空间命令到内核模块 init/exit 函数的完整调用链:
3.6 验证模块的“零基线”
空模块的价值在于它提供了一个“零基线”——即没有任何副作用的最小可加载单元。在后续章节中,我们将故意在 init 或 exit 中注入故障(如内存泄漏、死锁、空指针解引用),并与这个零基线对比,从而精确识别故障现象。
零基线检查清单:
- ✅
insmod返回 0,无错误信息。 - ✅
lsmod显示模块已加载,Used by为 0。 - ✅
rmmod无输出,无错误。 - ✅
dmesg中无新增的BUG、Oops或Call Trace信息。 - ✅
/proc/modules中模块状态为Live。
3.7 常见编译与加载错误速查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
内核版本不匹配或模块格式错误 | 使用 modinfo empty_module.ko 检查 vermagic |
module_init() 未定义 |
缺少 #include <linux/module.h> |
添加头文件包含 |
__init 属性冲突 |
函数定义与声明不一致 | 确保 static int __init 顺序正确 |
make: *** 没有规则可制作目标 |
Makefile 中 obj-m 拼写错误 |
检查模块名与 .c 文件名一致 |