深入理解 Go | 函数调用

概述

对于函数调用,Go 语言使用调用者预先分配的栈来传递参数和返回值,使得多值返回成为可能。

以下基于 Go1.14

考虑以下代码:

1
2
3
4
5
6
7
8
9
package main

func do(a, b int) (int, bool) {
return a + b, a == b
}

func main() {
do(33, 66)
}

然后运行以下命令查看对应的汇编指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ go tool compile -S -N -l hello.go
....
"".main STEXT size=68 args=0x0 locals=0x28
0x0000 00000 (hello.go:7) TEXT "".main(SB), ABIInternal, $40-0
0x0000 00000 (hello.go:7) MOVQ (TLS), CX
0x0009 00009 (hello.go:7) CMPQ SP, 16(CX)
0x000d 00013 (hello.go:7) PCDATA $0, $-2
0x000d 00013 (hello.go:7) JLS 61
0x000f 00015 (hello.go:7) PCDATA $0, $-1
0x000f 00015 (hello.go:7) SUBQ $40, SP # 分配 40 字节的栈空间
0x0013 00019 (hello.go:7) MOVQ BP, 32(SP) # 保存基址指针 BP 到栈上
0x0018 00024 (hello.go:7) LEAQ 32(SP), BP # 修改基址指针
0x001d 00029 (hello.go:7) PCDATA $0, $-2
0x001d 00029 (hello.go:7) PCDATA $1, $-2
0x001d 00029 (hello.go:7) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (hello.go:7) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (hello.go:7) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (hello.go:8) PCDATA $0, $0
0x001d 00029 (hello.go:8) PCDATA $1, $0
0x001d 00029 (hello.go:8) MOVQ $33, (SP) # 第一个参数
0x0025 00037 (hello.go:8) MOVQ $66, 8(SP) # 第二个参数
0x002e 00046 (hello.go:8) CALL "".do(SB) # 将当前的 IP 压入栈中,然后调用函数 do
0x0033 00051 (hello.go:9) MOVQ 32(SP), BP # 恢复基址指针
0x0038 00056 (hello.go:9) ADDQ $40, SP # 回收分配的栈空间
0x003c 00060 (hello.go:9) RET
......

从上面可以看出,在进行函数调用前,会为调用的函数分配栈空间(用于入参和返回值)、保存基址指针并修改其指向,然后将入参压入栈中(第一个参数在栈顶,以此类推)。

接着通过 CALL 指令,将 main 的返回地址压入栈中,然后进行函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
"".do STEXT nosplit size=45 args=0x20 locals=0x0
......
0x0000 00000 (hello.go:3) MOVQ $0, "".~r2+24(SP) # 初始化第一个返回值
0x0009 00009 (hello.go:3) MOVB $0, "".~r3+32(SP) # 初始化第二个返回值
0x000e 00014 (hello.go:4) MOVQ "".a+8(SP), AX # 获取第一个参数,AX = 33
0x0013 00019 (hello.go:4) ADDQ "".b+16(SP), AX # 做加法,AX = AX + 66 = 99
0x0018 00024 (hello.go:4) MOVQ AX, "".~r2+24(SP) # 把计算结果保存在第一个返回值中,24(SP) = AX = 99
0x001d 00029 (hello.go:4) MOVQ "".b+16(SP), AX # AX = 66
0x0022 00034 (hello.go:4) CMPQ "".a+8(SP), AX # 8(SP) 和 AX 进行比较,即 AX - 8(SP) = 66 - 33 = 33
0x0027 00039 (hello.go:4) SETEQ "".~r3+32(SP) # 判断上一步的计算结果是否为 0,保存在第二个返回值中,32(SP) = 0
0x002c 00044 (hello.go:4) RET # 修改 IP,返回调用点
......

被调用的函数在执行的时候,会将调用栈里面保存返回值的位置初始化为 0,然后进行一系列的操作后,将返回值保存在栈中,最后返回该函数的调用点。

参数传递

变长参数

do 函数修改为支持变长参数,如下所示:

1
2
3
4
5
6
7
8
func do(nums ...int) {
fmt.Printf("%T %v\n", nums, nums)
}

func main() {
do() // output: []int []
do(33, 44, 55) // output: []int [33 44 55]
}

可以看到,变长参数其实是一个语法糖,golang 会为这些变长参数隐式创建一个 slice,然后再把这个 slice 作为参数传入到调用的函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"".main STEXT size=196 args=0x0 locals=0x40
……
# do(33, 44, 55)
# 创建一个大小为 3,容量为 3 的 slice,值为[33, 44, 55]
0x0036 00054 (hello.go:11) LEAQ type.[3]int(SB), AX
0x003d 00061 (hello.go:11) PCDATA $0, $0
0x003d 00061 (hello.go:11) MOVQ AX, (SP)
0x0041 00065 (hello.go:11) CALL runtime.newobject(SB)
0x0046 00070 (hello.go:11) PCDATA $0, $1
0x0046 00070 (hello.go:11) MOVQ 8(SP), AX
0x004b 00075 (hello.go:11) PCDATA $1, $1
0x004b 00075 (hello.go:11) MOVQ AX, ""..autotmp_1+24(SP)
0x0050 00080 (hello.go:11) PCDATA $0, $0
0x0050 00080 (hello.go:11) MOVQ $33, (AX)
0x0057 00087 (hello.go:11) PCDATA $0, $1
0x0057 00087 (hello.go:11) MOVQ ""..autotmp_1+24(SP), AX
0x005c 00092 (hello.go:11) TESTB AL, (AX)
0x005e 00094 (hello.go:11) PCDATA $0, $0
0x005e 00094 (hello.go:11) MOVQ $44, 8(AX)
0x0066 00102 (hello.go:11) PCDATA $0, $1
0x0066 00102 (hello.go:11) MOVQ ""..autotmp_1+24(SP), AX
0x006b 00107 (hello.go:11) TESTB AL, (AX)
0x006d 00109 (hello.go:11) PCDATA $0, $0
0x006d 00109 (hello.go:11) MOVQ $55, 16(AX)
0x0075 00117 (hello.go:11) PCDATA $0, $1
0x0075 00117 (hello.go:11) PCDATA $1, $0
0x0075 00117 (hello.go:11) MOVQ ""..autotmp_1+24(SP), AX
0x007a 00122 (hello.go:11) TESTB AL, (AX)
0x007c 00124 (hello.go:11) JMP 126
0x007e 00126 (hello.go:11) MOVQ AX, ""..autotmp_0+32(SP)
0x0083 00131 (hello.go:11) MOVQ $3, ""..autotmp_0+40(SP)
0x008c 00140 (hello.go:11) MOVQ $3, ""..autotmp_0+48(SP)
0x0095 00149 (hello.go:11) PCDATA $0, $0
0x0095 00149 (hello.go:11) MOVQ AX, (SP)
0x0099 00153 (hello.go:11) MOVQ $3, 8(SP)
0x00a2 00162 (hello.go:11) MOVQ $3, 16(SP)
# 调用 do 函数
0x00ab 00171 (hello.go:11) CALL "".do(SB)
0x00b0 00176 (hello.go:12) MOVQ 56(SP), BP
0x00b5 00181 (hello.go:12) ADDQ $64, SP
0x00b9 00185 (hello.go:12) RET

返回值

命名返回值

我们将代码稍微修改下,命名函数 do 的返回值:

1
2
3
func do(a, b int) (res int, equal bool) {
return a + b, a == b
}

然后查看汇编,可以发现,main 在进行函数调用的时候基本没有改动,但是被调用函数在执行的时候多了几项操作。在执行前,会申请额外的栈空间来存放临时返回值。然后,初始化命名返回值,进行一系列操作后,把返回结果保存在临时返回值中。最后,再使用临时返回值一一设置命名返回值。接着释放存放临时返回值的栈空间,返回到函数调用点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"".do STEXT nosplit size=87 args=0x20 locals=0x18
0x0000 00000 (hello.go:3) TEXT "".do(SB), NOSPLIT|ABIInternal, $24-32
0x0000 00000 (hello.go:3) SUBQ $24, SP // 申请空间存放临时返回值
0x0004 00004 (hello.go:3) MOVQ BP, 16(SP)
0x0009 00009 (hello.go:3) LEAQ 16(SP), BP
0x000e 00014 (hello.go:3) PCDATA $0, $-2
0x000e 00014 (hello.go:3) PCDATA $1, $-2
0x000e 00014 (hello.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (hello.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (hello.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (hello.go:3) PCDATA $0, $0
0x000e 00014 (hello.go:3) PCDATA $1, $0
0x000e 00014 (hello.go:3) MOVQ $0, "".res+48(SP) // 初始化第一个返回值,res = 0
0x0017 00023 (hello.go:3) MOVB $0, "".equal+56(SP) // 初始化第二个返回值,equal = 0
0x001c 00028 (hello.go:4) MOVQ "".a+32(SP), AX // AX = a = 33
0x0021 00033 (hello.go:4) ADDQ "".b+40(SP), AX // AX = AX + b = 33 + 66 = 99
0x0026 00038 (hello.go:4) MOVQ AX, ""..autotmp_4+8(SP) // 计算结果保存在临时变量中
0x002b 00043 (hello.go:4) MOVQ "".b+40(SP), AX
0x0030 00048 (hello.go:4) CMPQ "".a+32(SP), AX
0x0035 00053 (hello.go:4) SETEQ ""..autotmp_5+7(SP) // 计算结果保存在临时变量中
0x003a 00058 (hello.go:4) MOVQ ""..autotmp_4+8(SP), AX
0x003f 00063 (hello.go:4) MOVQ AX, "".res+48(SP) // 用临时变量设置命名返回值
0x0044 00068 (hello.go:4) MOVBLZX ""..autotmp_5+7(SP), AX // 用临时变量设置命名返回值
0x0049 00073 (hello.go:4) MOVB AL, "".equal+56(SP)
0x004d 00077 (hello.go:4) MOVQ 16(SP), BP
0x0052 00082 (hello.go:4) ADDQ $24, SP
0x0056 00086 (hello.go:4) RET

也就是说,如果对于非命名返回值,执行逻辑为:

1
2
3
4
func do(a, b int) (int, bool) {
r1, r2 := a+b, a == b
return r1, r2
}

那么命名返回值的执行逻辑为:

1
2
3
4
5
6
func do(a, b int) (res int, equal bool) {
tmp1, tmp2 := a+b, a == b
res = tmp1
equal = tmp2
return res, equal
}

总结

golang 的函数调用具有以下特征:

  • 参数完全通过栈传递,从参数列表右至左压栈(第一个参数在栈顶)
  • 返回值通过栈传递
  • 调用者负责清理栈空间

参考

请言小午吃个甜筒~~