2017-09-16 86 views
6

如何分割栈工作?这个问题也适用于Boost.Coroutine,所以我在这里也使用C++标记。主要的疑问来源于此article看起来他们做的是保持一定的空间,在堆栈的底部,并检查它是否已经得到了通过注册某种信号处理与分配存在的内存崩溃(可能通过mmapmprotect?)然后,当他们发现他们的空间不足时,他们继续分配更多的内存,然后从那里继续。关于这个的3个问题如何分割栈工作

  1. 这不是构建用户空间的东西吗?他们如何控制新堆栈的分配位置,以及如何编译程序以了解这些情况?

    推送指令基本上只是向堆栈指针添加一个值,然后将值存储在堆栈的一个寄存器中,那么推送指令如何知道新堆栈的启动位置,相应地,弹出如何知道何时必须将堆栈指针移回旧栈?

  2. 他们也说

    我们已经有了一个新的堆栈段后,我们通过重试重新启动goroutine导致我们用完堆栈

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

  3. 他们如何检测程序已经溢出堆栈?如果他们将一个金丝雀存储区保留在底部,那么当用户程序创建的数组足够大而溢出时会发生什么?这会不会导致堆栈溢出并且是潜在的安全漏洞?

如果实现是不同的围棋和Boost我会很高兴知道如何要么他们应对这种情况

+2

开始使用版本1.3中的连续堆栈 – tkausl

+0

@tkausl嗯..只是了解它,这绝对看起来像是需要语言本身支持的东西,并且很难像Boost.Coroutine那样实现库。对?连续堆栈方法似乎更容易理解,尽管 – Curious

+0

@Curious:是需要语言支持--Go具有与C/C++不同的堆栈布局和调用约定。 – JimB

回答

5

我给你一个可能实现的速写。

首先,假设大多数堆栈帧比一些尺寸更小。对于更大的那些,我们可以在入口处使用更长的指令序列以确保有足够的堆栈空间。假设我们在一个拥有4k页的体系结构上,并且我们选择4k-1作为由快速路径处理的最大大小的堆栈帧。

堆栈分配在底部的单一防护页面。也就是说,没有被映射为写入的页面。在函数入口,堆栈指针由堆栈帧的大小,这是小于页的尺寸,然后该程序安排写在新分配的堆栈帧的最低地址的值递减。如果到达堆栈的末端,则该写入将导致处理器异常并最终转变为从操作系统到用户程序的某种上调 - 例如, UNIX系列操作系统中的一个信号。

信号处理程序(或等价物)必须能够确定这是来自故障指令地址和写入地址的堆栈扩展故障。这是可以确定的,因为指令位于函数的序言中,并且正在写入的地址位于当前线程的堆栈的守护页中。在序言中的指令可以通过在函数开始时要求非常特定的指令模式来识别,或者可能通过维护关于函数的元数据来识别。(可能使用回溯表)

此时处理程序可以分配一个新的堆栈块,将堆栈指针设置为块的顶部,做一些处理解除堆栈块的链接,然后调用发生故障的函数再次。第二次调用是安全的,因为错误在编译器生成的函数prolog中,并且在验证有足够的堆栈空间之前不允许有任何副作用。 (该代码可能还需要修复将其自动推入堆栈的体系结构的返回地址,如果返回地址在寄存器中,则只需在第二次调用时位于同一个寄存器中。)

可能最简单的处理解除链接的方法是将一个小的堆栈帧推送到新的扩展块上,以执行返回解除新堆栈块并释放分配内存的例程。然后它将处理器寄存器返回到调用时导致堆栈需要扩展的状态。

这种设计的优点是功能输入序列非常少,并且在非扩展情况下速度非常快。缺点是,在堆栈确实需要扩展的情况下,处理器会引发异常,这可能比函数调用花费更多。

如果我理解正确,Go实际上并不使用警卫页面。相反,函数prolog明确地检查了堆栈限制,如果新的堆栈帧不适合,它调用一个函数来扩展堆栈。

Go 1.3将其设计更改为不使用堆栈块的链接列表。这是为了避免陷阱成本,如果扩展边界以某种调用模式多次在两个方向上交叉。他们从一个小堆栈开始,并使用类似的机制来检测扩展需求。但是,当发生堆栈扩展故障时,整个堆栈将移动到更大的块。这消除了完全解除链接的需要。

这里有不少细节。 (例如,可能无法在信号处理程序本身中执行堆栈扩展,而是处理程序可以安排暂停该线程并将其交给管理线程,可能必须使用专用信号堆栈来处理信号)

这种事情的另一个常见模式是运行时需要在当前栈帧下面有一定量的有效栈空间,用于类似信号处理程序或调用运行时特殊例程。 Go以这种方式工作,并且堆栈限制测试保证在当前帧下面有一定数量的堆栈空间可用。人们可以例如调用堆栈上的纯C函数,只要保证它们不会消耗超过固定堆栈保留量。在栈帧(人们可以用它来调用C库函数理论,虽然大多数的这些有他们可能多少堆栈使用没有正式的规范。)

动态分配,如ALLOCA或堆栈分配的变长数组,为实施增加了一些复杂性。如果例程可以在序言中计算帧的整个最终大小,那么它是相当简单的。在例程运行时,帧大小的任何增加都可能必须建模为新的调用,尽管Go的新架构允许移动堆栈,但例程中的alloca点可以做成使得所有状态都允许一个堆栈移动发生在那里。