深入理解Go函数调用原理

函数和栈

在程序运行的过程中,会涉及到对函数的调用,调用时IP寄存器会指向被调用函数的地址,函数返回后继续执行本函数剩下的代码

程序执行单元(线程或者协程)在执行过程中需要记录程序上下文的数据结构,包括局部变量,BP,参数,返回值等。程序用栈帧这种结构来记录这些信息

要使得程序正常运行,需要时刻知道两个信息:

  • 当前栈帧在哪?
  • 当前在执行什么指令?

这两个信息存在使用BP,SP和IP中:

  • BP:存储函数调用堆栈基址指针的寄存器,即当前函数栈帧是从哪里开始的

  • SP:存储当前函数栈顶的位置,也就是当前栈的内存分配到了哪里

    • BP和SP寄存器表示一个函数栈的开始和结束
  • IP:指向下一条指令地址

栈帧

go的栈帧结构如下:
在这里插入图片描述

栈帧由以下5个部分组成:

  • Ret address:当前函数执行结束后,跳转到哪个位置继续执行
  • Caller bp:调用者bp寄存器的值
  • Local var:本地变量
  • Callee ret:被调用方的返回值
  • Callee param:被调用方需要的参数

每个函数的汇编中,都有如下的样板代码:

TEXT    "".funXXX(SB), ABIInternal, $stackFrameSize-X
SUBQ    $stackFrameSize, SP                  // 将SP下移stackFrameSize大小,即开辟栈帧
MOVQ    BP, stackFrameSize - 8(SP)           // 将callerbp的值保存到栈帧开头的8个字节处
LEAQ    stackFrameSize - 8(SP), BP           // 将当前函数的bp值放入bp寄存器
...
CALL    "".funcXXXX(SB)                      // 调用其他函数
...
MOVQ    stackFrameSize - 8(SP), BP           // 将callerbp恢复到bp寄存器
ADDQ    $stackFrameSize, SP                  // 将sp上移stackFrameSize大小,即释放栈帧
RET      // 返回,将SP位置的值放入ip寄存器,并且sp上移8位

下面以一个简单的go代码来分析具体的函数调用过程

汇编

假设有如下go代码,主函数main,调用函数cal,代码比较简单

package main

func main() {
   cal(1, 2)
}

func cal(a, b int) int {
   c := 1
   return a + b + c
}

go版本:1.14.10

执行命令:go tool compile -S -N -l main.go生成如下的汇编代码,其中:

  • -S:输出汇编代码
  • -N:禁止优化
  • -l:禁止内联

如果不加上 -N -l 的参数,编译器会对汇编代码进行优化,编译结果会跟这里的差别很大

go中几个常见的汇编指令:

  • CALL funa:将下一条指令的地址压栈,跳转到函数funa的起始地址
  • RET:将栈顶元素弹出到IP寄存器,即SP = SP + 8,跳转到之前压入的下一条指令位置

Cal

TEXT    "".main(SB), ABIInternal, $32-0
SUBQ    $32, SP
MOVQ    BP, 24(SP)
LEAQ    24(SP), BP
MOVQ    $1, (SP)
MOVQ    $2, 8(SP)
CALL    "".cal(SB)
MOVQ    24(SP), BP
ADDQ    $32, SP
RET
  • 开辟栈空间,保存/设置BP,SP指针

    • SUBQ $32, SP:SP寄存器的值减32,为main方法分配32字节的空间
    • MOVQ BP, 24(SP):将BP寄存器的值,即上一个栈的bp保存到SP+24位置的栈空间上
    • LEAQ 24(SP), BP:将SP+24的地址保存到BP寄存器,设置当前栈的BP
  • 准备参数,调用cal方法:

    • MOVQ $1, (SP):将立即数1放到栈顶位置
    • MOVQ $2, 8(SP):将立即数2放到栈顶+8的位置
    • CALL "".cal(SB):调用cal方法
  • 销毁栈帧`:

    • MOVQ 24(SP), BP:恢复上一个栈的BP,将SP+24位置的值放到BP寄存器
    • ADDQ $32, SP:SP+=32:"销毁"当前栈帧
    • RET:方法返回

在这里插入图片描述

在main函数中一共分配了40字节的栈空间:

  • 头8个字节存放caller BP
  • 接下来8个字节用于接收cal方法的返回值
  • 剩下16分字节用于设置调用cal方法的两个参数

doCal

go代码:

func cal(a, b int) int {
   c := 1
   return a + b + c
}

对应的汇编:

TEXT    "".cal(SB), NOSPLIT|ABIInternal, $16-24
SUBQ    $16, SP
MOVQ    BP, 8(SP)
LEAQ    8(SP), BP
MOVQ    $0, "".~r2+40(SP)
MOVQ    $1, "".c(SP)
MOVQ    "".a+24(SP), AX
ADDQ    "".b+32(SP), AX
INCQ    AX
MOVQ    AX, "".~r2+40(SP)
MOVQ    8(SP), BP
ADDQ    $16, SP
RET
  • 第一行

    • $16:为cal方法分配了16自己的栈帧
    • 24:参数和返回加起来有24字节
  • 2,3,4行:开辟16字节的栈空间,保存caller bp

  • MOVQ $0, "".~r2+40(SP):清空返回位置

  • MOVQ $1, "".c(SP):将局部变量c放到SP位置

  • MOVQ "".a+24(SP), AX:将SP+8位置的数放到AX

  • ADDQ "".b+32(SP), AX:将AX的值加上SP+32位置的数,再放到AX,此时AX = a + b

  • INCQ AX:AX = AX + 1,此时AX = a + b + c

    • 编译器做了优化,用INCQ指令实现 + c
  • MOVQ AX, "".~r2+40(SP):将AX的值放到SP+40位置

  • 最后3行:恢复caller bp,销毁栈空间,返回到caller

cal栈帧如下:

在这里插入图片描述

可以看出:

  • go中传递参数和返回值都用的栈,这样能方便传递多个返回值
  • 函数的入参和返回值的空间都在caller的栈帧中分配
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值