3. 开发环境搭建
字符设备驱动的开发与普通用户态程序不同,它需要一套专门的交叉编译环境、匹配的内核源码树以及精确的模块编译规则。本章将手把手搭建完整的开发环境,确保后续编写的驱动模块能够正确编译并加载到目标硬件平台。
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/uclibc | arm-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 开发环境架构总览
下图展示了宿主机、内核源码树、交叉工具链与目标设备之间的协作关系:
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,避免每次打开终端重复设置。