2. 内核模块开发环境搭建
字符设备驱动的开发离不开一个完整、可编译的内核源码树以及正确的编译工具链。本章将系统讲解如何获取内核源码、配置交叉编译环境,并编写符合内核模块规范的 Makefile,为后续的驱动代码编写与加载测试奠定基础。
2.1 内核源码树获取
内核模块必须与目标系统运行的内核版本严格匹配,否则无法加载。获取内核源码树有三种主流方式:
| 方式 | 适用场景 | 命令/操作示例 | 注意事项 |
|---|---|---|---|
| 发行版源码包 | 开发机与目标机架构相同 | apt-get install linux-source-$(uname -r) |
需手动解压并配置 |
| 内核主线源码 | 需要最新特性或定制内核 | git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git |
版本号需与目标内核一致 |
| 交叉编译头文件包 | 嵌入式目标板(ARM/RISC-V) | 从板卡厂商 BSP 中获取 | 必须包含 Module.symvers |
提示: 对于嵌入式开发,强烈建议直接从板卡厂商提供的 BSP(Board Support Package)中提取内核源码树,因为其中已包含针对特定硬件的补丁和配置文件,可避免大量移植工作。
2.2 交叉编译工具链配置
当开发主机(x86_64)与目标板(ARM64、RISC-V 等)架构不同时,必须使用交叉编译工具链。配置过程分为三步:
- 安装工具链:从 Linaro、ARM 官网或芯片厂商获取预编译工具链,解压到
/opt/toolchains/目录。 - 设置环境变量:将工具链的
bin目录加入PATH,并导出CROSS_COMPILE变量。 - 验证工具链:运行
${CROSS_COMPILE}gcc --version确认版本。
# 以 ARM64 (aarch64) 为例
export PATH=/opt/toolchains/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH
export CROSS_COMPILE=aarch64-none-linux-gnu-
export ARCH=arm64
# 验证
${CROSS_COMPILE}gcc --version
常见错误: 忘记设置
ARCH 环境变量会导致内核构建系统使用主机架构的头文件,编译出的模块在目标板上无法加载,错误信息通常为 Invalid module format。
2.3 Makefile 编写基础
内核模块的 Makefile 与普通用户态程序的 Makefile 有本质区别。它必须使用内核构建系统(Kbuild)提供的框架,通过 obj-m 变量指定要编译的模块目标。
2.3.1 单源文件模块 Makefile
# Makefile for a single-source character device driver
obj-m := chardev.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
2.3.2 多源文件模块 Makefile
当驱动由多个 .c 文件组成时,需要使用 模块名-objs 语法:
# Makefile for multi-source char driver
obj-m := mydriver.o
mydriver-objs := main.o ioctl_ops.o fops.o
KDIR := /path/to/kernel/source
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
2.3.3 交叉编译 Makefile 模板
# Cross-compilation Makefile for ARM64 target
obj-m := chardev.o
# 指定目标内核源码树路径
KDIR := /home/user/linux-5.15.0-arm64
PWD := $(shell pwd)
# 交叉编译环境变量
export ARCH=arm64
export CROSS_COMPILE=aarch64-none-linux-gnu-
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
关键变量说明:
obj-m:指定要编译成模块的目标文件(不含.ko后缀)。KDIR:指向已配置好的内核源码树根目录,必须包含Module.symvers和Makefile。M=$(PWD):告诉 Kbuild 在PWD目录下查找源文件并输出编译结果。-C:切换到内核源码目录执行其顶层 Makefile。
2.4 内核模块编译流程 SVG
下图展示了从源码到可加载模块的完整编译流程,以及交叉编译与本地编译的路径差异:
2.5 内核模块与用户程序 Makefile 对比
| 对比项 | 内核模块 Makefile | 用户程序 Makefile |
|---|---|---|
| 编译器 | 由内核构建系统调用 $(CROSS_COMPILE)gcc |
直接使用 gcc 或 $(CC) |
| 链接方式 | 不链接标准 C 库,使用内核导出符号 | 链接 glibc / musl 等用户态库 |
| 编译选项 | 由 Kbuild 自动添加 -D__KERNEL__、-DMODULE 等 |
开发者手动指定 -I、-L、-l |
| 输出文件 | .ko(内核对象文件) |
ELF 可执行文件或 .so 动态库 |
| 安装方式 | insmod / modprobe |
直接执行或 ldconfig |
2.6 常见编译错误与排查
- “No such file or directory”:
KDIR路径错误,或内核源码未执行make modules_prepare。 - “Invalid module format”:模块编译时的内核版本与运行内核版本不一致,检查
uname -r与KDIR/include/generated/uapi/linux/version.h中的版本宏。 - “Unknown symbol”:模块引用了未导出的内核符号,需在源码中
EXPORT_SYMBOL或检查Module.symvers。 - “Makefile: *** missing separator”:Makefile 中使用了空格而非 Tab 缩进,内核构建系统对缩进敏感。
调试技巧: 在 Makefile 中添加
V=1 参数(如 $(MAKE) -C $(KDIR) M=$(PWD) V=1 modules)可输出完整的编译命令,便于定位头文件路径或编译选项问题。