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 消息在内核日志中的传递路径。

内核模块加载/卸载与日志流 内核模块加载/卸载流程与 printk 日志路径 insmod hello.ko rmmod hello module_init(hello_init) module_exit(hello_exit) printk → 内核日志缓冲区 printk → 内核日志缓冲区 dmesg | tail -5 ⚠️ 必须使用 root 权限 insmod/rmmod 需要 sudo

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 是验证模块元数据和版本兼容性的第一工具。