背景
从事 Linux 内核开发的人,大概都知道 kallsyms_lookup_name() 这个函数。
但从 Linux 5.7 开始,为了防止第三方模块绕过 EXPORT_SYMBOL 的限制访问内部函数,它不再被导出了。
与此同时,eBPF 也在不断发展壮大,但它目前依旧不能取代传统内核模块,例如开发硬件驱动,或是实现某些需要挂起内核操作、等待用户态程序返回判定结果的安全功能。
这让我产生了一个疑问:既然传统内核模块存在兼容性问题,为什么不能在发展 eBPF 的同时,进一步完善传统内核模块的兼容机制?
关于滥用
禁止导出 kallsyms_lookup_name(),确实提高了第三方模块访问内部符号的门槛。
但对于真正需要访问这些符号的开发者来说,依旧可以通过其他方式定位目标。
由此产生的结果就是,原本可以直接通过导出函数完成的操作,现在却可能需要借助更多间接手段。
例如 Linux 5.7 的这一改动就曾影响了 LTTng。
我理解内核维护者不希望第三方驱动依赖内部实现,但取消一个查询入口并不等于 取消了需求。
当然,能够找到一个符号也不能等于可以随意调用它。
关于复杂度
对于不在主线内核中的驱动开发者来说,即使项目完全开源,也仍然需要面对符号 CRC、结构体变化、接口删除以及不同发行版之间的差异。
eBPF 确实解决了一部分问题,但无法覆盖所有场景。如今我们已经拥有两套能力不同的内核扩展机制,却依旧没有为传统外部模块提供一套通用的跨版本兼容方案。
另一种思路
借用奥卡姆剃刀的思路:既然内核已经拥有动态模块加载机制,为什么不尝试完善它,而是让外部开发者各自维护复杂的兼容方案?
既然内核维护者不愿意保证所有内部符号长期稳定,那么能不能在现有的基础上只保证一套最基础的模块加载协议和一个功能更加完善的符号查询接口?
例如,开发者查询符号时,可以返回更多元数据:
- 符号地址
- 符号的类型及 ABI 版本信息
- 函数原型或类型哈希
- 符号所属模块及其生命周期信息
- 导出类型、访问限制和可用能力
至于未导出的内部符号,也可以考虑引入更细粒度的访问授权,而不是一律禁止。
其中的类型信息可以尽可能通过构建工具自动生成,而行为兼容性信息则需要明确的版本约定,以减少维护者手动管理的负担。
而具体如何使用则是交由驱动开发者自行决定。
如此一来,内核仍然不必为绝大部分内部函数提供长期稳定的 ABI 承诺。
所以为什么不试试?
这种设计当然也有成本。
稳定的基础接口本身需要维护,符号的语义变化也不可能全部依靠一个版本号解决。尤其对于未导出的内部符号,如何划定访问权限仍然是一个问题。
但我认为,这些问题至少值得被认真讨论,而不是把"不保证稳定内核 ABI"当作讨论的终点。
如果某个基础接口确实需要发生不兼容的变化,可以引入新版本,并为旧接口设定合理的支持周期。
Red Hat 和 Android GKI 已经在不同范围内实践了稳定内核 ABI。这至少说明,在一定条件下维护稳定是可行的。
如果我们可以让内核自由演进,同时让外部驱动主动识别并适配变化,何乐而不为呢?