不要小瞧函数调用栈哦,它可是理解参数传递、命名匿名返回值、Function Value、defer等面试常客的关键呐~

我们按照编程语言的语法定义的函数,会被编译器编译为一堆堆机器指令,写入可执行文件。程序执行时可执行文件被加载到内存,这些机器指令对应到虚拟地址空间中,位于代码段。
如果在一个函数中调用另一个函数,编译器就会对应生成一条call指令,程序执行到这条指令时,就会跳转到被调用函数入口处开始执行,而每个函数的最后都有一条ret指令,负责在函数结束后跳回到调用处,继续执行。

01

函数栈帧

函数执行时需要有足够的内存空间,供它存放局部变量、参数等数据,这段空间对应到虚拟地址空间的栈。栈,只有一个口可供进出,先入栈的在底,后入栈的在顶,最后入栈的最早被取出。运行时栈,上面是高地址,向下增长,栈底通常被称为“栈基”,栈顶被称为“栈指针”。

栈高地址向下增长栈基栈指针

分配给函数的栈空间被称为“函数栈帧”,Go语言中函数栈帧布局是这样的,先是调用者栈基地址,然后是函数的局部变量,最后是被调用函数的返回值和参数。

函数栈帧BP of calleeSP of calleeBP of caller局部变量返回值参数

图:函数栈帧布局
BP of callee和SP of callee标识被调用函数执行时,栈基寄存器和栈指针寄存器指向的位置,但是注意“BP of caller”不一定会存在,有些情况下可能会被优化掉,也有可能是平台不支持。我们只关注局部变量和参数、返回值的相对位置就好。
举个例子,函数A调用函数B,函数A有两个局部变量,函数B有两个参数和两个返回值。

func A() {    var a1, a2, r1, r2 int64    a1, a2 = 1, 2    r1, r2 = B(a1, a2)    r1 = C(a1)    println(r1, r2)}func B(p1, p2 int64) (int64, int64) {    return p2, p1}func C(p1 int64) int64 {    return p1}

函数A的栈帧布局如下图所示,局部变量之后的空间用于存放被调用函数的返回值和参数,接下来要调用函数B,所以先有两个int64类型的变量空间用作B的返回值,再有两个int64类型的变量空间用于存放传递给B的参数。

......return1栈a1a2r1r2局部变量BP of ASP of A返回值BP of callerreturn2param1参数param2

图:函数A栈帧布局注意观察参数的顺序,先入栈第二个参数,再入栈第一个参数,返回值也是一样,上面是第二个返回值的空间,然后才是第一个返回值的空间。
其实这也好解释,因为这些是被调用函数的返回值和参数,被调用函数是通过栈指针加上偏移值这样相对寻址的方式来定位到自己的参数和返回值的,这样由下至上正好先找到第一个参数,再找到第二个参数。所以参数和返回值采用由右至左的入栈顺序比较合适。通常,我们认为返回值是通过寄存器传递的,但是Go语言支持多返回值,所以在栈上分配返回值空间更合适~

......return1栈a1a2r1r2局部变量BP of ASP of A返回值BP of callerreturn2param1参数param2SP of B+偏移......SP of B

图:被调用函数相对寻址参数
我们知道对函数B的调用会被编译器编译为call指令。实际上call指令只做两件事:
第一:将下一条指令的地址入栈,被调用函数执行结束后会跳回到这个地址继续执行,这就是函数调用的“返回地址”。
第二:跳转到被调用的函数B指令入口处执行,所以在“返回地址”下面就是函数B的栈帧了。

......return1栈a1a2r1r2局部变量BP of ASP of A返回值BP of callerreturn2param1参数param2SP of B 返回地址BP of A......

图:函数B的栈帧所有函数的栈帧布局都遵循统一的约定,函数B结束后它的栈帧被释放,回到函数A中继续执行。

......return1栈a1a2r1r2局部变量BP of ASP of A返回值BP of callerreturn2param1参数param2SP of B返回地址BP of A......

图:函数B结束
到了调用函数C的时候,它只有一个参数和一个返回值,它们会占用函数A栈帧中最下面的一部分空间,所以上面会空出来一块,这是为了在被调用函数中可以用标准的相对地址定位到自己的参数和返回值,而无需顾虑其它。
同样的,call指令会压入返回地址,并跳转到函数C的指令入口处,所以下面就是函数C的栈帧了。

......return1栈a1a2r1r2局部变量BP of ASP of A返回值BP of callerparam1参数SP of C 返回地址BP of A......

图:调用函数CGo语言中,函数栈帧是一次性分配的,也就是在函数开始执行的时候分配足够大的栈帧空间。就像上例中函数A一样,它要调用两个函数,除了调用者栈基地址、局部变量以外,再有四个int64的空间用作被调用函数的参数与返回值就足够了。一次性分配函数栈帧的主要原因是避免栈访问越界,如下图所示,三个goroutine初始分配的栈空间是一样的,如果g2剩余的栈空间不够执行接下来的函数,若函数栈帧是逐步扩张的,那么执行期间就可能发生栈访问越界。

......栈g1g2g3free越界了

图:栈访问越界
其实,对于栈消耗较大的函数,go语言的编译器还会在函数头部插入检测代码,如果发现需要进行“栈增长”,就会另外分配一段足够大的栈空间,并把原来栈上的数据拷过来,原来的这段栈空间就被释放了。

......栈g1g2g3free

图:g2栈增长示意图
了解了函数栈帧布局,接下来,我们看几个关于参数和返回值常见的问题。

02

传参

下面有一个swap函数,接收两个整型参数,main函数想要通过swap来交换两个局部变量的值,但是失败了......

func swap(a,b int) {    a,b = b,a}    func main() {    a,b := 1,2    swap(a,b)    println(a,b)  //1,2}

我们通过函数调用栈,看看失败的原因到底在哪儿?
main函数栈帧中,先分配局部变量存储空间,a=1,b=2。因为例子中调用的函数没有返回值,所以局部变量后面就是给被调用函数传入的参数。需要传入两个整型参数,Go语言中传参都是值拷贝,参数是整型,所以拷贝整型变量值。注意参数入栈顺序:由右至左。先入栈第二个参数,再入栈第一个参数。

......栈a=1b=2b=2a=1局部变量SP of main参数BP of caller......BP of main返回地址SP of swap值拷贝

图:函数main栈帧布局
调用者栈帧后面是call指令存入的返回地址,swap开始执行,再下面分配的就是swap函数栈帧了。
swap函数要交换两个参数的值,但是注意,swap的参数在哪里?main的局部变量a和b又在哪里?找到它们,交换失败的原因就找到了,想要交换的局部变量a和b在局部变量空间,但实际上交换的是参数空间的a和b。

......栈a=1b=2b=1a=2局部变量SP of main参数BP of caller......  BP of main 返回地址SP of swap交换

图:交换失败的原因
再来个例子,依然要交换两个整型变量的值,但是参数类型改为整型指针。这次交换成功了,同样通过函数调用栈,看看和上次有什么不同。

func swap(a,b *int) {    *a,*b = *b,*a}    func main() {    a,b := 1,2    swap(&a,&b)    println(a,b)  //2,1}

main函数栈帧中,先分配局部变量,然后分配参数空间,参数是指针,传参都是值拷贝,这里拷贝的是a和b的地址。依然由右至左,先入栈b的地址, 再入栈a的地址。再后面是返回地址,以及swap函数栈帧。

......栈a=1b=2&b&a局部变量SP of main参数BP of caller......  BP of main 返回地址SP of swap值拷贝地址

图:函数栈帧布局swap要交换的是这两个参数指针指向的数据,也就是局部变量空间这里的a和b,所以这一次能够交换成功!

......栈a=2b=1&b&a局部变量SP of main参数BP of caller......  BP of main 返回地址SP of swap交换

图:交换成功

03

返回值

直接看例子,这里main函数调用incr函数,然后把返回值赋给局部变量b,下面来看看函数调用栈的情况。

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)    println(a,b) //0,1}

main函数栈帧,先是局部变量,a=0,b=0,然后是incr的返回值,初始化为类型零值,再然后是参数空间。到incr函数栈帧这里,保存调用者main的栈基地址后,初始化局部变量b。

......栈a=0b=00a=0局部变量SP of main参数BP of callerb=0  BP of main 返回地址SP of incr返回值......

图:a++执行前incr函数会把参数a自增一,然后赋值给局部变量b,要注意它们的位置。

......栈a=0b=00a=1局部变量SP of main参数BP of caller......  BP of main 返回地址SP of incr返回值b=1

图:a++; b=a到incr函数的return这里,必须要明确一个关键问题。incr函数返回之前要给返回值赋值并执行defer函数,那谁先?谁后?答案是:“先赋值”所以incr函数返回前,会先把局部变量b的值拷贝到返回值空间,然后再执行注册的defer函数。

......栈a=0b=01a=1局部变量SP of main参数BP of caller......  BP of main 返回地址SP of incr返回值b=1拷贝返回值

图:incr返回前先拷贝返回值在defer函数里,a再次自增1,局部变量b也自增1。

......栈a=0b=01a=2局部变量SP of main参数BP of caller......  BP of main 返回地址SP of incr返回值b=2

图:执行defer函数所以,incr结束后,返回值为1,赋给main函数局部变量b,最后会输出0和1。

......栈a=0b=11a=2局部变量SP of main参数BP of caller......  BP of main 返回地址SP of incr返回值b=2b=incr(a)

图:incr返回值赋给b这是匿名返回值的情况,下面再来个例子,其它都不变,只把这里的局部变量b改成命名返回值,看看有什么不同。

func incr(a int) (b int) {    defer func(){        a++        b++    }()        a++    return a}func main(){    var a,b int    b = incr(a)    println(a,b) //0,2}

main函数栈帧,与上个例子完全相同,到incr函数栈帧这里,没有局部变量,执行到a++时,参数a自增1。返回前,先把参数a赋给返回值b,要注意返回值的位置。

......栈a=0b=0b=1a=1局部变量SP of main参数BP of caller......  BP of main 返回地址返回值拷贝返回值SP of incr

图:拷贝返回值然后执行defer函数,参数a再次自增1,返回值b也自增1,然后incr结束,返回值最终为2。

......栈a=0b=0b=2a=2局部变量SP of main参数BP of caller......BP of main返回地址返回值SP of incr

图:执行defer函数所以, main的局部变量b赋值为2,最后会输出0和2。

......栈a=0b=2b=2a=2局部变量SP of main参数BP of caller...... BP of main 返回地址SP of incr返回值b=incr(a)

图:incr结束命名返回值和匿名返回值相关的问题,最关键的还是函数栈帧布局,以及返回值被赋值的时机。最后,留给感兴趣的同学看看,在汇编指令层面怎么实现函数跳转与返回。

04

函数跳转与返回

程序执行时 CPU用特定寄存器来存储运行时栈基与栈指针,同时也有指令指针寄存器用于存储下一条要执行的指令地址。

......栈寄存器BPSPIP栈基栈指针指令指针指令push 3push 4

如果接下来要执行"push 3"这条指令,CPU读取后,会将指令指针移向下一条指令,然后栈指针向下移动,数字3入栈。

......栈寄存器BPSPIP栈基栈指针指令指针指令push 3push 43

继续执行下一条指令,再次移动栈指针入栈数字4。

......栈寄存器BPSPIP栈基栈指针指令指针指令push 3push 4......34

前面我们提过Go语言中函数栈帧不是这样逐步扩张的,而是一次性分配,也就是在分配栈帧时,直接将栈指针移动到所需最大栈空间的位置。

......栈寄存器BPSPIP栈基栈指针指令指针指令把3移动到SP+16处把4移动到SP+8处

然后通过栈指针加上偏移值这种相对寻址方式使用函数栈帧。例如sp加16字节处存储3,加8字节处存储4,诸如此类。

......栈寄存器BPSPIP栈基栈指针指令指针指令把3移动到SP+16处把4移动到SP+8处......34

接下来我们看看call指令和ret指令,是怎样实现函数跳转与返回的。

func A(){    a,b := 1,2    B(a,b)    return}func B(c,d int){    println(c,d)    return}

调用函数B之前函数A栈帧如下图所示,注意函数A和函数B的指令分布在代码段,而且函数A调用函数B的call指令在地址a1处,函数B入口地址在b1处。

......栈a=1b=2......代码段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPa1IPs1s2d=2c=1s3s4s5s6

图:call指令执行前然后到call指令这里,它的作用有两点:第一,把返回地址a2入栈保存起来;第二,跳转到指令地址b1处。

......栈a=1b=2......代码段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPb1IPa2a2d=2c=1s1s2s3s4s5s6

图:call指令执行后call指令结束。函数B开始执行,我们先看它最开始的三条指令:第一条指令,把SP向下移动24字节(从s6挪到s9),为自己分配足够大的栈帧;第二条指令,要把调用者栈基s1存到SP+16字节的地方(s7那里);第三条指令,把s7(SP+16)存入BP寄存器。

栈a=1b=2............代码段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPb4IPa2a2s1b4d=2c=1s1s2s3s4s5s6s7s8s9

图:执行函数B入口处插入的三条指令接下来就是执行函数B剩下的指令了,没有局部变量,只有被调用者的参数空间。在最后的ret指令之前,编译器还会插入两条指令:第1条指令:恢复调用者A的栈基地址,它之前被存储在SP+16字节(s7)这里,所以BP恢复到s1;第2条指令:释放自己的栈帧空间,分配时向下移动多少(从s6到s9)释放时就向上移动多少(从s9到s6)。

栈a=1b=2......代码段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPbnIPa2a2s1bnc=1d=2c=1d=2......s1s2s3s4s5s6s7s8s9

图:执行ret指令之前插入的两条指令然后就到ret指令了,它的作用也有两点:第一,弹出call指令压栈的返回地址a2;第二,跳转到call指令压栈的返回地址a2处。

......代码段............a1call b1~~~~~~~~~~~~~~~~b1~~~~~~~~......~~~~~~~~~~~~~~~~RETAB寄存器BPSPa2IPa2栈a=1b=2a2s1c=1d=2c=1d=2......s1s2s3s4s5s6s7s8s9

图:ret指令执行后现在可以从a2这里继续执行了。简单来说,函数通过call指令实现跳转,而每个函数开始时会分配栈帧,结束前又释放自己的栈帧,ret指令又会把栈恢复到call之前的样子,通过这些指令的配合最终实现了函数跳转与返回。

swap函数_【Golang】图解函数调用栈相关推荐

  1. swap函数_[C++基础入门] 6、函数

    点击上方 蓝字 关注我呀! [C++基础入门] 6.函数 文章目录 6 函数 6.1 概述 6.2 函数的定义 6.3 函数的调用 6.4 值传递 6.5 函数的常见样式 6.6 函数的声明 6.7 ...

  2. golang 函数调用栈

    golang函数调用栈 文章目录 golang函数调用栈 函数栈帧 函数跳转与返回 如果在一个函数中调用另一个函数,编译器就会对应生成一条call指令,程序执行到这条指令时,就会跳转到被调用函数入口处 ...

  3. python入门之函数调用内置函数_第九篇 python基础之函数,递归,内置函数

    阅读目录 一 数学定义的函数与python中的函数 二 为何使用函数 背景提要 三 函数和过程 四 函数参数 五 局部变量和全局变量 六 前向引用之'函数即变量' 七 嵌套函数和作用域 八 递归调用 ...

  4. 函数调用栈 剖析+图解

    栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量.注意 ...

  5. middle函数C语言,C语言函数调用栈(三)

    6 调用栈实例分析 本节通过代码实例分析函数调用过程中栈帧的布局.形成和消亡. 6.1 栈帧的布局 示例代码如下: //StackReg.c #include //获取函数运行时寄存器%ebp和%es ...

  6. 2.2基本算法之递归和自调用函数_数据结构与算法之5——队列和栈

    栈和队列比较简单,而且实用性非常广泛,这里主要介绍一下他们的概念和实现,在很多算法中,栈和队列的运用很重要,因此,虽然简单确是最重要的数据结构之一,必须重视. 栈是保证元素后进先出(后存入者先使用,L ...

  7. c语言 swap交换函数_重审C中老生常谈的swap函数交换数值

    概览 本文内容是关于C语言参数传值,以及x86底层实现的计算机科学. 包含了原理速览以及代码示例. 引言 如果你学习过C,可能会对经典的swap函数问题记忆深刻.简单的参数传值并不能在函数外部完成两个 ...

  8. linux调用堆栈函数,使用 backtrace 获得 Linux 函数调用栈

    一.源码 使用 backtrace 获得 Linux 函数调用栈 源码来自:man backtrace #include #include #include #include void myfunc3 ...

  9. 【九】Python全栈之路--文件函数_函数参数

    文章目录 1. 文件相关函数 2. 函数_函数的参数 2.1 函数 2.2 函数的参数 3. 收集参数 4. 命名关键字_总结 4. 小练习 1. 文件相关函数 # ### 刷新缓冲区 "& ...

最新文章

  1. 让Centos5.6的Firefox支持Java
  2. 使用TextInputLayout分分钟构造一个酷炫登录框架
  3. 第一次使用MsChart运行提示:ChartImg.axd 执行子请求时出错。
  4. “比特币耶稣”罗杰·沃推特赠币,留下BCH钱包地址就有份
  5. sharepoint 2013 自定义列表eventhandle权限控制
  6. POJ3228二分最大流
  7. 【Linux抓包工具之tcpdump】
  8. 关于android工程添加support-v7包的问题
  9. 计算机硬件的维护知识,计算机硬件维护知识
  10. Python多任务(2.线程的概念以及组成(创建线程的两种方式))
  11. python3中使用Redis
  12. puppet的相关介绍
  13. pyton-虚拟环境以及django的初步使用记录信息
  14. 大学生python心得1000字_大学生读书心得1000字3篇
  15. svn 回退到指定版本无法提交_svn回滚到指定版本
  16. 第三方支付业务流程介绍
  17. 一不做,二不休, 干脆把开局库更换的批处理文件也调试好放出来...
  18. 数据库范式(1 2 3 BCNF范式)详解
  19. python 中的 Fraction 模块
  20. Windows debugging tools

热门文章

  1. Greenplum 数据库一体机硬件配置正式开源
  2. 顶级的CSS和Javascript动画框架推荐
  3. GitLab 8.9 新增文件锁 和 U2F硬件支持
  4. 开源软件的商业化策略模型
  5. java 指针 引用_C++中的指针和引用与Java中的引用区别
  6. 多个服务器数据互通_数据中心
  7. 华理c语言设计网上作业,华理c程序实验报告(共9篇).doc
  8. 度量相似性数学建模_数学之美读书笔记
  9. 损失函数与代价函数区别
  10. Android 核心组件 Activity 之上