Go函数调用布局

函数具有局部变量(栈帧大小大于0)

从上图我们可以看到,函数调用时,参数和返回值是通过栈来传递的,通过栈来传递函数,能够很好的实现多返回值,而且当发生goroutine的调度时,只需要切换SP/BP等少量寄存器,而不需要对通用寄存器进行切换,而且,当我们使用命名返回值时,是直接在对应的栈上进行更新,因此我们可以在defer函数内更新返回值。

测试代码
1
2
3
4
func frameInfo(i int) (uintptr, uintptr, uintptr, uintptr,int,int)
func main() {
fmt.Println(frameInfo(15))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TEXT ·frameInfo(SB),$8-54 // 这里8表示函数局部栈帧8个字节,参数和返回值共54个字节
MOVQ SP, AX // 取硬件寄存器SP的内容,作为第一个返回值
MOVQ AX, ret0+8(FP)
LEAQ i+0(SP), AX // 取pseudo_sp的值,作为第二个返回值
MOVQ AX, ret2+16(FP)
MOVQ BP, AX // 取硬件寄存器BP的值,作为第三个返回值
MOVQ AX, ret1+24(FP)
LEAQ i+0(FP), AX // 取pseudo_fp的值,作为第四个返回值
MOVQ AX, ret3+32(FP)
MOVQ i+0(FP), AX // 通过伪寄存器fp获取参数i作为第五个返回值
MOVQ AX, ret4+40(FP)
MOVQ i+16(SP), AX // 通过伪寄存器sp获取参数i作为第六给返回值
MOVQ AX, ret5+48(FP)
RET

查看输出内容:

1
2
$ go run .
824634146480 824634146488 824634146488 824634146504 15 15

可以看到伪寄存器SP和栈底寄存器BP指向的是同一个地址,而伪寄存器FP比伪寄存器SP16个字节,其中高8个字节保存函数的返回地址,而低8个字节保存函数调用者的BP;而硬件SP和伪寄存器SP之间相差的字节数刚好是函数栈帧(这里的函数栈帧不包含保存调用者BP8个字节)的大小,可以通过调整函数的局部栈帧大小观察其关系

1
TEXT ·frameInfo(SB),$16-54 // 调整栈帧大小为16字节

查看输出内容:

1
2
$ go run .
824634187432 824634187448 824634187448 824634187464 15 15

可以看到,新的输出中,硬件SP和伪寄存器SP之间相差为16字节,正好为栈帧大小。

函数栈帧大小为0

当函数栈帧大小为0时,情况就有点不同了。

因为这个时候,当前函数没有分配栈帧,因此硬件寄存器BP不需要保存当前函数栈的栈底,也就不需要在栈上额外分配一个8字节的空间来保存函数调用者的BP寄存器内容,也就是说当前BP寄存器直接保存的就是函数调用者的BP寄存器信息。

这时候的栈结构:

测试代码
1
2
3
4
5
6
func getBp() (uintptr, uintptr)
func zeroFrame(i int) (uintptr, uintptr, uintptr, uintptr, int, int, uintptr)
func main() {
fmt.Println(getBp())
fmt.Println(zeroFrame(11))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TEXT ·getBp(SB),$8-16
MOVQ 0(BP), AX // 获取调用者函数的栈底地址
MOVQ AX, ret0+0(FP)
MOVQ ra+8(SP), AX // 获取当前函数的返回地址
MOVQ AX, ret0+8(FP)
RET

TEXT ·zeroFrame(SB),$0-64
MOVQ SP, AX // 取硬件寄存器SP的内容,作为第一个返回值
MOVQ AX, ret0+8(FP)
LEAQ i+0(SP), AX // 取pseudo_sp的值,作为第二个返回值
MOVQ AX, ret1+16(FP)
MOVQ BP, AX // 取硬件寄存器BP的值,作为第三个返回值
MOVQ AX, ret2+24(FP)
LEAQ i+0(FP), AX // 取pseudo_fp的值,作为第四个返回值
MOVQ AX, ret3+32(FP)
MOVQ i+0(FP), AX // 通过伪寄存器fp获取参数i作为第五个返回值
MOVQ AX, ret4+40(FP)
MOVQ i+8(SP), AX // 通过伪寄存器sp获取参数i作为第六给返回值
MOVQ AX, ret5+48(FP)
MOVQ addr+0(SP), AX // 获取当前函数的返回地址
MOVQ AX, ret6+56(FP)
RET

查看输出内容:

1
2
3
$ go run .
824634187656 4776270
824634187392 824634187392 824634187656 824634187400 11 11 4776438

函数getBp返回了调用者也即main函数的栈底信息,和当前函数的返回地址

然后调用函数zeroFrame,该函数栈帧大小为0,可以看到执行该函数时,BP寄存器依然是824634187656,而且硬件寄存器SP和伪寄存器SP指向同一个位置,并且和伪寄存器FP只相差8个字节,而这8个字节保存的是当前函数的返回地址,我们可以看到两个函数的返回地址是在同一个段中的。