深入剖析 defer 原理篇 —— 函数调用的原理?

peter 等级 929 0 0

本篇文章是深入剖析 golang 的 defer 的基础知识准备,如果要完全理解 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, 0x10  
    // 保存上一个函数的栈基地址  
    test_call.go:15     0x1056fe4   48896c2408      mov qword ptr [rsp+0x8], rbp  
    // rbp 指向当前的栈基  
    test_call.go:15     0x1056fe9   488d6c2408      lea rbp, ptr [rsp+0x8]  
    test_call.go:15     0x1056fee   48c744242000000000  mov qword ptr [rsp+0x20], 0x0  
    // 执行 a + 3  
    test_call.go:16     0x1056ff7   488b442418      mov rax, qword ptr [rsp+0x18]  
    test_call.go:16     0x1056ffc   4883c003        add rax, 0x3  
    // 保存到 c1 变量  
    test_call.go:16     0x1057000   48890424        mov qword ptr [rsp], rax  
    // 保存到返回值到栈变量  
=>  test_call.go:17     0x1057004   4889442420      mov qword ptr [rsp+0x20], rax  
    // 恢复 rbp 值(指向上一个函数的栈基)  
    test_call.go:17     0x1057009   488b6c2408      mov rbp, qword ptr [rsp+0x8]  
    // 回收栈空间  
    <autogenerated>:1   0x105700e   4883c410        add rsp, 0x10  
    // 返回调用函数  
    <autogenerated>:1   0x1057012   c3          ret`

深入剖析 defer 原理篇 —— 函数调用的原理?

dlv 调试到这个 C 函数的时候,rsp 和 rbp 寄存器的值分别是 0x000000c00002e6f8,0x000000c00002e700,相隔 8 个字节,所以可以说这个函数的栈帧就只有 8 个字节,不过有上面有 16 个字节要注意,就是 caller 函数 rbp 的保存值和 caller 下一行要执行的指令地址。另外要提一点的是,rbp 这个寄存器其实就函数执行的功能上来说,并不需要,rbp 基本上就是给用来调试的,标明一个个栈帧,这样 gdb 或者 dlv 执行 bt 命令的时候,就能看到堆栈了。

函数调用

函数调用在 golang 里面非常简单,比如 b1 := C(b) 就是一个函数调用,执行函数 C ,传入的实参是变量 b ,返回值存入局部变量 b1,对应的汇编指令是 call 。这个语句经过编译器的翻译,如下:

// 传入参数  
test_call.go:10  0x1056faf 4889442428  mov qword ptr [rsp+0x28], rax  
test_call.go:11  0x1056fb4 48890424  mov qword ptr [rsp], rax  
// 跳转到函数 C 执行指令  
test_call.go:11  0x1056fb8 e823000000  call $main.C  

这里我们注意到,一个简单的 b1 := C(b) 会翻译成多条汇编语句,通过汇编语句我们看到一行函数调用主要做两件事情:

  1. 设置函数参数;

  2. 执行 call 指令;

函数调用最重要的就是 call 指令。call 指令是一条基础的汇编指令,做两件事情:

  1. 把当前所在函数(caller)的下一行指令压栈;
  1. 会导致栈顶往下增长 8 字节
  1. 跳转到 C 函数指令执行(pc 的值切换成 C 的入口指令)

什么意思?举个例子,假如 b1 := C(b) 下一行的命令是 a :=1 ,如下:

b1 := C(b)  
a := 1  

调用 call $main.C 的时候,就先把 a := 1 这行语句对应的代码地址保存到栈上,然后 pc 寄存器加载函数 C 的入口指令。

深入剖析 defer 原理篇 —— 函数调用的原理?

进入函数里面,第一件做的事情就是保存 rbp 的值,后面从函数中退出的时候,用于恢复上下文。

函数返回

golang 语言层面函数返回对应了 return 关键字,这个有必要深入理解下。函数 C 的语句如下:

func C(c int) (r int) {  
 c1 := c + 3  
 return c1  
}  

和函数调用一样,函数返回(return)的调用也是多个步骤的。看起来就调用了一行 return c1,但其实这一行语句包含了多行指令:

  1. 设置返回值(函数调用是 b1 := C(b) ,这里说的设置返回值也就是设置 b1);
  1. 所以,设置返回值是在 callee 函数里;
  1. 执行 ret 指令

函数返回最重要的就是 ret 指令了,这个指令和 call 是配套的,动作是相反的,汇编指令 ret 主要做两件事情:

  1. 从当前栈顶处取出 [$rsp] 的值,恢复到 pc 寄存器,跳转到这个地址准备执行命令;

  2. 弹栈,栈顶往上缩减 8 字节

回想上面说的函数调用时候 call 时候的压栈,ret 取出来的地址就是 a :=1 指令,这样就刚好对上了,函数 C 调用完回到原函数继续执行下一行命令。

举个例子

了解完基础知识,我们以下面的例子,分析一下这个函数栈,复习一下:

package main  

func A(a int) int {  
 a = a + 1  
 a1 := B(a)  
 return a1  
}  

func B(b int) int {  
 b = b + 2  
 b1 := C(b)  
 return b1  
}  

func C(c int) (r int) {  
 c1 := c + 3  
 return c1  
}  

func main() {  
 a := A(7)  
 _ = a  
}  
`

函数栈帧如下:

深入剖析 defer 原理篇 —— 函数调用的原理?

这个地方的栈帧区域标注都是以 rsp,rpb 寄存器界定的,所以每个栈帧中间有 16 个字节的间隔,分别是函数压栈的地址,还有 rbp 的保存值。

总结

  1. go 的一行函数调用语句其实非原子操作,对应多行汇编指令,包括 1)参数设置,2) call 指令执行;

  2. 其中 call 汇编指令的内容也有两个:返回地址压栈(会导致 rsp 值往下增长,rsp-0x8),callee 函数地址加载到 pc 寄存器;

  3. go 的一行函数返回 return语句其实也非原子操作,对应多行汇编指令,包括 1)返回值设置 和 2)ret 指令执行;

  4. 其中 ret 汇编指令的内容是两个,指令pc 寄存器恢复为 rsp 栈顶保存的地址,rsp 往上缩减,rsp+0x8;

  5. 参数设置在 caller 函数里,返回值设置在 callee 函数里;

  6. rsp, rbp 两个寄存器是栈帧的最重要的两个寄存器,这两个值划定了栈帧;

  7. rbp 寄存器的常见的作用栈基寄存器,但其实再深入了解下你会知道 rbp 在当今体系里其实可以作为通用寄存器了。而最常见的用来用栈基寄存器还是为了调试,比较方便的划定栈帧;

思考

为什么深入理解 defer 需要先深入理解函数调用呢?

因为,这个关系到 defer 最本质的语义:defer 是在函数调用返回的时候执行的。那么这个执行时机到底是什么样子的?是先设置返回值,还是先执行 defer 函数呢?

比如下面的例子:

func f1 () (r int) {  
 t := 1  
 defer func() {  
  t = t +5  
 }()  
 return t  
}  

func f2() (r int) {  
 defer func(r int) {  
  r = r + 5  
 }(r)  
 return 1  
}  

func f3() (r int) {  
 defer func () {  
  r = r + 5  
 } ()  
 return 1  
}  

这三个函数的返回值分别是多少?可以思考下。

答案:f1() -> 1,f2() -> 1,f3() -> 6 。

你全对了吗?如果心有疑问,我们在下一次的 defer 原理分享里展开进一步的剖析。


本文转自 https://mp.weixin.qq.com/s/Tl67RsMVkaRIbmKCl0z-Ow,如有侵权,请联系删除。

收藏
评论区

相关推荐

Mac安装Golang和vscode
Mac第一次安装golang和vscode一起使用,遇到了不少的坑,下面介绍一下正确的安装方式。 1、使用brew安装Golang 如果不知道brew是什么,或怎么安装请看这里 brew官网(https://brew.sh/index_zhcn) brew install golang 安装完成后可以使用
golang 分析调试高阶技巧
layout: post title: “golang 调试高阶技巧” date: 2020603 1:44:09 0800 categories: golang GC 垃圾回收 golang 高阶调试 Golang tools nm compile
深入剖析 defer 原理篇 —— 函数调用的原理?
本篇文章是深入剖析 golang 的 defer 的基础知识准备,如果要完全理解 defer ,避免踩坑,这个章节的基础知识必不可少。我们先复习一个最基础的知识 —— 函数调用。这个对理解 defer 在函数里的行为必不可少。那么,当你看到一个函数调用的语句你能回忆起多少知识点呢? 地址空间 下图是一个典型的操作系统的地址空间示意图: (h
Go语言学习——彻底弄懂return和defer的微妙关系
疑问前面在函数篇里介绍了Go语言的函数是支持多返回值的。只要在函数体内,对返回值赋值,最后加上return就可以返回所有的返回值。最近在写代码的时候经常遇到在return后,还要在defer里面做一些收尾工作,比如事务的提交或回滚。所以想弄清楚这个return和defer到底是什么关系,它们谁先谁后,对于最后返回值又有什么影响呢? 动手验证了解
defer 让你的代码更清晰
日常开发中,我们经常会编写一些类似下面示例中的代码:gofunc writeToFile(fname string, data []byte, mu sync.Mutex) error mu.Lock() f, err : os.OpenFile(fname, os.ORDWR, 0666) if err ! nil mu.Unlock() retu
Go WEB入门
摘要 由于Golang优秀的并发处理,很多公司使用Golang编写微服务。对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器。加上Golang的协程,这个服务器可以拥有极高的性能。然而,正是因为代码过于简单,我们才应该去研究他的底层实现,做到会用,也知道为什么这么用。 在本文中,会以自顶向下的方式,从如何使用,到如何实现,一点点的分
Go语言入门系列(一)之Go的安装和使用
1.安装环境 ====== 1. 进入[Golang官网](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fgolang.org),进入下载页面。 (如果打不开可访问[Golang中国](https://www.oschina.net/action/GoToLink?u
Archlinux下Visual Studio Code配置Golang开发环境
一、Golang的安装 ----------- GoLang安装并验证一下: [cox@localhost ~]$ sudo pacman -S go [cox@localhost ~]$ go version go version go1.8.3 linux/amd64s 要注意,Golang的安装要确保两个环境变量,一个是G
Golang Gin实践 番外 请入门 Makefile
<h1>Golang Gin实践 番外 请入门 Makefile</h1> <p>原文地址:<a href="https://github.com/EDDYCJY/blog/blob/master/golang/gin/2018-08-26-Gin%E5%AE%9E%E8%B7%B5-%E7%95%AA%E5%A4%96-%E8%AF%B7%E5%85%A5
Golang In PingCAP
随着 Golang 在后端领域越来越流行,有越来越多的公司选择 Golang 作为主力开发语言。本次 GopherChina Beijing 2016 大会上,看到 Golang 在各家公司从人工智能到自动运维,从 Web 应用到基础架构都发挥着越来越多的作用。可以说 Golang 在这几年间,获得了长足的进步。 PingCAP 是一家由几名 Go
Golang 内存管理源码剖析
Golang 的内存管理基于 tcmalloc,可以说起点挺高的。但是 Golang 在实现的时候还做了很多优化,我们下面通过源码来看一下 Golang 的内存管理实现。下面的源码分析基于 go1.8rc3。 1.tcmalloc 介绍 ------------- 关于 tcmalloc 可以参考这篇文章 [tcmalloc 介绍](https://ww
Golang 开发环境搭建
Golang 是 Google 发布的开发语言,Go 编译的程序速度可以媲美 C/C++。 安装 -- sudo apt-get install golang sudo apt-get install golang-go.tools 使用 -- * 编译运行程序 go run main.go * 查看命令文
Golang中defer、return、返回值之间执行顺序的坑
原文链接:https://studygolang.com/articles/4809 Go语言中延迟函数defer充当着 cry...catch 的重任,使用起来也非常简便,然而在实际应用中,很多gopher并没有真正搞明白defer、return和返回值之间的执行顺序,从而掉进坑中,今天我们就来揭开它的神秘面纱! 先来运行下面两段代码: A. 无名返
Golang依赖管理工具:glide从入门到精通使用
介绍 -- 不论是开发Java还是你正在学习的Golang,都会遇到**依赖管理**问题。Java有牛逼轰轰的Maven和Gradle。 Golang亦有godep、govendor、glide、gvt、gopack等等,本文主要给大家介绍[gilde](https://www.oschina.net/action/GoToLink?url=https%3
Go的defer和方法修饰符的一个小坑
先看代码: ![](https://static.oschina.net/uploads/space/2018/0308/114424_6HKf_97951.png) ![](https://static.oschina.net/uploads/space/2018/0308/113236_C504_97951.png) [https://play.g