golang函数调用流程详解

前言不管是C语言还是golang语言,都有自己的函数调用流程,主要是在函数调用过程中,各种寄存器和内存堆栈的变化. 理解清楚整个函数调用流程,可以加深对golang语言的了解.
编译源代码对下面的简单函数,通过反汇编和调试器来看下golang的函数调用流程,主要是函数调用过程中的参数传递和关键寄存器的变化 。

golang函数调用流程详解

文章插图
 
为了避免编译器的优化,加上-gcflags '-l -N'选项,-gcflags是给编译器的选项,通过go tool compile可以看到选项列表,-l表示禁止内联,-N表示禁止优化 。一般我们要看一些细节的时候,都需要把这两个选项带上 。
#go build -gcflags '-l -N' 
golang函数调用流程详解

文章插图
 
通过如下命令得到反汇编信息
#go tool objdump --gnu -S 0913 > tmp.s打开tmp.s文件,找到main.test2,能看到main.main和main.test2的反汇编信息,这儿把plan9会把和gnu汇编都显示 。
golang函数调用流程详解

文章插图
 
整个调用流程如下
golang函数调用流程详解

文章插图
 
下面开始具体分析.
前导和结尾分析main.main,发现golang编译器给函数固定插入的前导和结尾有两部分.
第一部分如下.其作用是保证当前goroutine的栈空间足够,其方法是通过得到当前栈空间接近底部的一个地址0x10(CX)(g.stack.stackguard0)并和当前SP比较,如果SP的值小于等于0X10(CX)的值,那么栈的空间已经马上不够用了,必须进行扩容,然后就会jmp到runtime.morestack_noctxt进行扩容,完成之后再JMP到main.main,如果还是不够,就再扩容,直到检查到够了 。因为golang中的goroutine使用的栈都是新建的,初始值默认为2K,随着函数调用层数增加,或者有些函数的局部变量占用空间过大,会导致不够用,这个时候就需要扩容了. 由于这个处理扩容的代码是golang编译器加入的,我们就不用关心了.
0x45dac064488b0c25f8ffffffMOVQ FS:0xfffffff8, CX// mov %fs:0xfffffff8,%rcx0x45dac9483b6110CMPQ 0x10(CX), SP// cmp 0x10(%rcx),%rsp0x45dacd0f8687000000JBE 0x45db5a// jbe 0x45db5a 。。。0x45db5ae821aeffffCALL runtime.morestack_noctxt(SB)// callq 0x4589800x45db5f90NOPL// nop0x45db60e95bffffffJMP main.main(SB)// jmpq 0x45dac0第二部分如下.如果对C编译好的代码进行反汇编也能看到基本完全相同的汇编代码.这部分代码是对callee进行栈空间的分配和回收的.进入一个callee的时候,(0)SP是返回地址,也就是callee执行完成之后,caller要执行的指令地址 。
0x45dad3 4883ec48 SUBQ $0x48, SP // sub $0x48,%rsp0x45dad7 48896c2440 MOVQ BP, 0x40(SP) // mov %rbp,0x40(%rsp)0x45dadc 488d6c2440 LEAQ 0x40(SP), BP // lea 0x40(%rsp),%rbp 。。。0x45db50 488b6c2440 MOVQ 0x40(SP), BP // mov 0x40(%rsp),%rbp0x45db55 4883c448 ADDQ $0x48, SP // add $0x48,%rsp0x45db59 c3 RET // retq先将SP的地址下移0x48,这个就是main.main的栈帧大小(不同的函数需要的栈帧大小不一样,所以这儿的值不同,但是在编译的时候是可以计算出每个函数的局部变量需要的大小,以及调用其他函数需要传参使用的大小的),main.main的局部变量就放在这里面的 。注意这儿把main.main栈帧的顶部,也就是(0X40)SP放了老的BP的值,然后把这个地址放到了BP里面 。
这样就BP的值是一个地址,这个地址里面存放着上一个栈帧的BP的值,其也是一个地址,这样BP的值就弄成了一个链表,可以不断向上串联起来 。当然,如果我们不关心这个事情,那么BP是不需要的,实际上现在gcc有个选项就可以关闭对BP寄存器的使用,golang编译器在优化的情况下也会不使用BP寄存器 。栈帧里面所有的变量通过SP寄存器进行偏移就可以访问到了 。我们看上面的汇编代码,确实都没有用到BP寄存器来定位变量 。
在函数调用完成的时候,通过上面相反的调用顺序将栈空间进行回收.
详细调用过程分析上面已经介绍了基本过程.下面再通过分析main.main调用main.test2的整个过程来加深理解,推荐通过dlv工具来自己一步一步的走,可以加深理解.
进入代码目录使用dlv debug命令开始调试,然后使用b main.main设置断点,c开始运行,使用disassembly看反汇编代码,使用si命令来单指令执行.
1.main.main CALL main.test2之前的栈帧
也就是上面的0x45daf2执行之前的寄存器和栈的情况.现在RBP和RSP是main.main的栈帧,然后main.test2需要的两个入参已经准备好了.


推荐阅读