Golang 从汇编看函数调用

现在有如下代码

package main

import "fmt"

func main() {
    a := 1
    a := test(a)
}

func test(a int) int {
    return a + 100
}

go tool compile -N -l -S 1.go 之后汇编代码如下

// main 方法
"".main STEXT size=78 args=0x0 locals=0x20
                // TEXT 在 plan9 汇编里作为一个指令来定义函数,这里定义函数 main,
        // SB 是个伪寄存器,用来声明函数或全局变量
        // $32 即 32,plan9 汇编里数字前都加上了 $。32 表示栈帧长度,可以理解为局部变量的长度。
        // 0 是传入和传出参数的长度
        0x0000 00000 (1.go:3)   TEXT    "".main(SB), $32-0
        // 将线程本地存储(thread local storage) 传送到 CX 寄存器。
        0x0000 00000 (1.go:3)   MOVQ    (TLS), CX
        // 检查栈帧的大小是否超过目前分配的大小
        0x0009 00009 (1.go:3)   CMPQ    SP, 16(CX)
        0x000d 00013 (1.go:3)   JLS     71
        // 栈上分配 32 字节的空间
        0x000f 00015 (1.go:3)   SUBQ    $32, SP
        // 将基址指针存储到栈上(24)BP,一个字节
        0x0013 00019 (1.go:3)   MOVQ    BP, 24(SP)
        // 把 24(SP) 的地址放到 BP 里。
        0x0018 00024 (1.go:3)   LEAQ    24(SP), BP
        // FUNCDATA 是 golang 编译器自带的指令,用来给 gc 收集进行提示。提示 0 和 1 是用于局部函数调用的参数,需要进行回收。
        0x001d 00029 (1.go:3)   FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (1.go:3)   FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        // 定义局部变量 a = 1
        0x001d 00029 (1.go:4)   MOVQ    $1, "".a+16(SP)
        // 将 1 赋值到 SP 寄存器
        // 第一个参数。存在 0(SP)
        0x0026 00038 (1.go:5)   MOVQ    $1, (SP)
        0x002e 00046 (1.go:5)   PCDATA  $0, $0
        // 调用 test 方法
        0x002e 00046 (1.go:5)   CALL    "".test(SB)
        0x0033 00051 (1.go:5)   MOVQ    8(SP), AX
        0x0038 00056 (1.go:5)   MOVQ    AX, "".a+16(SP)
        0x003d 00061 (1.go:6)   MOVQ    24(SP), BP
        0x0042 00066 (1.go:6)   ADDQ    $32, SP
        0x0046 00070 (1.go:6)   RET
        0x0047 00071 (1.go:6)   NOP
        0x0047 00071 (1.go:3)   PCDATA  $0, $-1
        // 扩大栈帧空间
        0x0047 00071 (1.go:3)   CALL    runtime.morestack_noctxt(SB)
        0x004c 00076 (1.go:3)   JMP     0

// test 方法部分      
"".test STEXT nosplit size=24 args=0x10 locals=0x0
        0x0000 00000 (1.go:8)   TEXT    "".test(SB), NOSPLIT, $0-16
        0x0000 00000 (1.go:8)   FUNCDATA        $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
        0x0000 00000 (1.go:8)   FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        // 16(SP) 应该是返回值的地址,这里先给他赋值为 0 了。
        0x0000 00000 (1.go:8)   MOVQ    $0, "".~r1+16(SP)
        // 参数在 +8(SP), 赋值到 AX
        0x0009 00009 (1.go:9)   MOVQ    "".a+8(SP), AX
        // AX 的 值加 100,然后存到 AX
        0x000e 00014 (1.go:9)   ADDQ    $100, AX
        // 将 AX 存储到 +16(SP)
        0x0012 00018 (1.go:9)   MOVQ    AX, "".~r1+16(SP)
        0x0017 00023 (1.go:9)   RET

堆栈分配的具体清理

这里重新理一下堆栈的分配情况
首先是什么都没分配的时候

然后 SUBQ $32, SP 初始化 32 字节。

MOVQ    BP, 24(SP)
LEAQ    24(SP), BP

把 BP 的值放到 24(SP) 里。然后把 24(SP) 的地址放到 BP 里。

然后执行 MOVQ $1, "".a+16(SP) 初始化第一个局部变量。

接下来执行 MOVQ $1, (SP),填写调用函数 test 的参数。

可以发现这中间预留了 8 字节即 8(SP),这是留给 test 填写返回值的。

一切准备好后就执行 CALL "".test(SB) 准备调用 test() 函数。call 这个指令会把当前函数 main 的返回值地址压栈,并改变当前的栈指针。

然后执行 test() 函数的内容

// 初始化填写返回值的空间
MOVQ    $0, "".~r1+16(SP)
// 参数在 +8(SP), 赋值到 AX
MOVQ    "".a+8(SP), AX
// AX 的 值加 100,然后存到 AX
ADDQ    $100, AX
// 将 AX 存储到 +16(SP)
MOVQ    AX, "".~r1+16(SP)
RET

test() 函数执行完后栈空间就是这样了

最后 main 函数里还会执行

MOVQ    24(SP), BP
ADDQ    $32, SP`

来恢复栈基址指针并销毁已经失去作用的 32 字节空间。

参考
Golang汇编层面代码分析-内置函数和过程调用
理解 Golang 中函数调用的原理