x86

Linux x86 ORC 栈回溯

The Linux x86 ORC Stack Unwinder

Posted by icecube on October 7, 2019

没有人希望他们的Linux机器崩溃。 修复系统错误要求开发人员了解崩溃时计算机瞬时状态信息。 调试时比较关键的线索之一是由内核的堆栈展开器(stack unwinder)生成的函数接口列表。 但是内核的堆栈展开并非始终都是100%可靠的。 Josh Poimboeuf发布到Linux内核邮件列表中的x86 ORC补丁程序系列旨在改变这一状况。


堆栈展开的用途

如果Linux机器崩溃,内核开发人员希望知道崩溃的执行路径,因为可以帮助调试解决问题。 堆栈展开是为了弄清楚一个进程如何执行到当前机器指令的。 这个过程称为堆栈跟踪或调用栈。 它是到执行到当前指令途中调用函数的列表。

发生崩溃时,oops消息中将显示一个调用栈。一个示例如下:

性能分析器(例如perf和ftrace)也会使用栈回溯。甚至热补丁,也需要参考调用栈,因为只有所有进程当前都没有调用到旧的函数接口时才能将其重定向到新的补丁接口。这就需要在进程的调用栈中搜索该接口。

不只是内核空间用到该功能,用户程序gdb在调试运行程序或者处理coredump文件时也需要利用栈回溯

栈回溯展开功能有这么多用途,很多开发人员对确保堆栈正确展开感兴趣。 栈展开的两个最重要属性,一是生成一个调用栈有多快,二是展开调用栈有多精确。

从Linux v4.12开始,有多个x86展开器:guess和帧指针(frame pointer) 展开器。 guess展开器非常简单,它通过在堆栈上查找指令地址来对调用栈进行猜测。其准确性还有很多不足之处。

帧指针frame pointer展开器则更强大.

帧指针和DWARF展开器

主线内核中x86架构当前默认使用帧指针展开器。 它需要编译器的支持,编译器会使用x86-64 ABI中描述的规则在每个函数开始时将前一个帧指针的地址保存在%rbp寄存器中(序言-开栈指令)。 使用此选项编译内核时,%rbp寄存器仅用于该目的,而不再用作通用寄存器。

通过在寄存器被覆盖之前将%rbp的值复制到当前堆中帧指针中,可以将堆帧指针链接在一起。 因为堆帧指针具有固定的布局–并且函数返回地址在堆帧指针内处于已知的偏移量位置–展开堆栈只需要一些算术偏移和指针解引用; 这就是帧指针方式进行栈回溯时速度快的原因。

帧指针展开器的缺点之一是,使用它会为内核中的每个函数引入运行时开销。 无论是否发生崩溃,编译器在编译生成目标文件时都将修改每个函数序言,将堆帧指针的地址保存在%rbp中,并在函数结尾处返回前先恢复%rbp的值。 Poimboeuf在ORC系列报告中所说,启用GCC的帧指针选项后,内核文本大小会增加3.2%。 -fno-omit-frame-pointer  -fomit-frame-pointer

对于某些工作负载,这种开销可能很大。 Mel Gorman报告说,在运行netperf,页分配器微基准(通过SystemTap运行),pgbench和sqlite之类的基准测试时,观察到启用GCC的帧指针选项会增加5%到10%的开销,这是因为“所有函数入口处都添加了少量开销”。 这些开销是由每个函数序言和结尾中的附加指令带来的,这些附加指令也要占用CPU指令周期,从而增加了内核文本大小,最终可能甚至导致指令高速缓存未命中。

帧指针展开器在跨越内核和用户空间边界时可能会遇到麻烦-当使用perf来分析应用程序时,这可能是一个大问题。 如果内核是在启用了帧指针编译选项的情况下编译的,则应用程序及其使用的每个库也需要在启用了帧指针的情况下进行编译。 大多数Linux发行版都不会在启用帧指针的情况下编译其软件,如果用户想要完整的调用栈,则必须使用自定义编译器选项手动构建其软件堆栈。

最后,帧指针展开在跨越异常和中断的情况下是不可靠的。 中断可以在任何地方发生,包括在写入%rbp寄存器之前。 发生这种情况时,帧指针展开器将无法计算调用栈。 这会导致Oops消息中的堆栈跟踪不正确,并影响需要可靠堆栈跟踪的实时补丁功能。

DWARF展开器是一种在中断之间可靠的展开器。 DWARF展开器通过解析编译器发出的DWARF调用帧信息(CFI)表来生成调用栈。 DWARF是为调试应用程序而发明的,而生成调用栈只是其中的一小部分功能。 DWARF是一种非常复杂的调试格式。 它可以使用复杂的状态机在任何指令下描述机器寄存器的状态。

由于DWARF debuginfo位于数据表中,因此展开器使用的数据是带外的,可以作为单独的文件分发。 这正是许多Linux发行版针对其内核的常规操作。 这完全消除了运行时的开销,除了那些想要调试崩溃或分析任务的用户之外。 与帧指针展开器形成鲜明对比。

x86的主线Linux内核中没有DWARF展开器。 必须使用诸如gdb之类的单独的工具对崩溃转储进行后期处理,并为使用DWARF debuginfo构建的内核生成调用栈,因而无法将DWARF应用于Oops消息,实时补丁,perf和ftrace。

许多年前,内核中有一个DWARF展开器,但是在开发人员发现导致oops代码崩溃的错误之后将其删除–在崩溃处理程序中崩溃并不能实现更可靠的展开器。 Jiri Slaby的最新系列添加了SUSE内核中当前所载的DWARF展开器,这带来了许多历史问题。 其中一些是由GCC生成的DWARF信息丢失或不正确引起的。 Linus回复了Slaby的补丁,并重述了GCC DWARF调试信息的一些问题:“实际上并没有真正依赖它的人,而对其进行测试的人很少在内核中做我们要做的事情(例如,内联汇编等)。

尽管编译器可以为C代码自动生成DWARF debguinfo,但是任何用汇编语言编写的内容(x86源代码中有50,000行以上)都必须手工标注。 即使在删除了原始的DWARF展开器之后,注释仍在x86汇编代码中保留了多年。 但是最终,他们的维护负担变得太大了,Ingo Molnar在v4.2合并窗口中将其删除。

Linus态度明确,内核中不允许使用DWARF展开器:“因为从上次我们看中了展开器(sic)以来,所有因oops处理而带来的问题显著上涨,我再也不想看到这种实现复杂的展开器了,尤其是还要用在oops代码中”

走进ORC展开器的世界

Poimboeuf讨论了一种新的展开和调试格式的可能性。提出的解决方案是ORC展开器。

它使用objtool和现有的堆栈验证工作创建了一种全新的自定义debuginfo格式。 ORC格式较小,因此比DWARF简单得多,这意味着展开器中不需要复杂的状态机。

另外,采用完全由内核社区控制的格式,它应该提供DWARF缺少的可靠性保证(debuginfo中没有错误导致崩溃)和低维护开销(因为它不需要手工注释汇编代码)。 使用ORC展开器可以可靠地处理中断和异常时的情况。

与DWARF一样,但与帧指针不同,数据是带外的,并且不会增加内核文本的大小,尽管Poimboeuf说启用ORC展开器需要添加2-4MB的ORC debuginfo,这将只会增加在内核映像中数据部分的大小。。

正如Poimboeuf在比较ORC和帧指针时解释的那样:“相反,ORC展开器对文本大小或运行时性能没有影响,因为debuginfo是带外的。因此,如果禁用帧指针并启用ORC展开器,则可以全面改善性能,并且实现可靠的堆栈跟踪。”

ORC展开器的简单性也使其工作更快。 Jiri Slaby展示了不变形的速度比DWARF展开器快20倍。从那时起,就进行了性能调整,Poimboeuf推测速度现在可能接近40倍。

ORC 命名

在补丁程序系列的v3版之前,展开器的名称为“ undwarf”。 在Poimboeuf说他与名字无关后,Ingo Molnar建议使用ELF DWARF乐曲上的即兴演奏的替代方法。 Poimboeuf接受了“中土世界”主题并与之并驾齐驱,在阅读了Wikipedia上的《中土民族》文章后,终于选定为ORC:“兽人是矮人的天敌,它们是中世纪民间传说的可怕生物。 同样,ORC展开器的创建以DWARF的复杂性和缓慢性。” ORC的缩写是“Oops Rewind Capability”。

ORC 原理

所有使用带外数据的堆栈展开器都需要某种机制来生成该数据。当构建新内核时,ORC使用objtool来构建展开表,这些表在链接时构建到内核映像中。每当需要生成调用栈时,内核ORC展开器都会处理该展开表。

堆栈验证工具用于分析目标文件中的所有指令,并构建展开表以描述每个指令地址处的堆栈状态。此数据同时被写入两个并行数组.orc_unwind和.orc_unwind_ip。同时使用两个节可以更快地查找给定指令地址的ORC数据,因为数据的可搜索部分(.orc_unwind_ip)更紧凑。

展开表由结构体struct orc_entry元素组成,这些元素描述了如何找到上一个函数的堆栈指针和帧指针。每个元素对应一个或多个代码位置。

支持帧指针和guess展开器的现有x86展开器基础结构已经提供了ORC展开器所需的大部分代码。作为对展开器复杂度的粗略衡量,实现每个展开器(帧指针:391,ORC:582,DWARF:1802)所需的C代码行数使ORC展开器比DWARF更接近帧指针。

未来的工作

ORC展开器肯定是有希望的。但是它仍然缺少一些重要的功能,这些功能将阻止它成为内核中的默认展开器。

首先,它没有堆栈可靠性检查,这意味着它不能与实时修补一起使用。实时修补程序会执行运行时检查,并通过检查任务的调用栈中是否存在指定函数来确定什么时候打补丁是安全的。但是,ORC提供的跨越中断和异常的更高可靠性将使可靠性检查成为高优先级选项。

也缺少对动态生成代码(如来自ftrace和BPF的代码)的支持。

而且由于ORC是内核展开器,因此perf工具中不支持在用户空间中使用ORC生成调用栈。 Poimboeuf认为添加相应支持是可能的:“如果希望perf能够对用户空间二进制文件使用ORC而不是DWARF,尽管我看不到有任何技术障碍,但是目前尚无法实现。需要教会Perf读取ORC数据。”

到目前为止,基于大多数评论,所以ORC展开系列文章似乎很可能会在v4.14的时间范围内合并。是否默认启用它还有待观察-包括Linus在内的许多开发人员都生动地记得上一次在Oops代码中使用新的展开器。希望兽人orcs比矮人dwarves更可靠。