4. ABI基础与二进制兼容原理
在 Android 内核模块接口(KMI)工程中,源码兼容并不等于二进制兼容。一个模块即使能在新内核上重新编译通过,也不代表旧模块可以直接加载运行。ABI(Application Binary Interface,应用二进制接口)描述的是编译产物之间在机器码层面的契约,它规定了函数如何被调用、数据如何布局、符号如何解析、动态链接如何完成。理解 ABI,是判断 KMI 能否稳定、模块能否跨版本复用的基础。
一、ABI 的定义与构成要素
ABI 是二进制层面的接口规范,它比 API 更底层。API 关注“有哪些函数、参数是什么”,ABI 关注“这些函数在寄存器、栈、内存中如何传递,结构体每个字段落在哪个偏移,符号以什么名字和版本导出”。在 Android 内核场景中,ABI 主要由以下要素共同决定:
- 调用约定:参数传递顺序、寄存器分配、返回值位置、栈对齐与调用者/被调用者保存规则。
- 数据结构布局:结构体字段顺序、对齐、填充、位域、联合体以及指针宽度。
- 符号与版本:导出符号名、符号类型、CRC 校验、版本节点与命名空间。
- 动态链接机制:重定位、GOT/PLT、模块加载时的符号解析与依赖顺序。
- 编译与目标属性:目标架构、浮点 ABI、代码模型、编译器选项与 LTO 等。
这些要素中任意一项发生变化,都可能破坏二进制兼容性。因此 KMI 工程的核心任务,是把这些要素稳定下来,形成可验证的契约。
二、调用约定与数据结构布局
以 ARM64 为例,函数调用时前 8 个整型参数依次放入 x0~x7,浮点参数放入 v0~v7,超出部分通过栈传递,返回值放在 x0 或 v0。若模块与内核对该约定理解不一致,参数就会错位,表现为随机崩溃或数据错乱。调用约定通常由架构和编译器共同固定,相对稳定,但编译器选项(如是否启用 SCS、是否使用不同浮点 ABI)仍可能改变行为。
数据结构布局是更常见的破坏源。结构体字段的增删、顺序调整、类型宽度变化、对齐属性变化,都会改变字段偏移和整体大小。例如内核中一个导出结构体在版本 A 中为:
struct kmi_ops {
int (*init)(void *ctx);
void (*release)(void *ctx);
u32 flags;
};
若在版本 B 中把 flags 移到 init 之前,虽然源码层面仍可编译,但旧模块按旧偏移写入 flags 时,实际会覆盖函数指针,导致调用跳转到非法地址。这类问题在二进制兼容判定中必须通过布局比对来发现。
三、符号版本、CRC 与动态链接
Linux 内核通过 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 导出符号,并为每个符号生成 CRC 校验值。模块加载时,内核会比对模块记录的符号 CRC 与当前内核的 CRC,不一致则拒绝加载。这实际上是一种粗粒度的 ABI 校验:只要函数签名或相关类型布局变化,CRC 就可能改变。
符号版本进一步细化了这种约束。通过版本节点和命名空间,可以控制哪些符号对哪些模块可见,避免不同供应商模块之间的符号冲突。动态链接阶段,模块的未解析符号会按依赖顺序在内核符号表中查找,重定位完成后才允许执行初始化函数。
# 查看内核导出符号及其 CRC
cat /proc/kallsyms | grep kmi_ops
# 模块加载时的典型报错
kmi_demo: disagrees about version of symbol kmi_register
kmi_demo: Unknown symbol kmi_register (err -22)
当出现 “disagrees about version of symbol” 时,通常说明模块编译时使用的头文件与目标内核的符号 CRC 不一致,属于典型的 ABI 不匹配。
四、兼容性判定方法与流程
判断两个内核版本之间是否保持 KMI 二进制兼容,不能只看源码 diff,而应建立可重复的判定流程:
- 提取基线 ABI:从参考内核中导出符号列表、符号 CRC、导出结构体布局和调用约定相关属性。
- 提取目标 ABI:对目标内核执行同样的提取,生成可比对的数据集。
- 逐项比对:符号集合是否只增不减;已有符号 CRC 是否一致;结构体大小、字段偏移、对齐是否一致。
- 动态验证:用基线模块直接在目标内核上加载,观察符号解析、重定位和初始化是否成功。
- 回归确认:对关键路径执行功能测试,确认没有因隐式布局变化导致的行为差异。
下表给出常见 ABI 变化类型及其兼容性判定结论,可作为评审清单使用:
| 变化类型 | 示例 | 二进制兼容 | 处理建议 |
|---|---|---|---|
| 新增导出符号 | 新增 kmi_query | 兼容 | 可直接合入 |
| 删除导出符号 | 移除 kmi_register | 不兼容 | 保留旧符号或走版本升级 |
| 函数签名变化 | 参数由 2 个变 3 个 | 不兼容 | 新增函数,旧函数保留 |
| 结构体尾部追加字段 | 末尾增加 u64 cookie | 通常兼容 | 确认大小与对齐无副作用 |
| 结构体中间插入字段 | 在首字段后插入新字段 | 不兼容 | 禁止,改用预留字段 |
| 字段类型变宽 | u32 改为 u64 | 不兼容 | 新增字段并保留旧字段 |
| 对齐属性变化 | 增加 __aligned(16) | 不兼容 | 评估后统一基线 |
| CRC 变化 | 符号 CRC 与基线不同 | 不兼容 | 定位头文件或类型差异 |
五、典型场景与误区复盘
某项目在升级内核小版本后,旧显示模块无法加载,日志提示符号 CRC 不一致。排查发现,内核侧在一个导出结构体中把 bool 字段改成了 u8,源码语义几乎相同,但字段大小和对齐发生变化,导致 CRC 改变。修复方式不是强行忽略 CRC,而是恢复原布局,把新语义放入新增字段。
常见误区包括:
- 认为“源码能编译”就等于“二进制兼容”,忽略布局与 CRC。
- 只关注函数符号,忽略导出结构体和全局变量的布局。
- 依赖编译器默认对齐,未使用显式填充和预留字段。
- 在未验证的情况下关闭 CRC 校验,掩盖真实的不兼容问题。
ABI 是 KMI 稳定性的底层语言。只有把调用约定、数据布局、符号版本和动态链接统一纳入可度量、可回归的工程流程,才能真正实现“一次编译、多版本加载”的二进制兼容目标。