【问题标题】:How and when are stack frames built?堆栈帧是如何以及何时构建的?
【发布时间】:2014-10-19 14:16:01
【问题描述】:

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

【问题讨论】:

  • -1 表示未显示任何研究工作。谷歌指向wiki.osdev.org/Stacken.wikipedia.org/wiki/Call_stack#StructureIntel® 64 and IA-32 Architectures Software Developer Manuals 在几个地方解释堆栈框架等。
  • +1 来平衡@xmojmr。这些文件告诉一个人所做的布局选择。 OP 想知道这些选择是如何产生的,这对我来说似乎是一个很合理的问题。
  • @IraBaxter 链接的文档清楚地表明,在汇编语言中(使用的标签 OP)堆栈框架是使用程序员编写的 push 指令创建的,并被 1 对 1 编译成机器代码(因此堆栈框架布局不是在“程序执行之前”确定,也不是在“调用函数时”确定,而是在程序员编写pushenter 指令时确定)。在我看来,要么 OP 想提出不同的问题,要么没有表现出任何研究工作
  • 所以有两个隐含的问题:1)堆栈帧的设计是什么[它们都有什么共同点],2)堆栈帧的细节(例如,偏移量)是什么[通常在特定于正在编译的特定子例程的编译时确定]。他似乎专注于前者,并且似乎理解后者的发生。

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


【解决方案1】:

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

【讨论】:

  • 不一定是堆栈。
  • @IraBaxter OP 从 汇编语言 的角度询问 x86 上的 stack frame,而不是关于实现的任意激活记录正如您在答案中建议的那样,在堆栈上建模为链表。所以如果问题是 stack 那么答案是 stack
  • @xmojmr:他以非常笼统的方式询问了 Linux 下 x86 上运行的东西。我以非常笼统的方式回答。 “天地间还有更多……”
  • @IraBaxter 您所说的“不必是堆栈”评论是什么意思?
  • @xmojmr:我以为你明白我的回答:“堆栈框架”不必是“在堆栈中”。链接的激活记录是另一种方法。我的观点是设计运行时环境的方法有很多种,但大多数背后的基本原理并不经常写在容易找到的地方。
【解决方案2】:

我怀疑您通常会或很容易找到有关堆栈框架设计方式的文档化答案。正如其他人所观察到的,记录在案的是该设计过程的结果,通常没有很多相关的理由,我同意这会很有趣。

堆栈框架布局的每个设计都可能来自为特定处理器架构甚至特定操作系统设计编译器或一组可互操作编译器的人。这将受哪些子例程需要访问来自调用者的信息(参数?词法范围?),指令集做得好(大量寄存器?易于推送参数?),编译器的优势或劣势等影响。微软,例如,随着他们的编译器和 x86 的发展,这种设计在过去几十年中多次进行;他们对 x86-32 的约定与 x86-64 的约定确实不同。您可以从记录的结果中猜测其基本原理,有时会有提示,但并非总是如此。

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

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

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

给定堆栈框架设计,一种语言的编译器然后在框架内为正在编译的特定子例程分配空间。

【讨论】:

    【解决方案3】:

    这是编译器时的编译器选择,如果您在不同操作系统上为相同的处理器/目标使用具有相同构建选项的相同编译器,您可能会得到相同类型的堆栈帧结果。

    堆栈框架使编译器开发人员更容易调试代码以及​​其他人阅读代码,但是否使用堆栈框架的成本更高尚有争议。它也可能使调试器(软件)的工作更轻松,但您必须与编译器密切同步才能使其工作。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-13
      • 1970-01-01
      • 2010-09-06
      • 2015-01-16
      • 2013-10-12
      • 2018-01-26
      • 2020-02-21
      相关资源
      最近更新 更多