更多干货:关注公众号 奇伢云存储
本篇文章是深入剖析 golang 的 defer 的基础知识准备,如果要完全理解 defer ,避免踩坑,这个章节的基础知识必不可少。我们先复习一个最基础的知识 —— 函数调用。这个对理解 defer 在函数里的行为必不可少。那么,当你看到一个函数调用的语句你能回忆起多少知识点呢?
地址空间
下图是一个典型的操作系统的地址空间示意图:
最重要的几点:
- 内核栈在高地址,用户栈在低地址。如果是 32 位操作系统,那么最经典的就是,用户栈区域为 [0, 3G],内核栈区域位 [3G, 4G];
- 栈空间分配是从高地址往下分配的(所以我们经常看到栈分配空间,是通过减 rsp 的值来实现就是这个道理);
- 堆空间分配是从低地址往上分配的;
函数栈帧
函数调用执行的时候,需要分配空间存储数据,比如函数的参数,函数内局部变量,寄存器的值(用于上下文切换)。这些数据都需要保存在一个地方,这个地方就是栈空间上。因为这些数据的声明周期是和函数一体的,函数执行的时候存在,函数执行完立马就可以销毁。和堆空间不同,堆上用来分配声明周期由程序员控制的对象。栈的使用规划负责人是编译器,堆空间的使用规划负责人是程序员(在有垃圾回收的语言里,堆空间的使用由语言层面支持)。
当函数调用的时候,对应产生一个栈帧(stack frame),函数结束的时候,释放栈帧。栈帧主要用来保存:
- 函数参数
- 局部变量
- 返回值
- 寄存器的值(上下文切换)
函数在执行过程中使用一块栈内存来保存上述这些值。当发生函数调用时,因为 caller 还没执行完,caller 的栈帧中保存的数据还有用,所以 callee 函数执行的时候不能覆盖 caller 的栈帧,这种情况需要分配一个 callee 的栈帧。
栈空间的使用方式由编译器管理,在编译期间就确定。栈的大小就会随函数调用层级的增加而向低地址增加,随函数的返回而缩小,调用层级越深,消耗的栈空间就越大。所以,在递归函数的场景,经常见到有些递归太深的函数会报错,被操作系统直接拒绝,就是因为考虑到这个栈空间使用的合理性,我们对栈的深度有限制。
栈帧的划定
有两个寄存器的值来划定一个函数栈帧:
- rsp :栈寄存器,指向当前栈顶位置;
- rbp :栈帧寄存器,指向函数栈帧的起始位置;
所以,我们可以认为在一个函数执行的时候,rsp, rbp 这两个寄存器指向的区域就是当前函数的一个栈帧。在 golang 的一个函数的代码里,开头会先保存 rbp 寄存器的值,保存到栈上,函数执行完之后,需要返回 caller 函数之前,需要恢复 rbp 寄存器。
举个例子:
func C(c int) (r int) {
c1 := c + 3
return c1
}
汇编出来的指令如下,用 dlv 调试看下:
15: func C(c int) (r int) {
16: c1 := c + 3
=> 17: return c1
18: }
(dlv) disassemble
TEXT main.C(SB)
// 分配栈空间
test_call.go:15 0x1056fe0 4883ec10 sub rsp,