分段栈技术

2012-11-30

gcc中的splitstack技术原理

http://gcc.gnu.org/wiki/SplitStacks

分段栈的重要意义就在于,栈空间初始分配很小的大小,然后可以随便需要自动地增长栈空间.这样在多线程环境中就可以开千千万万个线程或协程而不至于耗尽内存.

基本实现

%gs寄存器存一个tcb结构的地址,go语言中是G这个结构体.这个结构中存了栈基址和stack_guard

对于使用分段栈的函数,每次进入函数后,前几条指令就是先检测%esp跟stack_guard比较,如果超了就要扩展栈空间

扩展栈

所有用于分配栈空间的函数本身不能使用分段栈.引入一个属性来控制函数的生成.当然,还要保证有足够的空间来调用分配函数。编译器在编译阶段加入特殊的标识.note.GNU-split-stack,链接时对于检测到有这些标签的函数,就会插入特殊指令。扩展完之后,使用新栈之间,需要一些处理

基于原栈%esp的数据都可以直接复制到新栈中来.对于栈中存的是对象的地址,这样做不会造成问题。对于带参函数,因为参数不能直接搬,编译时要特殊处理.函数使用的参数指针不是基于栈帧的.对于在栈中返回对象的函数,对象必须返回到原栈上

扩展栈时,函数的返回地址会被修改成一个函数,这个函数会释放分配的栈块,将栈指针重新设置成调用者旧栈块的地址,栈指针等,需要在新栈空间中的某处保存着.

兼容性

GCC中使用split栈的函数,编译split栈的函数,会加入.note.GNU-split-stack信息.链接时如果有这些东西,就会链接上split栈的相关runtime库.在gcc实现的split栈中要hack exit函数以便最后退出时处理这些分裂栈空间.

go语言中的具体实现

go语言使用的就是分段栈,这样可以起很多个goroutine. http://blog.nella.org/?p=849

这个上面讲的gcc怎么实现splitstack的,其作者正是gccgo的作者.在go语言的实现中其实思想和方法跟上面都是一致的.

进入函数后的前几条指令就是取%gs到%ecb 这时获得了结构体G的地址.这个结构体前两个域就是stackguard和stackbase

我还观察到,好象go编译的程序,没有使用%ebp,可能是因为G中已经存的栈基址的缘故.检测stackguard和%esp,如果空间不够了就会调用到runtime.morestack.这是一个汇编函数,在asm386.s文件中可以找到

TEXT runtime.morestack (SB),7,$0

其中这个7就是告诉编译器这个函数不使用分段栈
runtime.morestack会把一些信息存到结构体M
DX中是frame size, AX中是arg size,这些会被保存到M结构体,还有函数返回地址,保存以后这些东西在后面会清空,然后新栈和旧栈信息可以link起来.

当morestack函数保存好需要的东西以后,它切换到调度器的栈,然后将控制器交给runtime.newstack

注意调用到runtime.newstack的方式是CALL,并且用的是调度器的栈,函数的退出有点特殊.

栈空间的分配使用的普通的go runtime的空间分配技术,也就是会垃圾回收.但也有些特殊,也不完全是直接从垃圾回收的池子中来,回来垃圾回收的池子中去.

runtime.newstack不会返回到调用者morestack.不考虑reflect相关的东西,它做的事情就是分配一块内存,在头部放一个Stktop的结构体,特殊方式退出.
清除栈时,新栈中保存的这些栈桢的信息会起作用.

退出使用的是gogocall,是调度器实现上下文切换上函数.相当于直接jump过去的而不是函数调用协议那样过去的.保存的函数返回地址被设置为一个后处理的函数,这样遇到下一次RET指令时,会jump到more.lessstack函数,这个函数做的正好是跟morestack函数相反的工作.然后就到新栈中去工作了.

再重复一遍整个过程:

  1. 使用分段栈的函数头几个指令检测%esp和stackguard,调用于runtime.morestack
  2. runtime.more函数的主要功能是保存当前的栈的一些信息.然后转换成调试器的栈了调用runtime.newstack
  3. runtime.newstack函数的主要功能是分配空间,装饰此空间,将旧的frame和arg弄到新空间
  4. 使用gogocall的方式切换到新分配的栈,gogocall使用的JMP返回到被中断的函数
  5. 继续执行遇到RET指令时会返回到runtime.less,less做的事情跟more相反,它要准备好从newstack到old stack 整个过程有点像一次中断,中断处理时保存当时的现场,弄个新的栈,中断恢复时恢复到新栈中运行,运行到return时又要从runtime.less走回去

结语

本文是对splitstack的一个学习,主要是gcc中分段栈的原理和go语言的具体实现方式.

golang