本文简单地对Go的函数调用栈进行了探究。
需要声明的一点是,根据Go语言文档《A Quick Guide to Go's Assembler》的说明,这里反编译得到Go的汇编程序不是底层机器执行代码的直接表示。 只有其中一部分是和底层机器码一一对应的,另外一部分则没有。
函数

反编译及代码分析
执行下面的语句得到汇编文件
shell go tool compile -S -N -l main.go > main.s
去除掉跟Go语言垃圾回收有关的FUNCDATA、PCDATA后,main函数、myFunction1函数、myFunction2函数的部分汇编代码分别如图1、图2和图3所示。这里所提到的SP寄存器并不是真正的寄存器,是工具链维护的虚拟寄存器。主要部分指令的解释如图1、图2和图3中的注释所示,不再赘述。

图1. main函数汇编代码(部分)

图2. myFunction1函数汇编代码(部分)

图3. myFuntion2函数汇编代码(部分)
函数调用栈分析
图4~8展示了不同情况下的函数调用栈示意图,图的上边是高地址,下边是低地址,栈是向下生长的。
在执行CALL ''.myFunction1(SB)调用myFunction1函数前,main函数调用栈如图4的蓝色部分所示。从main函数的汇编代码可以看出,main函数分别在(SP)、8(SP)存放立即数77、66,也即要传递给myFuntion1函数的参数a、参数b。

图4. 调用myFuntion1前main函数调用栈示意图
在要退出myFunction1函数前,main函数和myFunction1函数调用栈分别如图5的蓝色、红色部分所示。从myFunction1的汇编代码可以看出,myFunction1函数分别从24(SP)、32(sp)(对应于图4中main函数调用栈的(SP)、8(SP))处获取参数a、参数b,将返回值放在40(SP)、48(sp)(对应于图4中main函数调用栈的16(SP)、24(SP)),局部变量c则放在(SP)(位于myFunction2函数自己的栈中)。

图5. main函数和myFunction1函数调用栈示意图
在退出myFunction1函数之后,并且执行CALL ''.myFunction2(SB)调用myFunction2函数前,main函数调用栈如图6的蓝色部分所示。从main函数的汇编代码可以看出,main函数分别在64(SP)、56(SP)、48(SP)存放变量x、y、z,并且分别将它们拷贝至(SP)、8(SP)、16(SP),也即要传递给myFuntion2函数的参数a、参数b、参数c。注意,24(SP)处保存的值没有被覆盖/擦除,仍然保留了myFunction1函数的第二个返回值。

图6. 调用myFuntion2前main函数调用栈示意图
在要退出myFunction2函数前,main函数和myFunction2函数调用栈分别如图7中的蓝色、红色部分所示。从myFunction2的汇编代码可以看出,myFunction2函数分别从24(SP)、32(SP)、40(SP)处(对应于 main函数调用栈的(SP)、8(SP)、16(SP))获取参数a、参数b、参数c,将返回值放在48(SP)处(对应于main函数调用栈的24(SP)),局部变量d则放在(SP)处(位于myFunction2函数自己的栈中)。

图7. main函数和myFunction2函数调用栈示意图
从上述情况可见,Go中的函数调用栈和C/C++有一定的区别。区别在于,Go中的函数调用中会把传递给被调用函数的参数和返回值保存在自己的栈中,而被调用函数则需要从调用函数的栈中去获取参数变量,并且返回值要写入到被调用函数栈中。Go中局部变量和C/C++一样,都是保存在所属函数的栈中。因此,Go的函数调用栈形式如图8所示。

图8. Go函数调用栈示意图
参考资料
——————————————————
BY:HW-YZQ

























一起学习











苏公安网备 32011402010933号