LLVM 中的分段堆栈

简介

分段堆栈允许增量分配堆栈空间,而不是在线程初始化时分配一个整体块(具有某种最坏情况的大小)。这是通过分配堆栈块(以下称为堆栈段)并将它们链接到双向链表中来完成的。函数序言负责检查当前堆栈段是否有足够的空间供函数执行;如果空间不足,则调用 libgcc 运行时来分配更多堆栈空间。分段堆栈通过 LLVM 函数上的 "split-stack" 属性启用。

运行时功能已经存在于 libgcc 中

实现细节

分配堆栈段

如上所述,函数序言检查当前堆栈段是否有足够的空间。当前的方法是使用 TCB 中的一个槽来存储当前的堆栈限制(减去分配新块所需的空间量) - 这个槽的偏移量再次由 libgcc 决定。在 x86-64 上的生成汇编代码如下所示

  leaq     -8(%rsp), %r10
  cmpq     %fs:112,  %r10
  jg       .LBB0_2

  # More stack space needs to be allocated
  movabsq  $8, %r10   # The amount of space needed
  movabsq  $0, %r11   # The total size of arguments passed on stack
  callq    __morestack
  ret                 # The reason for this extra return is explained below
.LBB0_2:
  # Usual prologue continues here

堆栈上函数参数的大小需要传递给 __morestack(此函数在 libgcc 中实现),因为这些字节数必须从前一个堆栈段复制到当前的堆栈段。这是为了使函数参数的 SP(和 FP)相对寻址按预期工作。

不寻常的 ret 是使调用 __morestack 的函数正确返回所必需的。__morestack 没有返回,而是调用了 .LBB0_2。这是可能的,因为 ret 指令的大小和调用 __morestack 的 PC 都是已知的。当函数体返回时,控制权会返回给 __morestack__morestack 然后会释放新的堆栈段,恢复正确的 SP 值,并执行第二次返回,这将控制权返回给正确的调用者。

可变大小的 Allocas

分配堆栈段部分自动假定每个堆栈帧的大小都是固定的。但是,LLVM 允许使用 llvm.alloca intrinsic 在堆栈上分配动态大小的内存块。当遇到这样的可变大小的 alloca 时,会生成代码来

  • 检查当前堆栈段是否有足够的空间。如果空间足够,就像正常情况一样,只需增加 SP。

  • 如果空间不足,则生成对 libgcc 的调用,它从堆中分配内存。

从堆中分配的内存被链接到当前堆栈段中的列表中,并与当前堆栈段一起释放。这可以防止内存泄漏。