3. 开发环境搭建:交叉编译工具链、内核源码树配置、模块编译 Makefile 编写

字符设备驱动的开发与普通用户态程序不同,它需要一套专门的交叉编译环境、匹配的内核源码树以及精确的模块编译规则。本章将手把手搭建完整的开发环境,确保后续编写的驱动模块能够正确编译并加载到目标硬件平台。

3.1 交叉编译工具链

嵌入式设备通常资源受限(如 ARM、RISC-V 架构),无法直接在设备上运行 gcc 编译大型内核模块。因此需要在性能强大的宿主机(x86_64 PC)上安装交叉编译工具链,生成目标架构的可执行代码。

3.1.1 工具链组成

组件说明典型名称(ARM 为例)
编译器将 C 源码编译为目标架构汇编arm-linux-gnueabihf-gcc
汇编器将汇编转为机器码arm-linux-gnueabihf-as
链接器链接目标文件与库arm-linux-gnueabihf-ld
二进制工具objdump、objcopy、strip 等arm-linux-gnueabihf-objdump
C 库目标架构的 glibc/uclibcarm-linux-gnueabihf 库目录

3.1.2 安装交叉工具链

推荐使用 Linaro 或芯片厂商提供的预编译工具链。以 ARM Cortex-A7 为例:

# 下载并解压 ARM 交叉工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 添加至 PATH
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证安装
arm-linux-gnueabihf-gcc --version
提示:工具链版本必须与目标内核版本匹配。建议使用与目标 Linux 内核同一时期的 Linaro 或 Linus 官方工具链,避免 ABI 不兼容。

3.2 内核源码树配置

内核模块编译不能直接使用宿主机 /usr/include/linux 头文件,必须依赖完整的内核源码树,且该源码树必须经过配置并生成 Module.symvers 和 include/generated/ 等编译产物。

3.2.1 获取内核源码

从内核官网或芯片厂商 BSP 获取源码:

# 从 kernel.org 下载主线内核
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.92.tar.xz
tar -xvf linux-5.15.92.tar.xz -C /home/developer/kernel/
cd /home/developer/kernel/linux-5.15.92

3.2.2 配置内核

使用目标平台的默认配置作为起点:

# 以 ARM 多平台 defconfig 为例
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- multi_v7_defconfig

# 或使用芯片厂商提供的配置文件
cp /path/to/vendor_defconfig .config
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- olddefconfig

关键配置项检查:

  • CONFIG_MODULES=y — 启用模块支持
  • CONFIG_MODULE_UNLOAD=y — 允许卸载模块
  • CONFIG_MODVERSIONS=y — 模块版本控制(建议开启)

3.2.3 编译内核与模块依赖

# 编译内核(仅需生成必要头文件和符号)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage

# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 编译模块(生成 Module.symvers)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
注意:如果只编译模块而不先编译一次内核,Module.symvers 文件可能缺失,导致模块导出符号无法正确解析。务必先执行 make modules 或至少 make prepare + make scripts。

3.3 模块编译 Makefile 编写

内核模块的 Makefile 遵循 Kbuild 系统规则,与普通应用 Makefile 有显著区别。下面给出一个标准模板。

3.3.1 标准 Makefile 模板

# Makefile — 字符设备驱动模块编译

# 目标模块名称(不带 .ko)
obj-m := char_driver.o

# 如果模块由多个源文件组成,使用模块名-objs
# char_driver-objs := main.o ioctl.o

# 内核源码树路径(必须指向已配置的内核目录)
KDIR := /home/developer/kernel/linux-5.15.92

# 当前目录
PWD := $(shell pwd)

# 交叉编译工具链前缀
CROSS_COMPILE := arm-linux-gnueabihf-

# 目标架构
ARCH := arm

all:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean

install:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules_install

3.3.2 Makefile 关键变量说明

变量作用示例值
obj-m指定要编译的模块目标文件char_driver.o
KDIR内核源码树绝对路径/home/.../linux-5.15.92
M=$(PWD)告诉 Kbuild 进入当前目录编译—
ARCH目标 CPU 架构arm
CROSS_COMPILE交叉工具链前缀arm-linux-gnueabihf-

3.3.3 编译与验证

# 编译模块
make

# 检查生成的 .ko 文件
file char_driver.ko
# 输出应包含 "ELF 32-bit LSB relocatable, ARM"

# 查看模块符号信息
arm-linux-gnueabihf-objdump -t char_driver.ko | head -20
常见错误:
  • *** No rule to make target 'arch/arm/tools/gen-mach-types' — 内核源码未配置,执行 make ARCH=arm ... defconfig。
  • Cannot use CONFIG_CC_STACKPROTECTOR_STRONG — 工具链不支持栈保护,在内核配置中关闭该选项。
  • Module.symvers missing — 先在内核根目录执行 make modules_prepare。

3.4 开发环境架构总览

下图展示了宿主机、内核源码树、交叉工具链与目标设备之间的协作关系:

交叉编译环境架构图 交叉编译环境架构 宿主机 (x86_64 Ubuntu) 交叉工具链 内核源码树 模块 Makefile char_driver.ko 目标设备 (ARM Cortex-A7) Linux 内核 insmod 加载 /dev/char_dev scp / nfs 关键路径 源码 → 交叉编译 → .ko → 目标板 → insmod

3.5 环境验证清单

完成上述步骤后,使用以下清单确认环境就绪:

检查项验证命令预期结果
交叉编译器可用arm-linux-gnueabihf-gcc --version显示版本信息,无错误
内核源码已配置ls -l $(KDIR)/include/generated/目录存在且包含 autoconf.h
Module.symvers 存在ls -l $(KDIR)/Module.symvers文件存在且非空
Makefile 语法正确make -n (dry-run)显示 Kbuild 调用命令,无报错
编译产物架构正确file char_driver.ko显示 ARM 架构 ELF 文件
最佳实践:将交叉工具链路径、内核源码路径、架构变量写入 ~/.bashrc 或项目根目录的 env.sh,避免每次打开终端重复设置。