Memory 如何以及何时构建堆栈框架?

Memory 如何以及何时构建堆栈框架?,memory,assembly,x86,stack-trace,low-level,Memory,Assembly,X86,Stack Trace,Low Level,我目前正在阅读关于利用Linux下的内存漏洞的文章,我发现很难找到关于何时决定堆栈帧布局的任何信息。换句话说,它是在编译时、程序执行之前确定的,还是在调用函数时生成的?不同操作系统的布局是否不同?有几个因素。在x86上,有一个函数定义如何调用函数。我假设其他架构也有类似的东西。系统库(如glibc)可以定义其他约定。但编译器最终决定如何使用堆栈——至少在它不需要与外部库接口并且需要遵循它们的堆栈布局时是这样 我怀疑您是否能够普遍或轻松地找到有关堆栈框架设计的书面答案。正如其他人所观察到的,记录下

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

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

我怀疑您是否能够普遍或轻松地找到有关堆栈框架设计的书面答案。正如其他人所观察到的,记录下来的是设计过程的结果,通常没有很多相关的基本原理,我同意这将是非常有趣的

堆栈框架布局的每一种设计都可能来自为特定处理器体系结构甚至特定操作系统设计编译器或一组可互操作编译器的人。这将受到访问调用者信息所需的子例程(参数?词法范围?)的影响,指令集的性能(大量寄存器?易于推送参数?)的影响,编译器的优缺点等。例如,Microsoft在过去几十年中多次进行过这种设计,随着编译器和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堆栈帧表示一个函数,其中可能包含大量静态定义的并行计算。因此,堆栈帧包含一组“grain”(并行线程)上下文块,这些块具有相关的固定大小堆栈,每个堆栈都通过其ESP寄存器访问。此方案允许编译器完成分配空间和设置并行粒度的大部分工作,最大限度地减少创建“粒度”的时间,从而允许更小的计算有效地并行运行。关于每个谷物控制块都有很多细节,不值得在这里解释;关键是堆栈框架设计中有很多细节
我的观点是,堆栈框架设计的基本原理是由机器架构和它应该支持的编程语言的目标驱动的。像上面这样的理由在很多文件中都没有出现,是的,这使得很难找到


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

这是编译器在编译时的一种选择,如果您在不同的操作系统上为同一处理器/目标使用相同的编译器和相同的构建选择,那么对于堆栈帧,您可能会得到相同类型的结果

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

它们通常是不需要的,无法想象调用约定为什么会在意,这只是一个实现问题,我是否必须始终跟踪函数中与更改堆栈顶部相关的每一点,或者我是否希望预先计算整个函数所需的所有堆栈,并且一次性使用它,然后对于函数的其余部分,我可以硬编码,其中所有内容都与之相关,使得读取和调试代码更容易,有时需要另一个寄存器,有时不依赖于实现