Go defer 原理剖析篇 — 深度 defer 的基础知识前提:理解函数调用

更多干货:关注公众号 奇伢云存储

本篇文章是深入剖析 golang 的 defer 的基础知识准备,如果要完全理解 defer ,避免踩坑,这个章节的基础知识必不可少。我们先复习一个最基础的知识 —— 函数调用。这个对理解 defer 在函数里的行为必不可少。那么,当你看到一个函数调用的语句你能回忆起多少知识点呢?

地址空间

下图是一个典型的操作系统的地址空间示意图:
在这里插入图片描述

最重要的几点:

  1. 内核栈在高地址,用户栈在低地址。如果是 32 位操作系统,那么最经典的就是,用户栈区域为 [0, 3G],内核栈区域位 [3G, 4G];
  2. 栈空间分配是从高地址往下分配的(所以我们经常看到栈分配空间,是通过减 rsp 的值来实现就是这个道理);
  3. 堆空间分配是从低地址往上分配的;

函数栈帧

函数调用执行的时候,需要分配空间存储数据,比如函数的参数,函数内局部变量,寄存器的值(用于上下文切换)。这些数据都需要保存在一个地方,这个地方就是栈空间上。因为这些数据的声明周期是和函数一体的,函数执行的时候存在,函数执行完立马就可以销毁。和堆空间不同,堆上用来分配声明周期由程序员控制的对象。栈的使用规划负责人是编译器,堆空间的使用规划负责人是程序员(在有垃圾回收的语言里,堆空间的使用由语言层面支持)。

当函数调用的时候,对应产生一个栈帧(stack frame),函数结束的时候,释放栈帧。栈帧主要用来保存:

  1. 函数参数
  2. 局部变量
  3. 返回值
  4. 寄存器的值(上下文切换)

函数在执行过程中使用一块栈内存来保存上述这些值。当发生函数调用时,因为 caller 还没执行完,caller 的栈帧中保存的数据还有用,所以 callee 函数执行的时候不能覆盖 caller 的栈帧,这种情况需要分配一个 callee 的栈帧。

栈空间的使用方式由编译器管理,在编译期间就确定。栈的大小就会随函数调用层级的增加而向低地址增加,随函数的返回而缩小,调用层级越深,消耗的栈空间就越大。所以,在递归函数的场景,经常见到有些递归太深的函数会报错,被操作系统直接拒绝,就是因为考虑到这个栈空间使用的合理性,我们对栈的深度有限制。

栈帧的划定

有两个寄存器的值来划定一个函数栈帧:

  1. rsp :栈寄存器,指向当前栈顶位置;
  2. 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, 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值