在尝试构建我自己的非 GNU 跨平台 C++ 环境时,我面临着一个事实,即我并不真正了解堆栈展开的基础知识。我搭建的环境如下:
libc++
← libc++abi
← libunwind
(或其他一些解卷器)。
我发现 libc++abi
已经包含某种 libunwind,但没有在 Linux 上使用它。从我理解的评论来看,它是特殊的 libunwind: LLVM Stack Unwinder,它只支持 Darwin 和 ARM 但不支持 x86_64 - 而且它令人困惑。 CPU架构如何影响堆栈展开过程?
我还知道以下堆栈展开器:
- 内置 glibc。
- libc++abi LLVM libunwind。
- GNU libunwind(来自热带草原)。
问题:
- 平台或 CPU 架构如何影响堆栈展开过程?
- 为什么要有很多堆放卷机 - 而不是只有一个?
- 存在哪些类型的开卷机,它们之间有什么区别?
对答案的期望:
我希望得到涵盖整个主题的答案,而不仅仅是每个问题的单独要点。
从根本上说,堆栈布局取决于编译器。它几乎可以按照它认为最好的任何方式布置堆栈。语言标准没有说明堆栈的布局方式。
在实践中,不同的编译器以不同的方式布置堆栈,并且相同的编译器在使用不同的选项运行时也可以不同地布置堆栈。堆栈布局将取决于目标平台上类型的大小(尤其是指针类型的大小)、编译器选项(例如 GCC 的 -fomit-frame-pointer
)、平台的 ABI 要求(例如 x64有一个定义的 ABI,而 x86 没有)。如何解释堆栈也将取决于编译器如何存储相关信息。这反过来部分取决于可执行文件的格式(现在可能是 ELF 或 COFF,但实际上,只要操作系统可以加载可执行文件并找到入口点,其他一切都非常重要),部分取决于调试信息格式——同样特定于正在使用的编译器/调试器组合。最后,完全有可能编写以没有展开程序能够遵循的方式来操纵堆栈和程序流的内联汇编程序。一些编译器还允许您自定义函数序言和结尾,给您另一个混淆任何展开算法的机会。
所有这一切的最终结果是不可能编写一个适用于所有地方的单一堆栈展开算法。展开算法必须与编译器、操作系统相匹配,除了最基本的信息外,还必须与调试器相匹配。您能做的最好的事情就是编写一个简单的堆栈展开接口(interface),并针对您支持的每个编译器/操作系统/调试器组合以不同的方式实现它。