0

深入理解 Go | 函数调用

 2 years ago
source link: https://ictar.github.io/2020/03/24/dive-into-go-func-call/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入理解 Go | 函数调用

发表于

2020-03-24 更新于 2020-12-25

Disqus: 0 Comments

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

以下基于 Go1.14

考虑以下代码:

package main

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

func main() {
do(33, 66)
}
然后运行以下命令查看对应的汇编指令:
$ 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 的返回地址压入栈中,然后进行函数调用。

"".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 函数修改为支持变长参数,如下所示:

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 作为参数传入到调用的函数中。
"".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 的返回值:
func do(a, b int) (res int, equal bool) {
return a + b, a == b
}
然后查看汇编,可以发现,main 在进行函数调用的时候基本没有改动,但是被调用函数在执行的时候多了几项操作。在执行前,会申请额外的栈空间来存放临时返回值。然后,初始化命名返回值,进行一系列操作后,把返回结果保存在临时返回值中。最后,再使用临时返回值一一设置命名返回值。接着释放存放临时返回值的栈空间,返回到函数调用点。
"".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
也就是说,如果对于非命名返回值,执行逻辑为:
func do(a, b int) (int, bool) {
r1, r2 := a+b, a == b
return r1, r2
}
那么命名返回值的执行逻辑为:
func do(a, b int) (res int, equal bool) {
tmp1, tmp2 := a+b, a == b
res = tmp1
equal = tmp2
return res, equal
}
## 总结 golang 的函数调用具有以下特征: * 参数完全通过栈传递,从参数列表右至左压栈(第一个参数在栈顶) * 返回值通过栈传递 * 调用者负责清理栈空间


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK