分段堆栈如何工作

How do segmented stacks work

本文关键字:工作 何工作 堆栈 分段      更新时间:2023-10-16

分段堆栈如何工作?这个问题也适用于Boost.Coroutine,因此我在这里也使用C 标签。主要的疑问来自本文,看起来他们要做的是在堆栈的底部保持一定的空间,并通过在其中分配的内存来注册某种信号处理程序(也许是通过mmapmprotect)来检查它是否已损坏。然后,当他们发现自己的空间用完时,他们通过分配更多的内存然后从那里继续进行。3个问题

  1. 这不是构造用户空间的东西吗?他们如何控制分配新堆栈的位置,以及如何对程序进行编译以了解这一点?

    推动指令基本上只是在堆栈指针中添加一个值,然后将值存储在堆栈上的寄存器中,那么推动指令如何才能意识到新堆栈的启动位置,并且相应地可以相应地知道Pop何时知道何时它必须将堆栈指针移回旧堆栈?

  2. 他们也说

    我们有了一个新的堆栈段后,我们通过重试使我们用完堆栈的功能来重新启动goroutine

    这是什么意思?他们是否重新启动了整个goroutine?这可能不会导致非确定性行为?

  3. 他们如何检测该程序已经超越了堆栈?如果他们在底部保持金丝雀般的内存区域,那么当用户程序创建一个足够大以使其溢出的数组时会发生什么?这不会导致堆栈溢出,并且潜在的安全漏洞吗?

如果实现与GO和Boost有所不同

我会为您提供一个可能实现的简短草图。

首先,假设大多数堆栈框架都比某些尺寸小。对于更大的人,我们可以在入口时使用更长的指令顺序,以确保有足够的堆栈空间。假设我们正在使用具有4K页面的体系结构,并且我们选择4K -1作为快速路径处理的最大尺寸堆栈框架。

堆栈在底部分配了单个后卫页面。也就是说,一个未映射的页面。在功能输入时,堆栈指针被堆栈框架大小降低,该大小小于页面的大小,然后该程序安排在新分配的堆栈框架中的最低地址编写值。如果达到了堆栈的末尾,则此写入将导致处理器异常,并最终变成从OS到用户程序的某种台词,例如unix家族的信号。

信号处理程序(或当量)必须能够确定这是堆栈扩展的故障,从错误的指令的地址及其编写的地址。这是可以确定的,因为该指令是在函数的序言中,并且写入的地址位于当前线程的堆栈的护罩页面中。可以通过在功能开始时或可能通过维护有关功能的元数据来识别的指令可以通过非常具体的指令模式来识别。(可能使用追溯表。)

此时,处理程序可以分配一个新的堆栈块,将堆栈指针设置为块顶部,做一些事情来处理堆栈块,然后调用再次出现故障的功能。第二个呼叫是安全的,因为故障是在函数序言中生成的编译器,并且在验证验证足够的堆栈空间之前不允许副作用。(该代码还可能需要修复将其将其推入堆栈的架构的返回地址。如果返回地址在寄存器中,则只需在第二个调用时就需要在同一寄存器中。)

处理无链接的最简单方法可能是将一个小堆栈框架推向新的扩展块,以进行例程,当返回到链接新的堆栈块并释放分配的内存时。然后,它将处理器寄存器返回到拨打通话时所处的状态,这导致堆栈需要扩展。

此设计的优点是功能输入序列很少有说明,并且在非扩展情况下非常快。缺点是,在需要扩展堆栈的情况下,处理器会引起例外,这可能比函数调用要多得多。

如果正确理解,GO实际上不会使用警卫页面。而是该函数prolag明确检查堆栈限制,如果新的堆栈框架不合适,则调用一个函数以扩展堆栈。

GO 1.3更改了设计,以不使用链接的堆栈块列表。这是为了避免陷阱成本,如果在两个方向上多次以某种呼叫模式越过扩展边界。它们从小堆栈开始,然后使用类似的机制来检测扩展的需求。但是,当确实发生堆栈扩展故障时,整个堆栈将移至更大的块。这完全消除了完全不束缚的需求。

这里有很多细节。(例如,一个人可能无法在信号处理程序本身中进行堆栈扩展。而不是安排悬挂线并将其交给管理员线程。一个可能必须使用专用的信号堆栈来处理信号也。)

这种事情的另一种常见模式是,运行时要求在当前堆栈框架下方有一定数量的有效堆栈空间,例如信号处理程序之类的东西或在运行时调用特殊例程。GO以这种方式工作,堆栈限制测试可以保证当前框架下方有一定固定量的堆栈空间。一个可以,例如只要一个人保证它们的消耗不超过固定的堆放储备金额,请在堆栈上调用普通C功能。(从理论上讲,人们可以使用它来调用C库例程,尽管其中大多数没有正式的规范它们可以使用多少堆栈。)

堆栈框架中的动态分配,例如Alloca或分配的变量长度阵列,为实现增加了一些复杂性。如果例程可以计算序列中框架的整个最终尺寸,则相当简单。框架大小在例程运行时的任何增加都必须将其建模为新调用,尽管使用GO的新体系结构可以移动堆栈,因此可以制作例程中的同种点,以使所有状态允许所有状态允许堆栈移动发生在那里。