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 模块加载与依赖关系流程图

下图展示了从用户空间触发模块加载到内核完成初始化的完整流程,以及不同内核版本的关键差异点:

模块加载与依赖解析流程(跨版本差异标注) 用户空间 insmod / modprobe 内核模块加载器 版本与 CRC 校验 依赖模块加载 命名空间检查 (5.4+) 签名验证 (5.10+ 强制) 模块初始化函数调用 各版本关键差异说明 1. 版本与 CRC 校验 • 4.x: vermagic 严格匹配,CRC 基于函数签名 • 5.10+: CRC 对结构体布局敏感 • 6.1+: 增加 kallsyms 回调检查 2. 依赖模块加载 • 4.x: 基于 modules.dep 顺序加载 • 5.x: 支持 softdep 软依赖 • 6.x: 改进循环依赖检测 3. 命名空间检查 (5.4+ 新增) • 模块必须 MODULE_IMPORT_NS • 未声明命名空间的符号不可见 • 6.1+ 强化检查,加载失败更明确 4. 签名验证 • 4.x: 可选,CONFIG_MODULE_SIG • 5.10+: CONFIG_MODULE_SIG_FORCE 强制 • Android GKI: 必须使用官方签名 5. 初始化调用 • 4.x: 直接调用 module_init • 5.15+: 支持压缩模块解压后初始化 • 6.1+: 增加初始化失败回滚机制

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 跨版本移植检查清单

在进行模块化驱动跨版本移植时,建议按以下清单逐项检查:

  1. 确认内核版本与配置:检查 CONFIG_MODULES、CONFIG_MODULE_SIG、CONFIG_MODULE_COMPRESS、CONFIG_MODULE_IMPORT_NS 等选项。
  2. 检查符号导出方式:确认驱动使用的 EXPORT_SYMBOL 是否改为 EXPORT_SYMBOL_NS,并添加对应的 MODULE_IMPORT_NS。
  3. 验证 CRC 兼容性:使用目标内核的 Module.symvers 文件重新编译模块,确保 CRC 一致。
  4. 处理模块签名:如果目标内核启用了强制签名,需获取签名密钥并重新签名模块。
  5. 测试依赖加载:使用 modprobe --dry-run 模拟加载,检查依赖是否完整。
  6. 检查内置驱动冲突:如果驱动已内置,模块加载时会报 module already built-in 错误,需将驱动从模块列表移除。
最佳实践:在 Android 内核移植中,建议将驱动代码组织为模块化 + 内置双模式。通过 Kconfig 的 tristate 选项,允许开发者根据需求选择编译为模块或内置。同时,在 Makefile 中使用 obj-$(CONFIG_DRIVER_NAME) += driver.o 实现灵活编译。