LWN:新版本的modversions!

关注了就能看到更多这么棒的文章哦~

A new version of modversions

By Jonathan Corbet
August 26, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/986892/

genksyms 工具长期以来深埋于内核构建系统之中;它是内核附带的两个 C 代码解析器之一(另一个是 令人毛骨悚然的 kernel-doc 脚本)。它是内核模块加载基础设施运作的关键部分。尽管 genksyms 在过去几十年中默默地完成了它的工作,但这可能即将结束。似乎 genksyms 无法处理 Rust 代码,因此 Sami Tolvanen 正在 提议一个新的工具 在未来处理这项任务。

在早期,内核只支持单一构建(monolithic builds);并没有可加载模块(loadable module)的概念。这种情况在 1994 年初的 0.99.15 版本 中发生了改变,该版本添加了模块支持以及其他一些功能。该版本也是 1.0 版本“代码冻结”的开始;Linus Torvalds 当时表示:

将 Linux 版本号提升至 1.0 并不意味着任何其他含义:它只是一个版本号的改变。更明确地说,它  意味着 Linux 将变得商业化(版权将保持不变),也不意味着开发将在此停止,并且 1.0 在这方面将成为任何特殊的东西。

当时的可加载模块与它们构建的特定内核版本绑定。然而,随着 Linux 逐渐变得更加商业化,人们对将给定二进制模块加载到多个内核版本中的能力越来越感兴趣。这种兴趣在那些发布树外模块并希望这些模块在尽可能多的内核中工作的人群中尤其强烈。

然后,就像现在一样,模块通过导出(export)的符号挂接(hook)到内核的其余部分。只有被显式导出的符号(对应于函数或变量)可供可加载模块使用,并且模块也可以导出自己的符号。当模块被编译以使用特定符号时,它自然地包含与该符号关联的类型信息。对符号的任何更改(例如,向函数原型添加参数或重新排列结构定义中的字段)都可能以微妙而令人不快的方式会破坏使用这些符号的模块。

modversions 的起源

内核开发一直都在快速发展;这导致了可加载模块看到的接口发生了很多变化。在早期,人们曾试图将这种变化降到最低,但项目的政策始终是内核的内部接口可以随时更改。树外模块维护人员能做的最好的事情是注意到这些更改并相应地更新他们的代码。

然而,即使注意到这些更改也很难,因为内核开发人员通常不会太担心让外部代码显而易见。但这是一个计算机非常适合做的工作。因此,1.1.85 版本(1995 年 1 月)添加了第一个“modversions”支持,旨在允许模块加载到多个内核版本中——并且明确指出何时不可行。

modversions 的核心是 genksyms=。该工具读取并解析一个经过预处理的 C 源文件,收集该文件导出的每个符号的定义,计算每个定义的校验和,并将结果以构建过程可以访问的形式输出。内核的 =modpost 工具然后使用这些信息创建一个 C 源文件,该文件用符号(导出和使用的)以及它们的校验和填充一个特殊的 ELF 部分。感兴趣的读者可以查看 一个示例,了解该文件的外观。当需要加载模块时,加载器会将该模块使用的每个符号的校验和与运行内核的校验和进行比较;如果两者匹配,则可以安全地加载该模块。

genksyms 最初与内核分开发布在 modutils 软件包中;这种情况在 2003 年 2 月的 2.5.64 开发内核中发生了 改变,被移入了内核的 scripts 目录。该工具多年来一直在维护,没有发生重大变化;在 Git 时代,它获得了(略微)少于 100 个补丁。

genksyms 能够运行如此之久,是因为 C 中的函数或数据结构的声明完全描述了对生成的二进制对象的接口,至少在考虑到传递给编译器的选项之后。编译器不会将它们重新排列成它可能更喜欢的形式。但是,正如 Tolvanen 所解释的,Rust 并非如此:

与 C 不同,Rust 源代码没有关于最终 ABI 的足够信息,因为编译器在调整结构布局以提高性能方面拥有相当大的自由度,例如,这使得使用像 genksyms 这样的源代码解析器成为不可能。

由于这个问题,内核配置不能同时启用 modversions 和 Rust。自然地,对于任何想要启用 Rust 并仍然将二进制模块加载到多个内核版本中的人来说,这不是好消息。由于 Android 属于这种情况,因此一些人致力于解决这种情况就不足为奇了。

向 Rust 抛出 DWARF

教 genksyms 如何解析 Rust 绝非易事,而且如上所述,无论如何,它都无法成为一个足够的解决方案。相反,选择的解决方案是停止解析代码并尝试猜测编译器;开始使用编译器生成的 DWARF 调试信息,它完整地描述了感兴趣的接口。采用这种方法,新的 gendwarfksyms 工具理论上可以对用任何语言编写的代码进行符号版本控制。

这个补丁集的第一个版本 使用新工具为 Rust 代码生成校验和,同时保留了 C 代码的现有机制。然而,模块维护者 Luis Chamberlain 提出 建议,它应该只用于所有代码,因此当前的系列就是这样做的。

这个解决方案看起来很有希望,但并非没有缺点。新工具只有在 DWARF 数据存在的情况下才能处理它,这意味着内核必须启用完整的调试信息进行编译。对于发行版来说,这并不一定是一个问题,因为他们通常也希望获得这些信息,但生成所有这些信息会显著减慢构建过程。内核的构建时间回归比几乎任何其他类型的问题都能更快地引起开发人员的注意,因此这种改变可能不会受到普遍欢迎。

虽然 gendwarfksyms 像 genksyms 一样生成校验和,但它生成的不是相同的校验和。因此,将现有内核切换到使用 gendwarfksyms 会导致所有校验和发生变化,并且任何现有模块都将无法加载。这使得这种切换成为一种需要由发行版仔细管理的“标志日”。

用来保存符号名称和校验和的 modversion_info 结构 限于长度不超过 55 个字符的名称;毕竟,这对任何人都应该足够了。但这对于 Rust 编译器来说还不够,它使用名称重整将类型信息编码到标识符中。更改该结构将破坏用户空间工具,因此不是一个简单的选择。该系列的第一个版本对较长的名称进行了哈希处理,以使其适合可用的空间,但这种行为在第二个发布中已被删除;相反,由 Matthew Maurer 进行的 单独的努力 正在努力使这种信息的表示更加灵活。

另一个有趣的挑战是 Petr Pavlu 描述的。有些发行版试图为内核模块维护 ABI 兼容性,尽管内核项目本身对此目标漠不关心(或持敌意)。他们使用的一个技巧是识别他们认为在内核支持生命周期内可能会获得更多元素的数据结构。这些结构将用一些占位符字段进行扩展,这些字段在设置 __GENKSYMS__ 时会被编译掉,而这只会发生在 genksyms 运行期间。这使他们能够在将来更改该字段的类型,同时将更改隐藏在 genksyms 中,从而避免明显的 ABI 更改。

该系列的第一个版本不支持此功能,但在第二个版本中添加了此功能。然而,在处理 DWARF 数据时,使用预处理宏将不起作用;需要采用不同的方法。因此,使用符号名称开发了另一种黑客方法。新的机制在 此补丁 中进行了描述;想象一个在发行版的内核中被扩展了占位符字段的内核结构,如下所示:

struct struct1 {
    long a;
    long __kabi_reserved_0; /* reserved for future use */
};

gendwarfksyms 识别 __kabi_reserved_ 前缀,尤其是在它出现在联合体中时。在将来的某个时间,上面的结构可以更改为类似以下内容:

struct struct1 {
    long a;
    union {
        long __kabi_reserved_0;
        struct {
            int b;
        int v;
        };
    };
};

描述这个结构的 DWARF 数据将相应地更改,但 gendwarfksyms 会用 __kabi_reserved_0 的定义来替换 union,从而使得该结构在校验和生成方面看起来没有改变。

人们一直在讨论最后一个技巧,尤其是在它如何在 Rust 方面工作。纯粹的 union 类型与 Rust 的做事方式背道而驰,因为它们不提供任何方法来确保在任何给定时间使用正确的字段,因此有人谈论提供更复杂的方案。

人们还担心整体方法将如何与链接时优化(link-time optimization)配合起来,这将使构建过程进一步减慢;此系列目前明确是跟与链接时优化功能冲突的,原因正是如此。不过,总的来说,审阅者似乎对补丁系列的当前形式感到比较满意。一旦找到解决长名称问题的方案,进入 mainline 可能会很快到来。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

fbc28fc42500603d332a8c0c82f11932.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值