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 等)架构不同时,必须使用交叉编译工具链。配置过程分为三步:

  1. 安装工具链:从 Linaro、ARM 官网或芯片厂商获取预编译工具链,解压到 /opt/toolchains/ 目录。
  2. 设置环境变量:将工具链的 bin 目录加入 PATH,并导出 CROSS_COMPILE 变量。
  3. 验证工具链:运行 ${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

下图展示了从源码到可加载模块的完整编译流程,以及交叉编译与本地编译的路径差异:

内核模块编译流程(本地 vs 交叉编译) 源文件 (.c / .h) 内核源码树 (KDIR) Makefile make -C $(KDIR) M=$(PWD) modules ARCH 与 CROSS_COMPILE 检查 本地编译 使用主机 gcc 交叉编译 使用 ${CROSS_COMPILE}gcc 模块文件 (.ko) Module.symvers 自动生成 交叉编译时,KDIR 必须指向目标架构的内核源码树,且已执行过 make modules_prepare

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)可输出完整的编译命令,便于定位头文件路径或编译选项问题。