本文已参与「新人创作礼」活动,一起开启掘金创作之路。
函数调用栈
-
编写的函数代码会被编译器编译为机器指令写入可执行文件
-
程序执行时,可执行文件被加载到内存,机器指令对应到虚拟地址空间中,位于代码段
-
如果在函数中调用另一个函数,编译器会生成一个CALL指令,每一个函数调用结束后会有一个RET用于调回到调用处
-
运行时用的内存栈上面的高地址,分配给函数的叫函数栈帧,栈底称为栈基BP,栈顶叫栈指针SP
内存布局
先是调用者的栈基地址,再到局部变量,再到调用函数的返回值,最后是参数,最后是返回地址,跳转到被调用函数的入口处执行。后面就是被调用的函数栈帧
go语言中函数栈帧是一次性分配的
在分配栈帧时直接将栈指针移动到所需最大栈空间的位置
然后根据栈指针sp+偏移offset的相对寻址方式使用函数栈帧
函数栈帧的大小在编译期间可以确定,但是如果栈空间是逐步扩张的,可能会发生栈访问越界
对于栈消耗较大的函数,go编译器会在函数头部插入一段检测代码,如果检测到越界,就会新申请另一端更大的栈空间,并将原来栈帧的数据copy到那上面,并free释放之前的栈空间的内存。
调用CALL指令实现跳转
- 把入栈返回地址保存起来
- 跳转到指令地址处(函数调用入口)
在执行完RET指令之前,编译器会插入两行代码
- 恢复调用者栈基(bp)
- 释放自己的栈帧空间,分配时向下移动多少,释放时就向上移动多少(sp)
在执行这两行代码时,会先执行给返回值赋值,然后执行defer调用函数
调用RET指令
- 弹出返回地址
- 跳转到返回地址
参数入栈顺序从右至左,先入栈第二个参数,再入栈第一个参数。返回值也一样,这样sp+偏移指示更方便
go支持多返回值,在栈上分配返回值空间
分析
func incr(a int) int {
var b int
defer func(){
a++
b++
}()
a++
b = a
return b
}
func main(){
var a, b int
b = incr(a)
fmt.Println(a, b) // 0, 1
}
调用栈帧分布如下,在执行defer之前会先返回返回值b,从b处拷贝到return value,再执行defer函数调用,a、b++。但是此时是局部变量自增,不影响到返回值。再执行RET跳转到返回地址。此时a=0,b=1
这种情况是基于返回值是匿名的情况
如果返回值不是匿名的,那么在执行defer函数的时候可能会修改return value
调用的不同函数,返回值和参数占用空间不同
func A(){
r1, r2 := B(p1, p2, p3)
r1 = C(p1)
}
A一次性分配
B占用的栈空间较多,B结束后会释放调锁占用的栈空间
这时轮到调用C了,参数和返回值只会占用下面的一部分空间,因为这样方便sp+offset寻址
参考
go语言笔试面试宝典