首页 > 编程知识 正文

函数调用栈图解,函数调用时栈中保存指针吗

时间:2023-05-03 23:30:28 阅读:135823 作者:537

golang函数调用栈文章目录golang函数调用栈函数栈帧函数跳转和返回

在一个函数中调用另一个函数时,编译器会生成相应的call指令,当程序执行到此指令时,它会跳到被调用函数的入口并开始执行。 每个函数的末尾都有一个ret指令,该指令在函数退出后跳转到调用方并继续执行。

函数堆栈框架

要执行函数,必须有足够的内存空间来存储局部变量、参数等数据。 该区域为虚拟地址空间的

3358www.Sina.com/,堆底通常称为3358www.Sina.com/,堆顶称为运行时栈,上面是高地址,向下增长

分配给函数的堆栈空间称为

Go语言中的函数堆栈框架布局首先是调用方堆栈基址,然后是函数的局部变量,最后一个被调用函数的返回值和参数。

3358www.Sina.com/和3358www.Sina.com/指示执行被调用函数时基于堆栈的寄存器和堆栈指针的寄存器所指向的位置。 但是,请注意,“BP of caller”不一定存在。 有时会进行优化,有时平台不支持。 您只需关注局部变量、参数和返回值的相对位置即可。

举个例子吧。

func A () varA1,a2,r1,r2 int64 A1,a2=1,2 r1,r2=b ) A1,a2 ) R1=c ) a1 ) a1 ) println ) R1,R2 ) ) func A ) P1,pppp 1

函数a的堆栈框架布局如下图所示。

请注意参数的顺序。 即使先堆栈第二个参数,然后重新堆栈第一个参数,返回值也是相同的。 上面是第二个返回值的空间,从那里开始是第一个返回值的空间。

因为这些是被调用函数的返回值和参数,所以被调用的函数以在堆栈指针上加上偏移值的相对地址方式被定位为自己的参数和返回值,从下向上正好找到第一个参数后,第二个参数因此,自变量和返回值最好采用从右到左的堆栈顺序。

通常认为返回值是通过寄存器传递的,但由于Go语言支持多个返回值,因此为堆栈分配返回值空间更好。

对函数b的调用由编译器编译为call指令。 实际上,call命令只做两件事。

将下一条指令的地址放入堆栈中,在被调用函数执行完毕后,跳转到该地址继续执行。 这就是函数调用的“返回地址”。 跳转到被调用的函数b指令的入口并执行,因此“返回地址”下有函数b的堆栈帧。

所有函数的堆栈帧布局遵循统一的约定,函数b结束后,其堆栈帧被释放,返回函数a继续执行。

调用函数c时,只有一个参数和返回值,并且占用函数a堆栈帧底部区域的一部分,因此上面有空块。 这样就可以在被调用的函数中用标准的相对地址定位自己的参数和返回值,所以不需要在意其他。

同样,call指令按下返回地址,跳到函数c的指令入口,因此下一个是函数c的堆栈帧。

在Go语言中,函数堆栈帧一次分配。 这意味着在函数开始执行时将分配足够大的堆栈帧空间。 如上面示例中的函数a所示,要调用这两个函数,除了调用栈的基地址和局部变量外,还需要四个int64作为调用函数的参数和返回值,这一点就足够了。

一次分配函数堆栈帧的主要原因是避免堆栈访问的越界。 如下图所示,3个goroutine最初分配的堆栈区域是相同的。 如果g2的剩馀堆栈空间不足以执行下一个函数,如果函数堆栈帧被逐步扩展,则在执行过程中可能会发生堆栈访问越界。

实际上,对于堆栈消耗较大的函数,go语言编译器在函数的开头插入校验码,如果发现需要“增加堆栈”,则分配另一个足够的堆栈空间,复制原始堆栈上的数据,释放原始堆栈空间

函数跳转和返回

程序运行时CPU在特定寄存器中存储运行时栈基和栈指针,同时还有指令指针寄存器存储下一个要执行的指令地址。

接下来,在执行' push 3'指令的情况下,CPU在读入后将指令指针移动到下一个指令,将堆栈指针向下移动,将数字3放入堆栈。

继续以下命令,将堆栈指针再次移动到堆栈数字4 :

如上所述,在Go语言中函数栈帧不是这样逐步扩展的,而是在分配栈帧时直接将栈指针移动到所需的最大栈区域的位置的一次性分配。

然后,以在堆栈指针上加上偏移值的相对地址方式使用函数堆栈帧。 例如,在sp 16字节上加3

8字节处存储4,诸如此类。




接下来我们来看看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处。




然后到call指令这里,它的作用有两点:

第一,把返回地址a2入栈保存起来;第二,跳转到指令地址b1处。



call指令结束。函数B开始执行,我们先看它最开始的三条指令:

第一条指令,把SP向下移动24字节(从s6挪到s9),为自己分配足够大的栈帧;第二条指令,要把调用者栈基s1存到SP+16字节的地方(s7那里);第三条指令,把s7(SP+16)存入BP寄存器。



接下来就是执行函数B剩下的指令了,没有局部变量,只有被调用者的参数空间。在最后的ret指令之前,编译器还会插入两条指令:

第1条指令:恢复调用者A的栈基地址,它之前被存储在SP+16字节(s7)这里,所以BP恢复到s1;第2条指令:释放自己的栈帧空间,分配时向下移动多少(从s6到s9)释放时就向上移动多少(从s9到s6)。



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



参考资料:

https://mp.weixin.qq.com/s/zcqzarXMJrDUY5DLXZXY1Q

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。