本节摘要:构建是驱动的第一道交付工序。本节从 2.1 节的最小构建升级到体系化:配置裁剪机制让驱动可按需编入或外挂、交叉编译的完整参数链、产物清单与构建可复现性,把"能编过"升级为"编得可管"。
2.1 节那个十行的构建文件解决了"从零到能跑",但产品化的问题它没碰:一个产品线五种硬件配置,LED 驱动在低端版要不要编进去?客户要求的安全审查要构建产物可复现怎么保证?交付给客户的模块要不要带调试信息?这些问题的答案共同构成"构建体系"。
内核的配置系统由两部分配合:配置描述文件声明"有哪些开关、开关之间什么关系",构建规则文件声明"开关各对应哪些产物"。
配置描述(放进内核源码树相应目录):
config LEDS_BOARD tristate "评估板 LED 驱动" depends on HAS_IOMEM help 评估开发板上的 LED 驱动,提供字符设备接口与闪烁控制。 编为内建则随内核启动加载;编为模块则按需装入; 不选则完全不参与编译。 config LEDS_BOARD_DEBUG bool "评估板 LED 驱动调试日志" depends on LEDS_BOARD help 打开后会输出逐次调用的详细日志,性能有损,仅供调试。
两行关键字各有含义。tristate 是三态开关:不编、编为模块、编进内核——产品量产版通常把基础驱动编进内核(少一个交付文件),开发版用模块(改起来快)。depends on 声明依赖:没有内存映射能力的平台(某些极简虚拟环境)根本不该看到这个选项。调试开关独立成项是个值得养成的习惯:量产与调试共用一套源码,差异收敛到一个布尔开关。
构建规则随之升级:
# 内核构建器视角 obj-$(CONFIG_LEDS_BOARD) += board-led.o board-led-y := core.o sysfs.o board-led-$(CONFIG_LEDS_BOARD_DEBUG) += debug.o
obj- 变量按配置值展开(空则不编、m 则模块、y 则内建);一个驱动拆多个源文件、调试代码独立文件按需编入——配置系统直接指挥构建系统,这是内核构建体系的核心设计。
嵌入式驱动的编译永远在"另一台机器"上。交叉编译的完整参数链有五环,环环都错不得:
ARCH=arm64 # 目标架构:决定头文件与汇编约定 CROSS_COMPILE=aarch64-linux- # 工具链前缀:编译器、汇编器、链接器全家 KERNELDIR=/path/to/target-kernel # 目标内核源码树:版本必须与目标板一致 INSTALL_MOD_PATH=./out # 模块安装根:模块安装后的存放地 -j$(nproc) # 并行加速
组装成一条可复用的构建命令:
make -C "$KERNELDIR" M="$PWD" \ ARCH=arm64 CROSS_COMPILE=aarch64-linux- \ modules # 产出 board-led.ko make -C "$KERNELDIR" M="$PWD" \ ARCH=arm64 CROSS_COMPILE=aarch64-linux- \ INSTALL_MOD_PATH=./out modules_install # 产物落入 out/lib/modules/内核版本/ 目录,随包分发
五环里最常见的翻车是内核树不匹配(2.1 节的版本魔术问题)与工具链版本漂移——同一份代码今天能编过明天报错,先查工具链有没有被全局更新。工程做法是把工具链版本写进构建脚本的检查逻辑,版本不符直接拒绝构建。
一个负责任的驱动交付包里,模块文件只是主角之一,配套物一样不能少:
| 产物 | 用途 | 缺失的后果 |
|---|---|---|
| 模块二进制 | 本体 | — |
| 模块签名(如平台要求) | 安全启动链校验 | 目标设备拒绝装入 |
| 符号与调试信息文件 | 7.4 节的 oops 翻译链 | 现场崩溃无法定位 |
| 设备树覆盖片段 | 板级描述的增量交付 | 设备不出现,静默失败 |
| 版本与变更说明 | 用户与运维的知情权 | 升级事故与回滚无据 |
| 构建清单(哈希值) | 可复现性验证 | 无法证明两次构建一致 |
其中调试信息文件要在构建时显式保留(默认发布构建常剥离它),并与模块成对归档——模块与调试信息版本错位,翻译出来的行号就是误导。
可复现性指"相同源码、相同工具链、相同配置,产出比特级一致的二进制"。它的敌人是构建过程中混入的环境变量量:时间戳取构建时刻、路径记录了构建者的目录结构、并行顺序影响未定初始化。治理手段:构建脚本固定环境变量、路径统一用相对形式、时间戳统一取版本控制提交时刻。做到可复现的构建才能谈"安全审计"与"供应链可信"——客户拿到产物能自行验证"这确实是从那版源码编出来的"。
⚠️ 常见坑:在源码目录直接构建,产物与源码混放。正确姿势是外部构建——源码目录只读,构建在独立的输出目录进行,一份源码可以同时服务多个目标配置(内建版、模块版、调试版各一个输出目录互不污染)。
把全节要点串成一次真实会话,照着走一遍就能验证自己的构建体系:
# 一 配置确认:目标内核的配置与树是否齐备 $ ls /path/to/target-kernel/.config && echo "内核树就绪" # 二 构建:五环参数一次配齐 $ make -C /path/to/target-kernel M=$PWD \ ARCH=arm64 CROSS_COMPILE=aarch64-linux- modules CC [M] board-led.o LD [M] board-led.ko # 三 安装与归档:模块入安装根,翻译链成套入库 $ make -C /path/to/target-kernel M=$PWD ARCH=arm64 \ CROSS_COMPILE=aarch64-linux- INSTALL_MOD_PATH=./out modules_install $ cp board-led.ko board-led.debug ./out/ $ md5sum ./out/board-led.ko >> build-artifacts.md5 # 四 自检:产物与模块信息核对 $ modinfo board-led.ko | head -4 filename: board-led.ko license: GPL depends: name: board_led
四步会话里有两处容易被忽略的动作:调试信息文件与模块一起拷入交付目录(第 7.4 节翻译链的原料)、产物哈希登记(可复现性的凭证)。构建会话脚本化后,它本身就是构建文档——新人拿到脚本就能产出与你比特级一致的模块。
构建体系立起来,驱动就有了批量生产的能力。下一节面对一个更长期的问题:内核每几个月就演进一次,驱动怎么跨版本活下去。