3. 内核模块与内核内置驱动
在 Android 内核驱动移植中,驱动代码既可以编译为内核模块(.ko),也可以直接内置(built-in)到内核镜像中。两种方式在加载时机、符号解析、依赖管理以及调试灵活性上存在显著差异。不同内核版本(如 Linux 4.14、4.19、5.4、5.10、5.15、6.1)对模块加载机制、符号导出规则和依赖关系的处理持续演进,直接影响到驱动在不同版本间的兼容性。
核心观点:模块化驱动在跨版本移植时,最大的陷阱并非代码语法,而是模块加载器行为变化、符号版本控制(CRC)机制以及内核符号导出白名单策略的差异。理解这些底层机制,是避免“模块加载成功但功能异常”或“insmod 报错 Unknown symbol”的关键。
3.1 模块加载机制的内核版本差异
内核模块的加载流程涉及模块二进制格式解析、符号解析与重定位、模块依赖加载、模块初始化函数调用等步骤。不同内核版本在这些环节引入了关键变化:
| 内核版本 | 模块加载器变化 | 关键影响 |
|---|---|---|
| Linux 4.x 系列 | 基于 load_module() 的传统加载路径,支持 MODULE_SIG 签名验证 |
模块需与内核版本严格匹配的 vermagic;签名验证可选 |
| Linux 5.4+ | 引入 module_layout 和 MODULE_IMPORT_NS 命名空间支持 |
模块必须声明所属命名空间,否则导出符号不可见 |
| Linux 5.10+ | 模块加载器增加 MODULE_INFO 字段校验,强化 MODULE_SIG 强制验证 |
未签名模块在启用 CONFIG_MODULE_SIG_FORCE 时直接拒绝加载 |
| Linux 5.15+ | 模块内存分配改用 vmalloc 区域,支持 MODULE_INIT_COMPRESSED 压缩模块 |
模块加载速度提升,但需要内核支持 CONFIG_MODULE_COMPRESS |
| Linux 6.1+ | 模块加载器重构,引入 module_kallsyms_on_each_symbol 回调,强化 MODULE_IMPORT_NS 检查 |
跨命名空间符号引用必须显式 MODULE_IMPORT_NS,否则加载失败 |
移植要点:从 5.4 开始,如果驱动模块使用了
EXPORT_SYMBOL 导出的符号,但未通过 MODULE_IMPORT_NS 声明所属命名空间,其他模块将无法解析该符号。移植时需检查 Kconfig 中是否定义了 MODULE_IMPORT_NS 宏。
3.2 符号导出机制与 CRC 版本控制
内核使用 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 将符号导出给模块使用。不同内核版本对符号导出的可见性控制和CRC 校验策略不同:
- 符号可见性:早期内核(4.x)中,所有
EXPORT_SYMBOL导出的符号全局可见。5.4+ 引入MODULE_IMPORT_NS后,符号被限制在命名空间内,模块必须MODULE_IMPORT_NS("namespace")才能访问。 - CRC 校验:模块编译时,
modpost工具会为每个导出符号生成 CRC 值(基于函数原型和头文件)。加载时,内核比较模块记录的 CRC 与当前内核的 CRC,不一致则拒绝加载。 - CRC 生成规则变化:5.10+ 对结构体布局敏感,即使函数签名相同,结构体成员顺序或大小变化也会导致 CRC 变化。
// 示例:模块中引用导出符号,CRC 不匹配时的典型错误
// dmesg 输出:
// [ 1234.5678] module_test: disagrees about version of symbol 'gpio_direction_output'
// [ 1234.5678] module_test: Unknown symbol gpio_direction_output (err -2)
避坑指南:跨版本移植时,不要只复制
.ko 文件。必须使用目标内核版本的源码重新编译模块,否则 CRC 校验必然失败。如果必须使用预编译模块,需确保内核版本、配置(CONFIG_*)和编译器版本完全一致。
3.3 模块依赖关系与自动加载
模块依赖关系由 modprobe 工具和内核的 modules.dep 文件管理。不同内核版本在依赖解析和自动加载方面存在差异:
| 特性 | Linux 4.x | Linux 5.x | Linux 6.x |
|---|---|---|---|
| 依赖文件格式 | modules.dep 基于 depmod 生成,格式稳定 |
增加 modules.softdep 软依赖支持 |
强化 modules.builtin.modinfo 内置模块信息 |
| 自动加载触发 | 基于 MODULE_DEVICE_TABLE 的 alias 匹配 |
增加 MODULE_ALIAS 的优先级排序 |
支持 MODULE_ALIAS_FS 文件系统别名 |
| 循环依赖处理 | 内核模块加载器检测循环依赖并报错 | 增加 module_dependency 调试接口 |
改进循环依赖检测算法,减少误报 |
| 内置模块依赖 | 内置驱动不参与 modules.dep |
5.4+ 内置模块信息写入 modules.builtin |
6.1+ 内置模块支持 MODULE_IMPORT_NS |
实践建议:在 Android 内核中,推荐将核心驱动(如 GPIO、I2C、SPI 控制器)编译为内置,将外设驱动(如传感器、音频编解码器)编译为模块。内置驱动避免依赖加载顺序问题,模块化驱动便于调试和更新。
3.4 模块化驱动与内置驱动的选择策略
下表总结了模块化驱动与内置驱动在不同场景下的选择依据:
| 对比维度 | 内核模块(.ko) | 内置驱动(built-in) |
|---|---|---|
| 加载时机 | 系统启动后动态加载,依赖 modprobe 或 insmod |
内核初始化阶段静态链接,随内核一起启动 |
| 调试灵活性 | 高:可单独卸载、重载、替换,无需重启 | 低:修改驱动需重新编译内核并重启 |
| 启动速度 | 稍慢:需解析依赖、加载模块、执行初始化 | 快:无额外加载开销 |
| 内存占用 | 按需加载,节省内存;但模块代码不可交换 | 始终驻留内存,即使设备未使用 |
| 符号依赖 | 需显式处理符号依赖,CRC 校验严格 | 无符号依赖问题,所有符号在内核链接时解析 |
| 安全签名 | 需通过 MODULE_SIG 验证,否则可能被拒绝 |
内置驱动随内核签名,无需单独签名 |
3.5 模块加载与依赖关系流程图
下图展示了从用户空间触发模块加载到内核完成初始化的完整流程,以及不同内核版本的关键差异点:
3.6 模块依赖关系示例与常见错误
以下是一个典型的模块依赖关系示例,展示 modules.dep 文件内容:
# modules.dep 文件片段 (Linux 5.10)
# 格式:模块路径: 依赖模块列表
kernel/drivers/i2c/busses/i2c-msm-geni.ko:
kernel/drivers/i2c/i2c-core.ko:
kernel/drivers/input/touchscreen/ft5x06_ts.ko: kernel/drivers/i2c/busses/i2c-msm-geni.ko kernel/drivers/i2c/i2c-core.ko
kernel/drivers/regulator/msm_geni_regulator.ko: kernel/drivers/i2c/busses/i2c-msm-geni.ko
常见错误与排查:
- Unknown symbol: 模块引用的符号未导出或 CRC 不匹配。使用
modprobe --show-depends查看依赖,nm module.ko | grep U查看未定义符号。 - Module format not recognized: 模块编译时的内核版本与运行内核版本不一致,或使用了错误的
ARCH编译。 - Required key not available: 内核启用了强制签名验证,但模块未签名或签名无效。Android GKI 内核必须使用
sign-file工具签名。 - Namespace import failure: 模块使用了
EXPORT_SYMBOL_NS导出的符号,但未通过MODULE_IMPORT_NS声明。检查模块源码中是否包含MODULE_IMPORT_NS宏。
3.7 跨版本移植检查清单
在进行模块化驱动跨版本移植时,建议按以下清单逐项检查:
- 确认内核版本与配置:检查
CONFIG_MODULES、CONFIG_MODULE_SIG、CONFIG_MODULE_COMPRESS、CONFIG_MODULE_IMPORT_NS等选项。 - 检查符号导出方式:确认驱动使用的
EXPORT_SYMBOL是否改为EXPORT_SYMBOL_NS,并添加对应的MODULE_IMPORT_NS。 - 验证 CRC 兼容性:使用目标内核的
Module.symvers文件重新编译模块,确保 CRC 一致。 - 处理模块签名:如果目标内核启用了强制签名,需获取签名密钥并重新签名模块。
- 测试依赖加载:使用
modprobe --dry-run模拟加载,检查依赖是否完整。 - 检查内置驱动冲突:如果驱动已内置,模块加载时会报
module already built-in错误,需将驱动从模块列表移除。
最佳实践:在 Android 内核移植中,建议将驱动代码组织为模块化 + 内置双模式。通过
Kconfig 的 tristate 选项,允许开发者根据需求选择编译为模块或内置。同时,在 Makefile 中使用 obj-$(CONFIG_DRIVER_NAME) += driver.o 实现灵活编译。