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 回溯器对文本大小或运行时性能没有影响,因为调试信息是带外的。因此,如果禁用帧指针并启用 ORC 回溯器,您将在各个方面获得良好的性能提升,并且仍然具有可靠的堆栈跟踪。

Ingo Molnar 说

“请注意,这不仅仅是性能提升,也是指令缓存局部性改进:3.2% 的 .text 节省几乎直接转化为缓存占用空间相应大小的减少。这可以转化为缓存局部性处于临界点的工作负载的更高加速。”

与帧指针相比,ORC 的另一个好处是它可以可靠地在中断和异常之间回溯。如果被中断的函数是叶函数,或者中断发生在帧指针保存之前,基于帧指针的回溯有时会跳过被中断函数的调用者。

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

9.3. ORC 与 DWARF

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

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

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

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

9.4. ORC 回溯表生成

ORC 数据由 objtool 生成。借助现有的编译时堆栈元数据验证功能,objtool 已经跟踪所有代码路径,因此它已经拥有从头生成 ORC 数据所需的所有信息。因此,从堆栈验证到 ORC 数据生成是一个简单的步骤。

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

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

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

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

缺点是回溯器现在依赖于 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 回溯能力)。