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 函数的完整调用链:

最小内核模块 init/exit 调用流程 模块加载/卸载与 init/exit 调用关系 insmod rmmod sys_init_module sys_delete_module do_init_module free_module empty_init() empty_exit() 调试关注点 • 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 文件名一致