9. ORC 解栈器

9.1. 概述

内核 CONFIG_UNWINDER_ORC 选项启用 ORC 解栈器,它在概念上类似于 DWARF 解栈器。区别在于 ORC 数据的格式比 DWARF 简单得多,这反过来使得 ORC 解栈器也简单得多且速度更快。

ORC 数据由 objtool 生成的解栈表组成。它们包含内核 ORC 解栈器使用的带外数据。Objtool 首先进行编译时堆栈元数据验证 (CONFIG_STACK_VALIDATION) 来生成 ORC 数据。在分析 .o 文件的所有代码路径后,它确定文件中每个指令地址的堆栈状态信息,并将该信息输出到 .orc_unwind 和 .orc_unwind_ip 段。

每个对象的 ORC 段在链接时组合,并在启动时进行排序和后处理。解栈器使用生成的数据将指令地址与运行时它们的堆栈状态相关联。

9.2. ORC 与帧指针

启用帧指针后,GCC 会将检测代码添加到内核中的每个函数。内核的 .text 大小增加约 3.2%,导致整个内核范围内的速度下降。Mel Gorman 的测量 [1] 显示,对于某些工作负载,速度下降了 5-10%。

相比之下,ORC 解栈器对文本大小或运行时性能没有影响,因为 debuginfo 是带外的。因此,如果您禁用帧指针并启用 ORC 解栈器,您将获得全面的性能提升,并且仍然具有可靠的堆栈跟踪。

Ingo Molnar 说

“请注意,这不仅仅是性能改进,而且还是指令缓存局部性的改进:节省 3.2% 的 .text 几乎直接转化为类似大小的缓存占用减少。对于缓存局部性处于临界状态的工作负载,这可以转化为更高的加速。”

与帧指针相比,ORC 的另一个优点是它可以可靠地跨中断和异常解栈。如果中断的函数是叶函数,或者中断在帧指针保存之前命中,则基于帧指针的解栈有时会跳过中断函数的调用者。

与帧指针相比,ORC 解栈器的主要缺点是它需要更多的内存来存储 ORC 解栈表:大约 2-4MB,具体取决于内核配置。

9.3. ORC 与 DWARF

ORC debuginfo 优于 DWARF 的优势在于它更简单。它摆脱了复杂的 DWARF CFI 状态机,也摆脱了对不必要寄存器的跟踪。这使得解栈器更加简单,这意味着更少的错误,这对于任务关键型 oops 代码尤其重要。

更简单的 debuginfo 格式还使解栈器比 DWARF 快得多,这对于 perf 和 lockdep 很重要。在 Jiri Slaby 的一项基本性能测试中 [2],ORC 解栈器比树外的 DWARF 解栈器快约 20 倍。(注意:该测量是在添加一些性能调整之前进行的,这些调整使性能翻了一番,因此相对于 DWARF 的加速可能接近 40 倍。)

与 DWARF 相比,ORC 数据格式确实有一些缺点。ORC 解栈表占用的 RAM 比基于 DWARF 的 eh_frame 表多约 50%(在 x86 defconfig 内核上多 +1.3MB)。

另一个潜在的缺点是,随着 GCC 的发展,ORC 数据可能最终变得简单,无法描述某些优化的堆栈状态。但 IMO 这不太可能,因为 GCC 会保存帧指针用于它所做的任何不寻常的堆栈调整,所以我怀疑我们实际上只需要在调用帧之间跟踪堆栈指针和帧指针。但即使我们最终不得不跟踪 DWARF 跟踪的所有寄存器,至少我们仍然可以控制格式,例如,没有复杂的状态机。

9.4. ORC 解栈表生成

ORC 数据由 objtool 生成。凭借现有的编译时堆栈元数据验证特性,objtool 已经遵循所有代码路径,因此它已经拥有了从头开始生成 ORC 数据所需的所有信息。因此,从堆栈验证到 ORC 数据生成是一个简单的步骤。

应该可以使用一个简单的工具来生成 ORC 数据,该工具将 DWARF 转换为 ORC 数据。但是,由于内核广泛使用 asm、内联 asm 以及异常表等特殊段,因此这样的解决方案是不完整的。

可以通过使用 GNU 汇编器 .cfi 注释在 .S 文件中手动注释这些特殊的代码路径,并为 .c 文件中的内联 asm 添加自定义注释来纠正这一点。但是,过去曾尝试过 asm 注释,但发现它们无法维护。它们通常不正确/不完整,并且使代码更难阅读和保持更新。并且根据查看 glibc 代码,在 .c 文件中注释内联 asm 可能会更糟。

Objtool 仍然需要一些注释,但仅在对堆栈执行不寻常操作的代码中,例如入口代码。即使这样,所需的注释也比 DWARF 需要的少得多,因此它们比 DWARF CFI 注释更易于维护。

因此,使用 objtool 生成 ORC 数据的优点是它可以提供更准确的 debuginfo,并且注释非常少。它还可以使内核免受工具链错误的影响,因为我们经常需要解决工具链旧版本中的问题多年,这可能非常痛苦。

缺点是解栈器现在依赖于 objtool 反向工程 GCC 代码流的能力。如果 GCC 优化对于 objtool 来说变得太复杂而无法遵循,则 ORC 数据生成可能会停止工作或变得不完整。(值得注意的是,livepatch 已经对 objtool 遵循 GCC 代码流的能力有这样的依赖性。)

如果较新版本的 GCC 出现一些破坏 objtool 的优化,我们可能需要重新审视当前的实现。一些可能的解决方案是要求 GCC 使优化更容易接受,或者让 objtool 使用 DWARF 作为附加输入,或者创建一个 GCC 插件来帮助 objtool 进行分析。但就目前而言,objtool 可以很好地遵循 GCC 代码。

9.5. 解栈器实现细节

Objtool 通过与编译时堆栈元数据验证特性集成来生成 ORC 数据,该特性在 tools/objtool/Documentation/objtool.txt 中有详细描述。在分析 .o 文件的所有代码路径后,它会创建一个 orc_entry 结构数组,以及一个与这些结构相关联的指令地址并行数组,并将它们分别写入 .orc_unwind 和 .orc_unwind_ip 段。

ORC 数据被分成两个数组是出于性能原因,以使数据的可搜索部分 (.orc_unwind_ip) 更紧凑。这些数组在启动时并行排序。

通过使用在运行时创建的快速查找表进一步提高了性能。快速查找表将给定的地址与 .orc_unwind 表的一系列索引相关联,以便只需要搜索该表的一个小子集。

9.6. 词源学

兽人是中世纪民间传说中可怕的生物,是矮人的天敌。同样,ORC 解栈器也是为了对抗 DWARF 的复杂性和缓慢性而创建的。

“虽然兽人很少考虑问题的多种解决方案,但他们擅长完成事情,因为他们是行动的生物,而不是思考的生物。” [3] 类似地,与深奥的 DWARF 解栈器不同,真实的 ORC 解栈器不会浪费时间或硅酮精力来解码基于可变长度零扩展无符号整数字节编码状态机的调试信息条目。

类似于兽人经常破坏其对手精心策划的计划,ORC 解栈器经常以残酷、毫不妥协的效率解开堆栈。

ORC 代表 Oops Rewind Capability (Oops 回溯能力)。