2. 内核模块基础
说实话,内核模块开发的第一步,往往就把不少人卡住了。不是代码多难写,而是环境没搭对。我见过太多人花了一整天装包,结果编译出来跑不了——说白了,就是交叉编译链和内核源码树没对齐。
这一章,我就带你把这套环境理清楚。咱们的目标很明确:能在你的开发机上编译出能在目标设备上运行的内核模块。不管是树莓派、ARM 开发板,还是某个工控设备,流程都一样。
2.1 交叉编译:为什么需要它?
你的开发机通常是 x86_64 架构,但目标设备可能是 ARM、RISC-V 或 MIPS。x86 上 gcc 编译出来的二进制,ARM 根本跑不了。所以我们需要交叉编译器——在 x86 上运行,但生成 ARM 指令集的工具链。
核心概念:交叉编译 = 在宿主机(Host)上编译出目标机(Target)可执行的代码。
我个人习惯把工具链放在 /opt/toolchains/ 下,方便管理。不同厂商的工具链命名规则不太一样,但核心文件就这几个:
| 工具 | 典型名称(ARM 为例) | 作用 |
|---|---|---|
| C 编译器 | arm-linux-gnueabihf-gcc |
编译 C 源码 |
| C++ 编译器 | arm-linux-gnueabihf-g++ |
编译 C++ 源码 |
| 链接器 | arm-linux-gnueabihf-ld |
链接目标文件 |
| 汇编器 | arm-linux-gnueabihf-as |
汇编源码 |
| 归档工具 | arm-linux-gnueabihf-ar |
创建静态库 |
| 二进制工具 | arm-linux-gnueabihf-objdump |
反汇编、查看段信息 |
小技巧:用 which arm-linux-gnueabihf-gcc 检查工具链是否在 PATH 里。如果找不到,记得把 /opt/toolchains/xxx/bin 加到 ~/.bashrc 的 export PATH 里。
2.2 配置内核源码树
内核模块不是随便编译的。它需要依赖内核源码树里的头文件和 Makefile 规则。说白了,你得有一份与目标设备上运行的内核版本完全一致的源码。
为什么必须一致?因为内核内部的数据结构、函数签名、宏定义,每个版本都可能变。你拿 5.10 的源码编译模块,插到 5.15 的内核里,大概率 insmod 就报 Invalid module format。
我踩过这个坑。有一次客户现场升级了内核,我没同步更新源码树,结果模块加载直接 panic。嗯,从那以后我养成了习惯:每次部署前先 uname -r 确认版本。
2.2.1 获取内核源码
有两种方式:
- 从内核官网下载:
kernel.org上找到对应版本,解压到/usr/src/或自定义目录。 - 从设备厂商获取:很多嵌入式板子(比如树莓派、NXP i.MX)会提供修改过的内核源码,带自己的驱动补丁。建议优先用厂商的。
解压后,进入源码目录,先做一次配置:
# 假设目标设备的内核配置已导出为 .config 文件
# 你可以从设备上拷贝 /proc/config.gz 解压得到
zcat /proc/config.gz > .config
# 或者使用默认配置(不一定匹配)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
# 然后编译内核头文件和模块相关目标
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare
注意:modules_prepare 这一步很关键。它不会编译整个内核,但会生成 Module.symvers 和必要的头文件。没有这个文件,你的模块编译时会找不到符号。
2.2.2 设置内核源码树路径
编译模块时,Makefile 需要知道内核源码树在哪。通常通过 KERNELDIR 变量指定:
# 在你的模块 Makefile 中
KERNELDIR ?= /home/user/linux-5.10.100
# 或者通过环境变量
export KERNELDIR=/home/user/linux-5.10.100
我个人习惯在 ~/.bashrc 里加一个别名,省得每次敲路径:
export KERNEL_SRC=/opt/kernel/linux-5.10.100
2.3 安装必要工具链
除了交叉编译器,你还需要一些辅助工具。别小看它们,少了哪个都可能卡住。
| 工具 | 安装方式(Ubuntu/Debian) | 用途 |
|---|---|---|
build-essential |
apt install build-essential |
基础编译工具(gcc, make, ld 等) |
libncurses-dev |
apt install libncurses-dev |
menuconfig 图形配置界面依赖 |
bc |
apt install bc |
内核编译脚本中用到的基本计算器 |
flex / bison |
apt install flex bison |
内核解析器生成工具 |
elfutils |
apt install elfutils |
处理 ELF 文件,调试符号相关 |
openssl-dev |
apt install libssl-dev |
内核模块签名功能需要 |
避坑指南:我曾经在编译 5.10 内核时,因为没装 elfutils,modules_prepare 一直报 Cannot generate ORC metadata。装完就好了。所以建议一次性把这些都装上。
2.4 验证环境:编译一个空模块
环境搭没搭好,跑一个最小模块就知道了。写一个最简单的 hello.c:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, kernel module!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple test module");
对应的 Makefile:
obj-m := hello.o
KERNELDIR ?= /path/to/your/kernel/source
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
执行 make,如果看到生成了 hello.ko,恭喜你,环境通了。
验证命令:file hello.ko 应该显示 ELF 32-bit LSB relocatable, ARM 或类似架构信息。
2.5 整体流程一览
下面这张图把整个搭建流程串起来了。你跟着箭头走一遍,心里就有谱了。
2.6 常见问题与排查
环境搭建过程中,有几个问题几乎每个人都会遇到。我列出来,你遇到了直接对照查:
- “找不到
” :说明KERNELDIR没设对,或者内核源码没执行modules_prepare。 - “Invalid module format”:内核版本不匹配,或者
Module.symvers缺失。检查uname -r和源码版本。 - “/lib/modules/xxx 不存在”:目标设备上没装内核模块目录。用
make modules_install安装,或者手动拷贝.ko文件。 - “relocation truncated to fit”:链接时地址空间不够。检查内核配置中
CONFIG_ARM_MODULE_PLTS是否开启。
重要提醒:不要在宿主机上直接 insmod 交叉编译的模块。x86 内核加载 ARM 模块会直接报错,甚至可能让宿主机内核 panic。我见过有人把开发板上的模块拷到 PC 上试,结果系统直接挂了。
好了,环境搭到这里,你已经可以编译出能在目标设备上运行的 .ko 文件了。下一件事,就是把它加载进去,看看能不能正常工作。不过那是后面章节的内容了,咱们先把基础打牢。