C&Golang函数调用过程详解(三)

161 篇文章 12 订阅

《C&Golang函数调用过程详解(一)》、《C&Golang函数调用过程详解(二)》两文完整介绍了C函数调用过程,在此基础之上,带着《C&Golang函数调用过程详解(一)》中开篇的五个问题,来聊一聊Go函数调用过程。

先来看个简单的Go代码例子:

package main//计算a, b的平方和func sum(a, b int) int {  a2 := a * a  b2 := b * b  c := a2 + b2  return c}func main() {  sum(1, 2)}

使用go build编译该程序,这里需指定-gcflags "-N -l"来关闭编译器优化,不然编译器会优化掉对sum的调用。

PS D:\tools\workSpace\algorithm-算法\test> go build  -gcflags "-N -l" .\main.go

执行上述指令编译后得到可执行程序,来看main的反汇编代码:

Dump of assembler code for function main.main:  0x000000000044f4e0 <+0>: mov   %fs:0xfffffffffffffff8,%rcx #暂时不关注  0x000000000044f4e9 <+9>: cmp   0x10(%rcx),%rsp #暂时不关注  0x000000000044f4ed <+13>: jbe   0x44f51d <main.main+61> #暂时不关注  0x000000000044f4ef <+15>: sub   $0x20,%rsp #为main函数预留32字节栈空间  0x000000000044f4f3 <+19>: mov   %rbp,0x18(%rsp) #保存调用者的rbp寄存器  0x000000000044f4f8 <+24>: lea   0x18(%rsp),%rbp #调整rbp使其指向main函数栈帧开始地址  0x000000000044f4fd <+29>: movq   $0x1,(%rsp) #sum函数的第一个参数(1)入栈  0x000000000044f505 <+37>: movq   $0x2,0x8(%rsp) #sum函数的第二个参数(2)入栈  0x000000000044f50e <+46>: callq 0x44f480 <main.sum> #调用sum函数  0x000000000044f513 <+51>: mov   0x18(%rsp),%rbp #恢复rbp寄存器的值为调用者的rbp  0x000000000044f518 <+56>: add   $0x20,%rsp #调整rsp使其指向保存有调用者返回地址的栈单元  0x000000000044f51c <+60>: retq   #返回到调用者  0x000000000044f51d <+61>: callq 0x447390 <runtime.morestack_noctxt> #暂时不关注  0x000000000044f522 <+66>: jmp   0x44f4e0 <main.main> #暂时不关注End of assembler dump.

上述指令中前三条和最后两条是Go编译器插入用于检查栈溢出的代码,无需关注,其它部分跟C函数差不多,差别就是Go函数调用时参数放在了栈上(第七、八条指令将参数放在了栈上),从第四条指令可以看出,编译器给main预留了32个字节用于存放main栈基地址rbp以及调用sum的两个参数,这三个元素各占8字节,一共使用了24个字节,剩下的8字节用于存放sum返回值。

来看下sum的汇编代码:

Dump of assembler code for function main.sum:  0x000000000044f480 <+0>: sub   $0x20,%rsp #为sum函数预留32字节的栈空间  0x000000000044f484 <+4>: mov   %rbp,0x18(%rsp) #保存main函数的rbp  0x000000000044f489 <+9>: lea   0x18(%rsp),%rbp #设置sum函数的rbp  0x000000000044f48e <+14>: movq   $0x0,0x38(%rsp) #返回值初始化为0  0x000000000044f497 <+23>: mov   0x28(%rsp),%rax #从内存中读取第一个参数a(1)到rax  0x000000000044f49c <+28>: mov   0x28(%rsp),%rcx #从内存中读取第一个参数a(1)到rcx  0x000000000044f4a1 <+33>: imul   %rax,%rcx #计算a * a,并把结果放在rcx  0x000000000044f4a5 <+37>: mov   %rcx,0x10(%rsp) #把rcx的值(a * a)赋值给变量a2  0x000000000044f4aa <+42>: mov   0x30(%rsp),%rax #从内存中读取第二个参数a(2)到rax  0x000000000044f4af <+47>: mov   0x30(%rsp),%rcx #从内存中读取第二个参数a(2)到rcx  0x000000000044f4b4 <+52>: imul   %rax,%rcx #计算b * b,并把结果放在rcx  0x000000000044f4b8 <+56>: mov   %rcx,0x8(%rsp) #把rcx的值(b * b)赋值给变量b2  0x000000000044f4bd <+61>: mov   0x10(%rsp),%rax #从内存中读取a2到寄存器rax  0x000000000044f4c2 <+66>: add   %rcx,%rax #计算a2 + b2,并把结果保存在rax  0x000000000044f4c5 <+69>: mov   %rax,(%rsp) #把rax赋值给变量c, c = a2 + b2  0x000000000044f4c9 <+73>: mov   %rax,0x38(%rsp) #将rax的值(a2 + b2)复制给返回值  0x000000000044f4ce <+78>: mov   0x18(%rsp),%rbp #恢复main函数的rbp  0x000000000044f4d3 <+83>: add   $0x20,%rsp #调整rsp使其指向保存有返回地址的栈单元  0x000000000044f4d7 <+87>: retq   #返回main函数End of assembler dump.

上述指令比较直观,基本就是对Go里的sum函数的直接翻译,可看到sum通过rsp从main函数栈中获取值,返回值也通过rsp保存在main栈帧中。

结合上述汇编代码,来看下执行完sum的0x000000000044f4c9 <+73>: mov    %rax,0x38(%rsp)指令,但并未开始执行下一条指令时的栈和寄存器的状态图,来加深对函数调用过程中的参数传递、返回值以及局部变量在栈上的位置关系的理解,如下:

来总结一下函数调用过程:

  1. 参数传递。gcc编译的C/C++一般通过寄存器传递参数,在AMD64 Linux平台,gcc约定函数调用时前6个参数分别通过rdi、rsi、rdx、r10、r9、r8传递,Go则在通过栈将参数传递给被调用函数,最后一个参数最先入栈,第一个参数最后入栈,参数在调用者栈帧中,被调用函数通过rsp加一定偏移量来获取参数。

  2. call指令负责将执行call时的rip(函数返回地址)入栈。

  3. gcc通过rbp加偏移量访问局部和临时变量,Go则用rsp加偏移量方式来访问。

  4. ret负责将call入栈的函数返回地址出栈给rip,从而实现从被调用函数返回到调用函数继续执行。

  5. gcc用rax返回函数调用的返回值,Go则用栈返回函数调用的返回值。

好啦,到这里本文就结束了,喜欢的话就来个三连击吧。

扫码关注公众号,获取更多优质内容。

  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luyaran

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值