9. ORC 栈展开器¶
9.1. 概述¶
内核的 CONFIG_UNWINDER_ORC 选项启用了 ORC 栈展开器,它在概念上类似于 DWARF 栈展开器。不同之处在于,ORC 数据的格式比 DWARF 简单得多,这反过来又使得 ORC 栈展开器更加简单和快速。
ORC 数据由 objtool 生成的展开表(unwind tables)组成。它们包含由内核中的 ORC 栈展开器使用的带外(out-of-band)数据。Objtool 通过首先进行编译时栈元数据验证(CONFIG_STACK_VALIDATION)来生成 ORC 数据。在分析了 .o 文件的所有代码路径后,它会确定文件中每个指令地址处的栈状态信息,并将该信息输出到 .orc_unwind 和 .orc_unwind_ip 段中。
每个目标文件的 ORC 段在链接时进行组合,并在引导时进行排序和后处理。栈展开器在运行时利用生成的结果数据将指令地址与其栈状态关联起来。
9.2. ORC 与帧指针(frame pointers)¶
启用帧指针后,GCC 会向内核中的每个函数添加插桩代码。内核的 .text 大小增加了约 3.2%,导致整个内核范围内性能下降。Mel Gorman [1] 的测量表明,某些工作负载的性能下降了 5-10%。
相比之下,ORC 栈展开器对代码段大小或运行时性能没有影响,因为调试信息是带外的。因此,如果您禁用帧指针并启用 ORC 栈展开器,您将在整体上获得不错的性能提升,同时仍能获得可靠的栈回溯。
Ingo Molnar 表示
“请注意,这不仅是性能的提升,也是指令缓存局部性的提升:节省 3.2% 的 .text 大小几乎可以直接转化为相同大小的缓存占用减少。对于缓存局部性处于边缘的工作负载,这可以转化为更高的速度提升。”
与帧指针相比,ORC 的另一个好处是它可以可靠地跨越中断和异常进行栈展开。基于帧指针的栈展开有时会跳过被中断函数的调用者,如果它是一个叶子函数(leaf function)或者如果中断在保存帧指针之前发生。
与帧指针相比,ORC 栈展开器的主要缺点是它需要更多内存来存储 ORC 展开表:根据内核配置,大约需要 2-4MB。
9.3. ORC 与 DWARF¶
ORC 调试信息相比 DWARF 本身的优势在于它要简单得多。它摆脱了复杂的 DWARF CFI 状态机,同时也摆脱了对不必要寄存器的追踪。这使得栈展开器变得简单得多,意味着更少的 bug,这对于关键任务的 oops 代码尤为重要。
更简单的调试信息格式也使栈展开器的速度比 DWARF 快得多,这对 perf 和 lockdep 非常重要。在 Jiri Slaby [2] 进行的一项基本性能测试中,ORC 栈展开器的速度比树外(out-of-tree)DWARF 栈展开器快约 20 倍。(注意:该测量是在添加一些性能调整之前进行的,这些调整使性能翻倍,因此相比 DWARF 的速度提升可能接近 40 倍。)
与 DWARF 相比,ORC 数据格式确实有一些缺点。与基于 DWARF 的 eh_frame 表相比,ORC 展开表多占用约 50% 的内存(在 x86 defconfig 内核上增加约 1.3MB)。
另一个潜在的缺点是,随着 GCC 的演进,可以想象,对于某些优化,ORC 数据最终可能会变得过于简单,以至于无法描述栈的状态。但我认为这不太可能,因为 GCC 会为其所做的任何不寻常的栈调整保存帧指针,所以我怀疑我们实际上只需要在调用帧之间跟踪栈指针和帧指针。但即使我们最终不得不跟踪 DWARF 跟踪的所有寄存器,至少我们仍然能够控制格式,例如没有复杂的状态机。
9.4. ORC 展开表生成¶
ORC 数据由 objtool 生成。借助现有的编译时栈元数据验证功能,objtool 已经能够跟踪所有代码路径,因此它已经具备了从头生成 ORC 所需的所有信息。因此,从栈验证过渡到 ORC 数据生成是非常简单的一步。
理论上也可以改用一个将 DWARF 转换为 ORC 数据的简单工具来生成 ORC 数据。然而,由于内核大量使用了汇编(asm)、内联汇编以及像异常表这样的特殊段,这样一个解决方案将是不完整的。
这可以通过在 .S 文件中使用 GNU 汇编器 .cfi 注解手动注释这些特殊代码路径,以及为 .c 文件中的内联汇编使用自研注释来纠正。但是过去曾尝试过汇编注解,结果发现它们是不可维护的。它们经常不正确或不完整,并使代码更难阅读和保持更新。而且根据对 glibc 代码的观察,注释 .c 文件中的内联汇编可能会更糟。
Objtool 仍然需要一些注释,但仅限于对栈做不寻常操作的代码中,比如入口代码。即便如此,所需的注释也远少于 DWARF 所需的注释,因此它们比 DWARF CFI 注解要好维护得多。
因此,使用 objtool 生成 ORC 数据的优势在于它能提供更准确的调试信息,且只需极少的注释。它还使内核免受工具链 bug 的影响,在内核中处理工具链 bug 会非常痛苦,因为我们通常必须连续多年绕过旧版本工具链中的问题。
缺点是栈展开器现在变得依赖于 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. 词源¶
兽人(Orcs,中世纪民间传说中可怕的生物)是矮人的天敌。同样地,ORC 栈展开器的创建也是为了对抗 DWARF 的复杂和缓慢。
“尽管兽人很少为问题考虑多种解决方案,但他们非常擅长把事情做好,因为他们是行动的生物,而不是思考的生物。” [3] 同样,与深奥的 DWARF 栈展开器不同,真实可靠的 ORC 栈展开器不浪费任何时间和硅基算力去解码基于状态机的、可变长度的零扩展无符号整数字节码调试信息条目。
就像兽人经常瓦解其对手苦心孤诣的计划一样,ORC 栈展开器以残酷、不屈不挠的效率频繁地解开栈。
ORC 代表 Oops 倒带能力(Oops Rewind Capability)。