深度探索Go语言(三) 函数

从代码结构来看,层层函数调用就是一个后进先出的过程,与数据结构中的入栈出栈操作完全一致,所以非常适合用栈来管理函数的局部变量等数据。x86架构提供了对栈的支持,汇编基础部分介绍了栈指针寄存器SP,以及入栈出栈对应的指令。x86还通过CALL指令和RET指令实现了对过程的支持(汇编语言中的过程等价于Go语言中的函数)​。下面就先从CPU的视角,看一下函数调用的过程。

CPU在执行程序时,IP寄存器会指向下一条即将被执行的指令,而SP寄存器会指向栈顶。图3-1为下一条指令即将调用函数f1()函数的场景。

f1()函数的调用由CALL指令实现。CALL指令会先把下一条指令的地址压入栈中,这就是所谓的返回地址,然后会跳转到f1()函数的地址处执行。

当f1()函数执行到最后时会有一条RET指令。RET指令会从栈上弹出返回地址,然后跳转到该地址处继续执行。

 栈帧

在一个函数的调用过程中,栈不只被用来存放返回地址,还被用来传递参数和返回值,以及分配函数局部变量等。

栈帧的布局也是由编译器在编译阶段确定的,其依据就是函数代码,所以也可以说函数栈帧是由编译器管理的。

参照上面的函数栈帧布局示意图,从空间分配的角度来看,函数的栈帧包含以下几部分。

(1)return address:函数返回地址,占用一个指针大小的空间。实际上是在函数被调用时由CALL指令自动压栈的,并非由被调用函数分配。

(2)caller’s BP:调用者的栈帧基址,占用一个指针大小的空间。用来将调用路径上所有的栈帧连成一个链表,方便栈回溯之类的操作,只在部分平台架构上存在。函数通过将栈指针SP直接向下移动指定大小,一次性分配caller’s BP、locals和args to callee所占用的空间,在x86架构上就是使用SUB指令将SP减去指定大小的。

(3)locals:局部变量区间,占用若干机器字。用来存放函数的局部变量,根据函数的局部变量占用空间大小来分配,没有局部变量的函数不分配。

(4)args to callee:调用传参区域,占用若干机器字。这一区域所占空间大小,会按照当前函数调用的所有函数中返回值加上参数所占用的最大空间来分配。当没有调用任何函数时,不需要分配该区间。callee视角的args from caller区间包含在caller视角的args to callee区间内,占用空间大小是小于或等于的关系。

按照一般代码的逻辑,函数的栈帧应该包含返回值、参数、返回地址和局部变量这4部分。从空间分配的角度来看,返回值和参数是由caller负责分配的,CALL指令将返回地址入栈,然后callee通过SUB指令在栈上分配空间。

实际上,代码中的println()函数会被编译器转换为多次调用runtime包中的printlock()、printunlock()、printpointer()、printsp()、printnl()等函数。前两个函数用来进行并发同步,后3个函数用来打印指针、空格和换行。这5个函数均无返回值,只有printpointer()函数有一个参数,会在调用者的args to callee区间占用一个机器字。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值