Day 8–9 · 内核模块基础设施¶
预计时长:1.5 小时 / 天,共 3 小时
类型:实验
做什么¶
写一个最小内核模块,彻底搞清楚模块的生命周期、符号导出、版本控制机制。重点不是 Hello World 本身,而是理解 module_init 背后的机制,以及 WSL2 环境下加载模块的特殊问题。
要了解什么¶
1. module_init / module_exit 的真实机制¶
这两个宏不是函数调用,是声明。展开后大致是:
// module_init 展开(简化)
static initcall_t __initcall_my_init_func __used \
__attribute__((__section__(".initcall6.init"))) = my_init_func;
insmod 时,内核读取模块 ELF 的特定 section,找到 init 函数指针,调用它。rmmod 时调用 exit 函数。如果没有 module_exit,该模块不可卸载。
内建模块(obj-y,编进内核)的 module_init 函数在内核启动时按优先级依次调用,你在 initcall_debug 输出中看到的就是这些。
2. MODULE_LICENSE 的实际作用¶
这不只是声明,有实际效果:
- 只有 GPL 兼容许可证的模块才能访问标记为
EXPORT_SYMBOL_GPL的符号 - 加载非 GPL 模块后,内核会打印
kernel tainted警告,意味着这个内核状态不再"纯洁" - 内核开发者可能拒绝分析被 tainted 内核产生的 bug 报告
大量重要的内核接口(如 usb_register_driver、i2c_add_driver)都是 EXPORT_SYMBOL_GPL,实际上强迫驱动采用 GPL 许可。
3. printk 与 pr_* 系列¶
// 旧式写法(仍然有效)
printk(KERN_DEBUG "value = %d\n", val);
printk(KERN_INFO "module loaded\n");
printk(KERN_ERR "failed: %d\n", ret);
// 现代写法(推荐,自动添加模块名前缀)
pr_debug("value = %d\n", val);
pr_info("module loaded\n");
pr_err("failed: %d\n", ret);
// 在驱动中绑定设备(推荐,带设备路径前缀)
dev_info(&pdev->dev, "probe success\n");
dev_err(&pdev->dev, "failed to request IRQ %d\n", irq);
日志级别(数字越小越紧急):
KERN_EMERG(0) > KERN_ALERT(1) > KERN_CRIT(2) > KERN_ERR(3) > KERN_WARNING(4) > KERN_NOTICE(5) > KERN_INFO(6) > KERN_DEBUG(7)
4. WSL2 加载内核模块的特殊性¶
WSL2 运行的是微软定制内核(5.15.x 或更高),但 WSL2 的头文件可能不完整。你有两个选择:
方案 A(推荐):在 QEMU 虚拟机里加载模块(最接近真实嵌入式场景)
方案 B:使用 WSL2 自身内核头文件加载到 WSL2 内核(测试基本逻辑,但有限制)
方案 B 的头文件获取:
# 查看当前 WSL2 内核版本
uname -r
# 安装对应头文件
sudo apt install linux-headers-$(uname -r)
# 如果找不到,说明 WSL 内核版本太新,改用方案 A
5. modinfo 和 Module.symvers¶
vermagic 字段包含内核版本 + 编译器版本,加载时严格匹配,这就是为什么你不能把 ARM 编译的模块直接 insmod 到 x86 内核。
练习¶
练习 1:最小模块¶
创建 hello.c:
// hello.c — 最小内核模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("你的名字");
MODULE_DESCRIPTION("Hello World kernel module");
MODULE_VERSION("0.1");
// module_param 示例:允许 insmod 时传参
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of greetings (default: 1)");
static int __init hello_init(void)
{
int i;
pr_info("hello: module loaded, count=%d\n", count);
for (i = 0; i < count; i++)
pr_info("hello: greeting %d/%d\n", i + 1, count);
return 0; // 非零返回值会导致 insmod 失败,errno 被设置
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded\n");
// void 返回,不能失败
}
module_init(hello_init);
module_exit(hello_exit);
创建 Makefile(注意缩进必须是 TAB):
# 在目标内核源码树下编译
KDIR ?= ~/kernel/linux-stable
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-
obj-m += hello.o
all:
make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
# 编译
make
# 查看模块信息
arm-linux-gnueabihf-objdump -d hello.ko | head -30
modinfo hello.ko # 注意:这是用宿主机 modinfo 读取,会报 vermagic 不匹配,但能看元信息
练习 2:在 QEMU 中加载模块¶
# 把模块打包进 initramfs
cp ~/labs/hello_module/hello.ko ~/kernel/initramfs/
cd ~/kernel/initramfs
find . | cpio -H newc -o | gzip > ../rootfs.cpio.gz
# 启动 QEMU
cd ~/kernel
qemu-system-arm -M virt -cpu cortex-a7 -m 256M \
-kernel linux-stable/arch/arm/boot/zImage \
-initrd rootfs.cpio.gz \
-append "console=ttyAMA0 rdinit=/init" \
-nographic
在 QEMU 的 shell 里:
# 加载模块
insmod /hello.ko count=3
# 查看日志
dmesg | tail -10
# 查看模块是否在列表里
lsmod
# 带参数查看详情
cat /proc/modules | grep hello
# 卸载
rmmod hello
dmesg | tail -3
- [ ] 验证
count=3参数生效,dmesg 里有 3 条 greeting - [ ] 卸载后,检查 dmesg 是否打印了 exit 消息
练习 3:符号导出实验¶
创建 ~/labs/sym_export/ 目录,写两个模块:
module_a.c(提供符号):
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
int my_add(int a, int b)
{
pr_info("my_add called: %d + %d\n", a, b);
return a + b;
}
EXPORT_SYMBOL_GPL(my_add); // 只有 GPL 模块才能使用
static int __init module_a_init(void) { return 0; }
static void __exit module_a_exit(void) {}
module_init(module_a_init);
module_exit(module_a_exit);
module_b.c(使用符号):
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
extern int my_add(int a, int b); // 声明外部符号
static int __init module_b_init(void)
{
int result = my_add(3, 4);
pr_info("my_add(3, 4) = %d\n", result);
return 0;
}
static void __exit module_b_exit(void) {}
module_init(module_b_init);
module_exit(module_b_exit);
- [ ] 先 insmod module_a.ko,再 insmod module_b.ko,观察结果
- [ ] 尝试先 insmod module_b.ko(不加载 a),看报什么错
- [ ] 把 module_b 的 LICENSE 改成
"Proprietary",重新编译加载,看内核的 tainted 警告
延伸阅读¶
| 资料 | 具体位置 | 说明 |
|---|---|---|
| 《Linux 设备驱动开发详解》宋宝华 | 第 2 章 | 内核模块基础,中文写得很清楚 |
| Linux Device Drivers Corbet 等 | Ch.2 "Building and Running Modules" | LDD3,免费在线:https://lwn.net/Kernel/LDD3/ |
| Linux Device Driver Development John Madieu | Ch.2 | 更新的实践,基于 5.x 内核 |
| 内核源码 | include/linux/module.h |
MODULE_* 宏的定义,必读 |
| 内核文档 | Documentation/kbuild/modules.rst |
模块编译的官方说明 |