Golang底层原理剖析之函数调用栈-栈帧布局与函数跳转

简介: Golang底层原理剖析之函数调用栈-栈帧布局与函数跳转

栈帧布局

我们按照编程语言的语法定义的函数,会被编译器编译为一堆机器指令,写入可执行文件,程序执行时,可执行文件被加载到内存,这些机器指令对应到虚拟地址空间中,位于代码段。

如果在一个函数中调用另一个函数,编译器就会对应生成一条call指令,程序执行到这条指令时,就会跳转到被调用函数处开始执行,而每个函数的最后都有一条ret指令,负责在函数结束后跳回到调用处,继续执行。

函数执行时需要有足够的内存空间,供函数存放局部变量,参数等数据,这段空间对应到虚拟地址空间的栈,栈是向下增长,分配给函数的栈空间,被称为栈帧,栈底被称为栈基bp,栈顶又叫栈指针sp

go语言函数栈帧布局是这样的,先是调用者函数栈基地址,接下来是局部变量,然后是调用函数的返回值和参数

函数跳转

call指令只做两件事

第一将下一条指定的地址入栈,这就是返回地址,被调用函数执行结束后会跳回到这里(从现象看,返回地址是被CALL指令压栈的,既不是调用者分配的,也不是被调用者分配的。逻辑上看,调用者不会访问栈上的返回地址以及位于返回地址之下的地方。但被调用者的视角看,从上到下依次是参数、返回地址、局部变量,所以应该算是被调用者的栈帧,也就是可以理解为被调用者栈帧第一个存的是return addr,第二个存的是调用者bp。)

第二,跳转到被调用函数的入口处开始执行

所有的函数的栈帧布局都遵循统一的约定,所以,被调用者是通过栈指针加上相应的偏移来定位到每个参数和返回值的

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

go语言的栈不是逐步扩张的,而是一次性分配,也就是在分配栈帧时,直接将栈指针移动到所需最大栈空间的位置,然后通过栈指针+偏移值这种相对寻址方式使用函数栈帧

之所以一次性分配,主要是为了避免栈访问越界。由于函数栈帧的大小,可以在编译时期确定,对于栈消耗较大的函数,go语言的编译器会在函数头部插入检测代码,如果发现需要进行“栈增长”,就会另外分配一段足够大的栈空间,并把原来栈上的数据拷贝过来,原来这段栈空间就被释放了。

  1. 接下来看看call指令和ret指令是怎样实现函数跳转与返回的

一个函数A在a1处调用b1处的函数B,跳转前寄存器和栈的情况是这样的

然后到call指令这里,它的作用有两点,第一,把下一条指令执行地址a2入栈保存起来,第二,跳转到指令执行地址b1处,call指令就结束了。

函数B开始执行,先把sp向下移动24字节,为自己分配足够大的栈帧,所以栈指针移到s7,接下来是b2这条指令,要把调用者栈基(caller bp)存到sp+16的地方,接下来b3把sp+16存入栈基寄存器,接下来就是执行函数B剩下的指令了

在ret指令之前,编译器还会插入两条指令,第一条指令恢复调用者A的栈基地址,它之前被存储在sp+16字节这里,这就是为什么栈帧布局第一条就是caller’s bp的原因,第二条指令释放自己的栈帧空间,分配时向下移动多少,释放时就向上移动多少

然后就到ret指令了,它的作用也有两点,第一弹出call指令压栈的返回地址,第二,指令指针寄存器跳转到这个返回地址,ok现在可以从a2这里继续执行了

简单来说,函数通过call指令实现跳转,而每个函数开始时会分配栈帧,结束前又会释放自己的栈帧,ret指令又会把call恢复到call之前的样子,通过这些指令的配合能够实现函数的层层嵌套。如果函数A嗲用B,B调用C,C调用D,就会形成这样的栈

如果每次调用的都是A,就是递归调用栈了


目录
相关文章
|
12天前
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
75 29
|
10天前
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
|
2月前
|
算法 安全 测试技术
golang 栈数据结构的实现和应用
本文详细介绍了“栈”这一数据结构的特点,并用Golang实现栈。栈是一种FILO(First In Last Out,即先进后出或后进先出)的数据结构。文章展示了如何用slice和链表来实现栈,并通过golang benchmark测试了二者的性能差异。此外,还提供了几个使用栈结构解决的实际算法问题示例,如有效的括号匹配等。
golang 栈数据结构的实现和应用
|
2月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
58 3
Golang语言之函数(func)进阶篇
|
2月前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
59 2
Golang语言之函数(func)基础篇
|
2月前
|
Unix Go
Golang语言标准库time之日期和时间相关函数
这篇文章是关于Go语言日期和时间处理的文章,介绍了如何使用Go标准库中的time包来处理日期和时间。
47 3
|
6月前
|
Go
golang中置new()函数和make()函数的区别
golang中置new()函数和make()函数的区别
|
3月前
|
算法 NoSQL 关系型数据库
熔断原理与实现Golang版
熔断原理与实现Golang版
|
3月前
|
存储 关系型数据库 Go
SOLID原理:用Golang的例子来解释
SOLID原理:用Golang的例子来解释
|
3月前
|
存储 人工智能 Go
golang 反射基本原理及用法
golang 反射基本原理及用法
28 0