分段堆栈如何工作

Cur*_*ous 8 c++ go fiber goroutine boost-coroutine

分段堆栈如何工作?这个问题也适用于Boost.Coroutine我所以我也在使用C++标签.主要的疑问来自这篇文章看起来他们所做的是在堆栈的底部保留一些空间并通过在那里分配的内存(可能通过mmapmprotect?)注册某种信号处理程序来检查它是否已经损坏了当他们发现他们已经耗尽了空间时,他们继续分配更多的内存,然后从那里继续.3个问题

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

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

  2. 他们也说

    在我们得到一个新的堆栈段之后,我们goroutine通过重试导致我们用完堆栈的函数来重新启动它

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

  3. 他们如何检测到程序已超出堆栈?如果他们在底部保留一个canary-ish内存区域,那么当用户程序创建一个足够大的数组溢出时会发生什么?这不会导致堆栈溢出并且是一个潜在的安全漏洞吗?

如果Go和Boost的实现不同,我很高兴知道它们中的任何一个如何处理这种情况

Zal*_*ern 8

我将简要介绍一种可能的实现方式.

首先,假设大多数堆栈帧小于某个大小.对于较大的那些,我们可以在入口处使用更长的指令序列以确保有足够的堆栈空间.让我们假设我们的架构有4k页,我们选择4k - 1作为快速路径处理的最大大小堆栈帧.

堆栈在底部分配有单个保护页面.也就是说,未映射用于写入的页面.在函数入口处,堆栈指针按堆栈帧大小递减,该堆栈帧大小小于页面的大小,然后程序安排在新分配的堆栈帧中的最低地址处写入值.如果已达到堆栈的末尾,则此写入将导致处理器异常并最终变为从OS到用户程序的某种上行调用 - 例如UNIX系列OS中的信号.

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

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

处理unchaining的最简单方法可能是将一个小的堆栈帧推送到新的扩展块上,以获得一个例程,该例程在返回时取消链接新的堆栈块并释放分配的内存.然后,它会将处理器寄存器返回到调用时所处的状态,从而导致需要扩展堆栈.

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

如果我理解正确,Go实际上并没有使用保护页面.相反,函数prolog显式检查堆栈限制,如果新的堆栈帧不适合,则调用函数来扩展堆栈.

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

这里有很多细节.(例如,可能无法在信号处理程序本身中执行堆栈扩展.相反,处理程序可以安排将线程挂起并将其交给管理器线程.可能必须使用专用信号堆栈来处理信号同样.)

另一种常见的模式是运行时要求在当前堆栈帧下面有一定数量的有效堆栈空间,用于信号处理程序或运行时调用特殊例程.Go以这种方式工作,堆栈限制测试保证当前帧下方有一定量的堆栈空间可用.可以例如在堆栈上调用普通C函数,只要保证它们不会消耗超过固定堆栈保留量.(理论上可以使用它来调用C库例程,尽管其中大多数都没有正式规范它们可能使用多少堆栈.)

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