3. 内核模块开发入门
内核模块是Android内核可动态加载的代码单元,它运行在内核态,拥有与内核核心代码相同的特权。与用户态程序不同,内核模块没有main函数,也不能调用标准C库;它通过模块初始化函数和模块退出函数向内核注册自己的生命周期入口。本章从最小可运行模块出发,串联Makefile编写、编译、加载与卸载的完整闭环,为后续签名与加载工程打下基础。
3.1 核心原理:模块的生命周期与入口
一个内核模块在编译后得到.ko(Kernel Object)文件。加载时,内核调用模块的初始化函数;卸载时,调用退出函数。这两个函数通过宏module_init()和module_exit()注册。模块还必须声明许可证,否则内核会标记为“污染”(tainted),在Android的GKI(Generic Kernel Image)环境下可能直接拒绝加载。
模块代码中常用的头文件包括<linux/module.h>、<linux/kernel.h>和<linux/init.h>。打印日志使用pr_info()或printk(),日志会进入内核环形缓冲区,可通过dmesg查看。注意:内核模块中不能使用printf,因为用户态C库不可用。
3.2 编写第一个Hello World模块
下面是最小可运行模块的完整代码。它只做两件事:加载时打印一条信息,卸载时打印另一条信息。
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Android Kernel Course");
MODULE_DESCRIPTION("A minimal Hello World kernel module");
static int __init hello_init(void)
{
pr_info("hello: module loaded, Hello World!\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded, Goodbye!\n");
}
module_init(hello_init);
module_exit(hello_exit);
其中__init和__exit是内核标记宏,用于把函数放入特定的内存段,加载完成后初始化段可被释放,从而节省内存。返回值0表示初始化成功,非零表示失败,内核会回滚加载。
3.3 Makefile编写:连接源码与内核构建系统
内核模块不能像普通程序那样直接调用gcc编译,必须借助内核源码树中的Kbuild系统。Makefile的核心是obj-m变量和M=参数。
# Makefile
obj-m += hello.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 += hello.o告诉Kbuild把hello.c编译为可加载模块;-C $(KDIR)切换到内核构建目录,读取内核的顶层Makefile和配置;M=$(PWD)告诉Kbuild回到当前目录处理外部模块。Android内核开发中,KDIR通常指向AOSP内核源码的out/.../kernel或预编译内核头文件目录,而不是宿主机的/lib/modules。
ARCH=arm64和CROSS_COMPILE=aarch64-linux-android-,并确保使用的内核源码与目标设备内核版本、配置一致,否则加载时会出现“version magic”不匹配。
3.4 编译、加载与卸载流程
完整操作流程如下:
- 在模块目录执行
make,生成hello.ko。 - 使用
insmod hello.ko加载模块,或使用modprobe(需模块位于标准路径)。 - 执行
dmesg | tail查看加载日志。 - 使用
lsmod | grep hello确认模块已在内核中。 - 使用
rmmod hello卸载模块,再次查看日志。
$ make
$ sudo insmod hello.ko
$ dmesg | tail -n 3
[ 1024.123456] hello: module loaded, Hello World!
$ lsmod | grep hello
hello 16384 0
$ sudo rmmod hello
$ dmesg | tail -n 3
[ 1030.654321] hello: module unloaded, Goodbye!
在Android设备上,通常通过adb push把.ko推送到/data/local/tmp,再以root权限执行insmod。但Android 11及以上对模块加载有严格限制,未签名或签名不匹配的模块会被内核拒绝,这正是后续章节要解决的核心问题。
3.5 常见误区与复盘
初学者在第一个模块上最容易踩的坑集中在以下几类:
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| insmod 报 “Invalid module format” | 内核版本或配置不匹配,version magic 不一致 | 对比 uname -r 与编译内核版本,检查 vermagic |
| insmod 报 “Operation not permitted” | Android 未关闭模块签名校验,或 SELinux 限制 | 查看 dmesg 中的签名拒绝日志,确认 GKI 策略 |
| dmesg 无输出 | 日志级别过低,或 pr_info 被过滤 | 使用 dmesg -w 实时观察,检查 printk 级别 |
| rmmod 报 “Module is in use” | 模块引用计数不为零,或退出函数未正确注册 | 检查 lsmod 第三列计数,确认 module_exit 存在 |
| 编译报 “No such file or directory” | KDIR 指向错误,内核头文件未准备 | 确认内核源码已编译,build 链接存在 |
复盘本章:Hello World模块虽小,却完整覆盖了内核模块开发的四个关键动作——写入口、配Makefile、编译成.ko、加载并验证。掌握这条最小闭环后,后续任何复杂模块都只是在这个骨架上增加功能代码和依赖声明。真正需要长期关注的是内核版本一致性和签名校验,它们决定了模块能否从“编译成功”走到“加载成功”。