go在stack上干了神马?

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

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

<br />
package main<br />
var (<br />
    i = 1<br />
)<br />
func main() {<br />
    i = i + 1<br />
    print(i, &quot;\n&quot;)<br />
    main()<br />
}<br />


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

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

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

<br />
gdb stackless<br />
...<br />
(gdb) b main.main<br />
...<br />
(gdb) r<br />
...<br />
(gdb) disassemble<br />

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

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

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 中,执行:

<br />
(gdb) b runtime.morestack<br />
...<br />
(gdb) r<br />

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

2 thoughts on “go在stack上干了神马?”

Leave a Reply

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