堆栈帧如何以及何时构建?

时间:2014-10-19 14:16:01

标签: memory assembly x86 stack-trace low-level

我目前正在阅读有关利用Linux下的内存漏洞的文章,我发现很难找到有关何时确定堆栈帧布局的任何信息。换句话说,它是在编译时,在程序执行之前确定的,还是在调用函数时构建的?操作系统之间的布局是否不同?

3 个答案:

答案 0 :(得分:2)

有几个因素。在x86上,有calling convention定义了如何调用函数。我假设其他架构有类似的东西。系统库(例如glibc)可以定义其他约定。但最终编译器决定它如何使用堆栈 - 至少当它不需要连接外部库并且需要遵循它们的堆栈布局时。

答案 1 :(得分:2)

我怀疑您是否会一般或轻松找到有关如何设计堆栈帧的文档答案。正如其他人所观察到的那样,记录的内容是设计过程的结果,通常没有很多相关的理由,我同意这些理由会非常有趣。

堆栈帧布局的每个设计来自可能是人们为特定处理器架构设计编译器或一组可互操作的编译器,甚至可能针对特定OS。这将受到从调用者访问信息的子程序(参数?词法范围?),指令集表现良好(许多寄存器?容易推送参数?),编译器的优点或缺点等因素的影响。微软,例如,随着编译器和x86的发展,这几十年来设计了几次;他们对x86-32的约定与x86-64的约定完全不同。您可以从记录的结果中猜出基本原理,有时会有提示,但并非总是如此。

我可以为我公司在x86上运行的并行编程语言设计“堆栈帧”给你一些想法。

  • 因为语言是并行的,所以堆栈帧是堆分配的(来自极快的线程局部块分配器),而不是堆栈分配;所以“堆栈框架”不是一个正确的术语,我们称之为“激活记录”。 (我将在本讨论中继续称它们为“堆栈帧”)。该方案支持并行编程,其中一个函数可以分叉多个并行子计算,每个子计算都需要自己的堆栈帧;他们显然不能共享一个堆栈。这意味着每个堆栈帧必须包含一个指向前一帧的显式指针,以使被调用者返回。因此,堆栈帧中有一个低偏移量的槽来保存调用者堆栈帧指针。类似地,有一个槽来保存调用者的堆栈指针。使用这两个插槽代替传统的PUSH EBP / LEA ESP,k [ESP]传统上由x86调用约定使用。
  • 词法范围确定要求每个被调用者都可以访问父母的词法范围。这是通过在堆栈帧中留出一组低偏移点来保存经典的“显示”(包含范围的指针集),并将指针传递给ECX中的调用者显示给被调用者来实现的。被调用者复制父母显示所需的内容,如果被调用者不是叶子过程,可能会增加。
  • 具有有限数量寄存器的CPU意味着您无法在寄存器中传递所有参数甚至许​​多参数。我们选择在EAX中传递一个32位参数,在EDX中传递第二个参数,或者在EAX / EDX中传递64位参数;通过推送堆栈中的参数并简单地调用子例程来传递更大的参数列表。被调用者想要访问参数;我们选择在堆栈帧中以低偏移量分配2个插槽来保存EAX / EDX。
  • 与单线程代码不同,每个PARLANSE堆栈帧表示具有可能大量静态定义的并行计算的函数。因此,堆栈帧包含 set 的“grain”(并行线程)上下文块,其具有相关联的固定大小的堆栈,每个堆栈通过其ESP寄存器进行访问。这种方案允许编译器完成大部分分配空间和设置并行粒度的工作,最大限度地缩短创建“粒度”的时间,从而允许更小的计算有效地并行运行。关于每个谷物控制块的内容有很多细节,这里不值得解释;重点是有很多细节是堆栈框架设计的一部分。

我的观点是堆栈框架设计的基本原理是由机器架构和它应该支持的编程语言的目标驱动的。如上所述的基本原理并没有出现在许多文件中,是的,这使得它很难找到。

给定堆栈帧设计,语言的编译器然后在帧内分配空间,用于编译的特定子例程。

答案 2 :(得分:0)

在编译器时,它是编译器的选择,如果在同一处理器/目标的不同操作系统上使用相同的构建选项,则可以获得相同类型的堆栈帧结果。

堆栈帧使编译器开发人员更容易调试代码以及​​其他人来读取代码,关于使用堆栈帧是否成本更高是值得商榷的。它也可能使调试器(软件)的生活更轻松,但您必须与编译器紧密同步才能工作。

它们通常不是必需的,不能想象为什么一个调用约定会关心,它只是一个实现的东西,我是否经常必须跟踪函数中的每个点,事物是相对于我改变栈顶,或者我是否想要预先计算整个函数所需的所有堆栈,并将其消耗一次,然后对于函数的其余部分,我可以硬编码所有相关的内容,使其更易于读取和调试代码有时以另一个寄存器为代价,有时不依赖于实现。

堆栈帧是编译器人员的设计选择,是编译时的事情,而不是运行时的事情。如果您使用相同的编译器和相同的选项,您可以在操作系统中获得相同的布局,在同一操作系统或不同的操作系统上使用不同的编译器,并且无法保证使用相同的布局或者两者都使用堆栈帧。