go在stack上干了神马?

  1. 对这个话题已经有深入理解的童鞋请绕道;
  2. 对这个话题感兴趣,且有极强学习能力的同学请阅读这里,并且不用回来了。
  3. 其他和我一样愚笨的IT民工们,继续向前冲吧……
  4. 【2013年3月21日】以下关于 stackless 的描述有致命的脑残错误,请忽略。感谢 @minux 指出。

首先,来看一段神奇的 golang 代码:

package main
var (
    i = 1
)
func main() {
    i = i + 1
    print(i, "\n")
    main()
}


熟悉 c 语言的人都知道,如果在 c 语言中编译执行类似的代码,程序最终会发生栈溢出(stack overflow),从而导致段错误(segmentation fault)。在 32 位环境下(我只有 32 位的实验环境)编译链接并执行这段代码,预期的“段错误”并没有出现。

程序长时间稳定的执行。同时,利用 top 命令,会看到程序所使用的内存不断上升,丝毫没有减少的痕迹。

祭出神器 gdb,来看一下这个 go 程序到底做了什么。

gdb stackless
...
(gdb) b main.main
...
(gdb) r
...
(gdb) disassemble

当输入 disassemble 命令后,gdb 帮我们反编译了程序代码:

   0x08048c00 <+0>:	mov    %gs:0x0,%ecx
   0x08048c07 <+7>:	mov    -0x8(%ecx),%ecx
   0x08048c0a <+10>:	cmp    (%ecx),%esp
   0x08048c0c <+12>:	ja     0x8048c17 <main.main+23>
   0x08048c0e <+14>:	xor    %edx,%edx
   0x08048c10 <+16>:	xor    %eax,%eax
   0x08048c12 <+18>:	call   0x8049515 <runtime.morestack>
   0x08048c17 <+23>:	sub    $0x1c,%esp
   0x08048c1a <+26>:	mov    0x806c00c,%eax
   0x08048c20 <+32>:	inc    %eax
   0x08048c21 <+33>:	mov    %eax,0x806c00c
   0x08048c27 <+39>:	cltd   
   0x08048c28 <+40>:	mov    %eax,(%esp)
   0x08048c2b <+43>:	mov    %edx,0x4(%esp)
   0x08048c2f <+47>:	call   0x804ef1b <runtime.printint>
   0x08048c34 <+52>:	lea    0x80557b0,%esi
   0x08048c3a <+58>:	lea    (%esp),%edi
   0x08048c3d <+61>:	cld    
   0x08048c3e <+62>:	movsl  %ds:(%esi),%es:(%edi)
   0x08048c3f <+63>:	movsl  %ds:(%esi),%es:(%edi)
   0x08048c40 <+64>:	call   0x804f099 <runtime.printstring>
   0x08048c45 <+69>:	call   0x8048c00 <main.main>
   0x08048c4a <+74>:	add    $0x1c,%esp
   0x08048c4d <+77>:	ret 

main.main+0到main.main+12处,我们看到取了两个值进行比较,当 (%ecx)大于%esp时,跳转到main.main+23。

main.main+23到main.main+64处,显然是我们程序的逻辑处理:变量i自加一、打印i、打印”\n”。

main.main+69再次调用地址 0x8048c00 的 main.main。

除了main.main+0到main.main+12的比较,main.main+14到main.main+18对 runtime.morestack 的调用外,这和一个普通的程序并无二至。

那么程序一开始比较两个数据和调用 runtime.morestack 是什么呢?

原来在程序一开始先比较了栈限制和已经使用的栈的大小(%esp)。当栈限制大于已经使用的栈的时候,说明空间充裕,则直接跳转到实际代码处执行(+23)。当栈限制小于等于时已经使用的栈时,执行 runtime.morestack(+18)申请更多的栈空间。

在 $(GOROOT)/src/pkg/runtime/stack.h 中,有略微详细的说明。

由此看来 golang 实现的并不是 stackless,而是在每次函数调用前判断栈空间是否足够,如果发现栈空间不够用,就立刻申请新的栈空间使用。当函数退出时,才释放这些栈空间。

还是在 gdb 中,执行:

(gdb) b runtime.morestack
...
(gdb) r

在函数调用 100 多次后(猜测 32 位跟 64 位应当不同,没试),触发了 runtime.morestack 申请新的栈空间。

Join the Conversation

2 Comments

Leave a comment

Your email address will not be published. Required fields are marked *