协程栈概述

说明

计算机中的栈一个很大的应用场合使用在函数调用中。我们这里简单说说golang的协程栈布局,学过计算机的应该都不会陌生。

程序事例

package main

func f(a, b int) int {
    sum := 0
    sum = a + b
    for i := 0; i < 1000; i++ {
        println("sum is:", sum)
    }
    return sum
}

func main() {
    f(1, 2)
}

汇编代码
(gdb) disas
Dump of assembler code for function main.main:
0x00000000004010b0 <main.main+0>:       mov    %fs:0xfffffffffffffff8,%rcx
0x00000000004010b9 <main.main+9>:       cmp    0x10(%rcx),%rsp
0x00000000004010bd <main.main+13>:      jbe    0x4010de <main.main+46>
0x00000000004010bf <main.main+15>:      sub    $0x18,%rsp
0x00000000004010c3 <main.main+19>:      movq   $0x1,(%rsp)
0x00000000004010cb <main.main+27>:      movq   $0x2,0x8(%rsp)
0x00000000004010d4 <main.main+36>:      callq  0x401000 <main.f>
0x00000000004010d9 <main.main+41>:      add    $0x18,%rsp
0x00000000004010dd <main.main+45>:      retq

0x00000000004010de <main.main+46>:      callq  0x44abd0 <runtime.morestack_noctxt>
0x00000000004010e3 <main.main+51>:      jmp    0x4010b0 <main.main>
0x00000000004010e5 <main.main+53>:      add    %al,(%rax)
0x00000000004010e7 <main.main+55>:      add    %al,(%rax)
0x00000000004010e9 <main.main+57>:      add    %al,(%rax)
0x00000000004010eb <main.main+59>:      add    %al,(%rax)
0x00000000004010ed <main.main+61>:      add    %al,(%rax)
0x00000000004010ef <main.main+63>:      add    %ah,-0x75(%rax,%rcx,2)
End of assembler dump.
(gdb) disas
Dump of assembler code for function main.f:
0x0000000000401000 <main.f+0>:  mov    %fs:0xfffffffffffffff8,%rcx
0x0000000000401009 <main.f+9>:  cmp    0x10(%rcx),%rsp
0x000000000040100d <main.f+13>: jbe    0x401097 <main.f+151>
0x0000000000401013 <main.f+19>: sub    $0x20,%rsp
0x0000000000401017 <main.f+23>: mov    0x28(%rsp),%rbx
0x000000000040101c <main.f+28>: mov    0x30(%rsp),%rbp
0x0000000000401021 <main.f+33>: add    %rbp,%rbx
0x0000000000401024 <main.f+36>: mov    %rbx,0x10(%rsp)
0x0000000000401029 <main.f+41>: xor    %eax,%eax
0x000000000040102b <main.f+43>: mov    %rax,0x18(%rsp)
0x0000000000401030 <main.f+48>: cmp    $0x3e8,%rax
0x0000000000401036 <main.f+54>: jge    0x401088 <main.f+136>
......
0x0000000000401088 <main.f+136>:        mov    0x10(%rsp),%rbx
0x000000000040108d <main.f+141>:        mov    %rbx,0x38(%rsp)
0x0000000000401092 <main.f+146>:        add    $0x20,%rsp
执行过程中stack变化情况

在main调用f()时,协程堆栈情况:

golang函数调用stack分布

注意:这里的返回地址是由call指令自动push至esp所指向内存,而参数内容则是由调用者main函数设置的,如下代码:

// we have 2 argument and 1 return value
// so must reserve 24 bytes in amd64(0x18)
0x00000000004010bf <main.main+15>:      sub    $0x18,%rsp
0x00000000004010c3 <main.main+19>:      movq   $0x1,(%rsp)
0x00000000004010cb <main.main+27>:      movq   $0x2,0x8(%rsp)
0x00000000004010d4 <main.main+36>:      callq  0x401000 <main.f>

在f函数内部执行时,会扩充当前stack,为了临时存储一些本地变量,如sum等,则f执行时堆栈情况如下:

golang函数调用stack分布

可以看到为本地变量sum和i自动在栈上分配了存储空间,计算sum,然后将sum的值存储到f()返回值该去的地方((esp) + 0x38)

可以简单看看main.f()的主要汇编代码

// sub esp to allocate space for local variable
0x0000000000401013 <main.f+19>: sub    $0x20,%rsp
// get parameters, compute and store sum 
0x0000000000401017 <main.f+23>: mov    0x28(%rsp),%rbx
0x000000000040101c <main.f+28>: mov    0x30(%rsp),%rbp
0x0000000000401021 <main.f+33>: add    %rbp,%rbx
// store sum in (esp) + 0x10
0x0000000000401024 <main.f+36>: mov    %rbx,0x10(%rsp)
// for loop assemble code
0x0000000000401029 <main.f+41>: xor    %eax,%eax
0x000000000040102b <main.f+43>: mov    %rax,0x18(%rsp)
0x0000000000401030 <main.f+48>: cmp    $0x3e8,%rax
0x0000000000401036 <main.f+54>: jge    0x401088 <main.f+136>
......
// store sum into return value address(esp + 0x38)
// and shrink stack((%esp) + 0x20) and return to main 
0x0000000000401088 <main.f+136>:        mov    0x10(%rsp),%rbx
0x000000000040108d <main.f+141>:        mov    %rbx,0x38(%rsp)
0x0000000000401092 <main.f+146>:        add    $0x20,%rsp