函数栈 #
了解 go 的函数调用底层逻辑,能更清晰的理解 defer、recover、panic 的工作方式。go 的函数栈如下:
栈从高地址往低地址依次是:
- 栈基
- 局部标量
- 调用函数返回值
- 调用函数参数
- 函数返回地址
- 栈指针
func main() {
int a = 5;
println(a);
}
在为 main
函数分配函数栈时,会将使用两个 int 大小的空间 r1、r2 分别存储变量 a
和传入 println
函数的参数 a
。你可能会好奇为什么 a
需要占两份,因为对于 go 来说,函数参数皆是值拷贝,只是区别是拷贝目标对象还是拷贝指针,函数调用前就会一次性为形参和返回值分配内存空间,并将实参拷贝到形参中。由于 println 没有返回值,所以仅有两个变量 a
的空间。
指令 #
理解了以上后,就需要知道一些额外的指令
call 指令 #
- 将下一条指令入栈,作为返回地址
- 跳转到被调用者函数执行
执行函数前的处理:
- 入栈调用函数栈的栈基
- 分配函数栈
- 设置被调用函数的栈基(bp)
执行函数 给返回值赋值 执行 defer 函数
执行 ret 指令前的处理:
- 恢复调用函数的栈基
- 释放被调函数的函数栈
ret 指令 #
- 弹出调用前入栈的返回地址
- 跳转到该返回地址
Base Stack #
传值/传指针 #
传值
func swap(a, b int) {
a, b = b, a
}
func main() {
a, b = 1, 2
swap(a, b)
fmt.Println(a, b) // 1, 2
}
分析以上代码,main
方法在执行时会给局部变量 a
、b
,调用 swap
的两个参数(参数从右往左)分配栈空间并赋值如下:
栈帧 | |
---|---|
… | |
局部变量 a = 1 | main BP |
局部变量 b = 2 | |
swap 参数 b = 2 | |
swap 参数 a = 1 | |
返回地址 | main SP |
main BP | |
… |
swap
方法中会将参数 a
,b
交换:
栈帧 | |
---|---|
… | |
局部变量 a = 1 | main BP |
局部变量 b = 2 | |
swap 参数 b = 1 | |
swap 参数 a = 2 | |
返回地址 | main SP |
main BP | |
… |
可以看到,swap
方法并没有修改调用者的变量,因此 main
方法中的 a
,b
交换失败了。
传指针
func swap(a, b *int) {
a, b = b, a
}
func main() {
a, b = 1, 2
swap(&a, &b)
fmt.Println(a, b) // 1, 2
}
栈帧 | ||
---|---|---|
… | ||
addrA | a = 1 | main BP |
addrB | b = 2 | |
swap 参数 b = addrB | ||
swap 参数 a = addrA | ||
返回地址 | main SP | |
main BP | ||
… |
可以看到,此时 swap
方法交换的是 addrA
和 addrB
对应的数据,此时就可以交换成功了。
Function Receiver #
type A struct {
}
func (A) F1(string) string {
return ""
}
func (*A) F2(string) string {
return ""
}
func f1(A, string) string {
return "xxx"
}
func f2(*A, string) string {
return "xxx"
}
func main() {
fmt.Println(reflect.TypeOf(A.F1) == reflect.TypeOf(f1)) // true
fmt.Println(reflect.TypeOf((*A).F2) == reflect.TypeOf(f2)) // true
}
返回值 #
匿名返回值
func inc(a int) int {
var b int
defer func() {
a++
b++
}()
a++
b = a
return b
}
func main() {
var a, b int
b = inc(a)
fmt.Println(a, b) // 0, 1
}
before call inc
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 0 | |
inc 参数 a = 0 | |
返回地址 | main SP |
main BP | |
局部变量 b = 0 | inc BP |
进入 inc
方法后,会将 a
增加 1 并赋值给 b
,最后将 b
赋值给返回值
before return
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 1 | |
inc 参数 a = 1 | |
返回地址 | main SP |
main BP | |
局部变量 b = 1 | inc BP |
在返回 b
之后会执行 defer 匿名函数的内容,将 a
,b
都增加 1,最后返回:
handle defer
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 = 1 | |
inc 参数 a = 2 | |
返回地址 | main SP |
main BP | |
局部变量 b = 2 | inc BP |
返回 main
方法后,将返回值赋值给局部变量 b
:
return main
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 1 | |
inc 返回值 1 | |
inc 参数 a = 2 | |
返回地址 | main SP |
main BP | |
… |
所以最后输出的 a
,b
为 0
,1
。
命名返回值
func inc(a int) (b int) {
defer func() {
a++
b++
}()
a++
b = a
return b
}
func main() {
var a, b int
b = inc(a)
fmt.Println(a, b) // 0, 2
}
Details
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 0 | |
inc 参数 a = 0 | |
返回地址 | main SP |
main BP |
Details
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 1 | |
inc 参数 a = 1 | |
返回地址 | main SP |
main BP |
Details
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 0 | |
inc 返回值 2 | |
inc 参数 a = 2 | |
返回地址 | main SP |
main BP |
Details
栈帧 | |
---|---|
… | |
局部变量 a = 0 | main BP |
局部变量 b = 2 | |
inc 返回值 2 | |
inc 参数 a = 2 | |
返回地址 | main SP |
main BP |
基于寄存器 #
- Proposal: Register-based Go calling convention
- https://www.kandaoni.com/news/56576.html
- https://www.kuangstudy.com/m/bbs/1624703556664086530
- https://www.cnblogs.com/luozhiyun/p/14844710.html
- https://www.kandaoni.com/news/56576.html
- https://www.bilibili.com/video/BV1WZ4y1p7JT/?spm_id_from=333.337.search-card.all.click&vd_source=ddc8289a36a2bf501f48ca984dc0b3c1
- https://www.bilibili.com/video/BV1tZ4y1p7Rv/?spm_id_from=333.788.recommend_more_video.-1&vd_source=ddc8289a36a2bf501f48ca984dc0b3c1