3. 第一个内核模块:编写 Hello World 模块、Makefile 编写、模块加载与卸载
本章是内核模块开发的起点。我们将从零构建一个最简单的 Hello World 内核模块,掌握 Makefile 编写、模块加载/卸载 以及 基本故障排查。通过这个最小可运行示例,你将理解内核模块的生命周期与内核日志系统。
3.1 Hello World 模块源码结构
一个标准的内核模块至少包含两个宏:module_init() 和 module_exit(),分别指定模块加载和卸载时的入口函数。此外,还需要 MODULE_LICENSE() 声明许可证,否则内核会报“tainted”警告。
关键点: 内核模块运行在内核空间,不能链接标准 C 库(如 glibc),只能使用内核导出的 API(如
printk)。printk 是内核版的 printf,输出到内核日志缓冲区,可通过 dmesg 查看。
/* hello.c - 第一个内核模块 */
#include <linux/module.h> /* 所有模块都需要 */
#include <linux/kernel.h> /* printk 所需 */
#include <linux/init.h> /* __init / __exit 宏 */
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World from kernel module!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0; /* 0 表示加载成功,负数表示失败 */
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
| 元素 | 作用 | 说明 |
|---|---|---|
__init |
标记初始化函数 | 加载完成后内核可释放该函数占用的内存 |
__exit |
标记退出函数 | 模块卸载时调用;若模块编译进内核,此函数会被忽略 |
KERN_INFO |
日志级别 | 表示信息性消息,还有其他级别如 KERN_ERR、KERN_WARNING |
MODULE_LICENSE |
许可证声明 | 不声明或非 GPL 兼容许可证会导致内核标记为“污染” |
3.2 Makefile 编写:与内核构建系统对接
内核模块的编译必须依赖内核源码树(或内核头文件包)提供的 Kbuild 构建系统。Makefile 的核心是调用 make -C /lib/modules/$(uname -r)/build M=$(PWD) modules。
提示: 确保已安装内核头文件:
sudo apt install linux-headers-$(uname -r)(Debian/Ubuntu)或 sudo yum install kernel-devel(RHEL/CentOS)。
# Makefile - 编译 hello.ko
obj-m := hello.o # 目标模块名(不含 .ko)
# 当前路径
PWD := $(shell pwd)
# 内核源码树路径(自动匹配当前运行内核)
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
| 变量/目标 | 含义 |
|---|---|
obj-m |
指定要编译为可加载模块的目标文件(.o) |
KDIR |
当前运行内核的构建目录,包含 Makefile、Kconfig 等 |
M=$(PWD) |
告诉 Kbuild 在指定目录下查找模块源码 |
modules |
Kbuild 目标,编译所有 obj-m 指定的模块 |
clean |
清理编译产物(.o、.ko、.mod.c 等) |
3.3 模块加载与卸载操作
编译成功后生成 hello.ko,使用 insmod 加载、rmmod 卸载,并用 lsmod 查看已加载模块列表。内核日志通过 dmesg 查看。
注意: 不要使用
modprobe 加载本地模块,除非已将模块安装到标准路径(/lib/modules/$(uname -r)/)。modprobe 会自动处理依赖,但要求模块在 depmod 数据库中注册。
# 编译模块
make
# 加载模块(需要 root 权限)
sudo insmod hello.ko
# 查看已加载模块(确认 hello 出现在列表中)
lsmod | grep hello
# 查看内核日志(应看到 Hello World 消息)
dmesg | tail -5
# 卸载模块
sudo rmmod hello
# 再次查看日志(应看到 Goodbye 消息)
dmesg | tail -5
3.4 模块生命周期与内核日志流
下图展示了从 insmod 到 rmmod 的完整流程,以及 printk 消息在内核日志中的传递路径。
3.5 常见故障与排查方法
新手在加载第一个模块时经常遇到以下问题。掌握这些排查手段能快速定位问题。
| 错误现象 | 可能原因 | 排查命令/方法 |
|---|---|---|
insmod: ERROR: could not insert module hello.ko: Operation not permitted |
未使用 root 权限 | 使用 sudo insmod 或以 root 身份执行 |
insmod: ERROR: could not insert module hello.ko: Invalid module format |
内核版本不匹配或架构不一致 | 检查 uname -r 与 KDIR 是否对应;确认 Makefile 中 KDIR 路径正确 |
insmod: ERROR: could not insert module hello.ko: Unknown symbol in module |
模块引用了未导出的内核符号 | 使用 nm hello.ko 查看未定义符号;检查是否缺少 EXPORT_SYMBOL |
加载后 dmesg 无输出 |
日志级别过滤或 printk 未调用 |
检查 cat /proc/sys/kernel/printk;确认 hello_init 被正确调用 |
rmmod: ERROR: Module hello is not currently loaded |
模块未加载或已卸载 | 使用 lsmod | grep hello 确认状态 |
make: *** /lib/modules/.../build: No such file or directory |
未安装内核头文件 | 安装 linux-headers-$(uname -r) 或 kernel-devel |
调试技巧: 在
hello_init() 中故意返回 -ENOMEM(内存不足错误),观察 insmod 输出和 dmesg 中的错误信息。这是理解内核错误码传递机制的最佳实践。
3.6 模块信息查看与版本校验
编译后的 .ko 文件包含模块信息和版本魔术字。使用 modinfo 查看模块元数据,使用 modprobe --show-depends 查看依赖关系。
# 查看模块详细信息
modinfo hello.ko
# 输出示例:
# filename: /home/user/hello/hello.ko
# license: GPL
# author: Your Name
# description: A simple Hello World kernel module
# depends:
# vermagic: 5.15.0-91-generic SMP mod_unload modversions
# 查看内核版本魔术字是否匹配
modprobe --show-depends hello.ko
vermagic 字段必须与当前运行内核的版本和配置完全一致,否则模块无法加载。这是内核模块安全机制的一部分,防止二进制不兼容的模块被注入内核。
3.7 本章小结
- 内核模块使用
module_init/module_exit定义生命周期入口。 - Makefile 通过
obj-m和KDIR对接 Kbuild 系统。 insmod加载、rmmod卸载、lsmod查看、dmesg查看日志。- 常见错误集中在权限、版本匹配、符号缺失和头文件安装。
modinfo是验证模块元数据和版本兼容性的第一工具。