让我们来看几个 go 函数调用的简单例子。通过研究 go 编译器为这些函数生成的汇编代码,我们来看看函数调用是如何工作的。这个课题对于一篇小小的文章来讲有点费劲,但是别担心,汇编语言是非常简单的,连 CPU 都能理解它。

来看看我们的第一个函数,对,我们简单的将两个数相加。

go build -gcflags '-N -l'objdump -s main.add func

如果你以前从来没有接触过汇编语言,那么恭喜,现在它对你来说是个新的东西。我在 mac 电脑上做的试验,所以汇编代码是英特尔 64位 的。

在这里我们应该看什么呢?每一行都分成如下四个部分:

  • 源文件名和行号 (main.go:15)。源文件的这一行的代码被翻译成带行号的汇编指令。Go 的一行有可能被翻译成多行汇编。
  • 在目标文件中的偏移量(如 0x22C0)。
  • 机器码(如 48c744241800000000)。这是 CPU 真正执行的二进制机器码。我们不会去看这部分,基本上也没人会去看。
  • 机器码的汇编语言表达形式。这部分是我们希望去理解的。

让我们聚焦于汇编代码这部分。

  • MOVQ, ADDQ 以及 RET 是指令。它们告诉 CPU 要做什么操作。跟在指令后面的是参数,告诉 CPU 要对谁进行操作。
  • SP, AX 及 CX 是 CPU 的寄存器,是 CPU 存储工作用到的变量的地方。除了这几个,CPU 还会用到其它的一些寄存器。
  • SP 是个特殊的寄存器,它用于存储当前的栈指针。栈是用于存储局部变量、函数的参数及函数返回地址的内存区域。每个 goroutine 对应一个栈。当一个函数调用另一个函数,被调用函数再继续调用别的函数,每个函数都会在栈上得到一个内存区域。函数调用时,SP 的值会减去被调用函数所需栈空间大小,这样就得到了一块供被调用函数使用的内存区域。
  • 0x8(SP) 指向比 SP 所指内存位置往后8个字节的位置。

所以,几个要素包括:内存位置、CPU 寄存器、在内存和寄存器之间移动数据的指令,以及对寄存器的操作。这些差不多就是 CPU 所做的全部。

ab
MOVQ $0x0, 0x18(SP)MOVQ 0x8(SP), AXMOVQ 0x10(SP), CXADDQ CX, AXMOVQ AX, 0x18(SP)RET
aba+bMOVQ 0x8(SP), AXaaMOVQ 0x10(SP), CXbbADDQ CX, AXabMOVQ AX, 0x18(SP)
ab
MOVQ $0x0, 0x18(SP)

来看看我们学到了什么。

  • 看起来参数被存储在栈上,第一个参数位于 SP+0x8,另一个位于紧接着的更高地址的位置。
  • 返回值看起来也是通过栈存储的,在比参数更高地址的位置。

现在我们来看另一个函数。这个函数有一个局部变量,但我们还是让它尽量保持简单。

用同样的方式我们得到了以下的汇编代码。

啊,看起来比上一个要复杂一些。让我们试着理解它。

前四条指令对应的是第 15 行的源代码。这行是:

这行看起来并没有做太多。所以这可能是函数的某种"序言"。让我们来分解一下。

SUBQ $0x10, SPMOVQ BP, 0x8(SP)LEAQ 0x8(SP), BPMOVQ $0x0, 0x20(SP)
b := 3MOVQ $0x3, 0(SP)b
return a + bab
MOVQ 0x18(SP), AXaADDQ $0x3, AXbMOVQ AX, 0x20(SP)a+bMOVQ 0x8(SP), BPADDQ $0x10, SPRET

那么我们学到了什么?

  • 调用者函数为返回值和参数在栈上申请空间。返回值在栈上的地址高于参数。
  • 如果被调用函数有局部变量,它将通过减小栈指针 SP 的值来申请空间。这与寄存器 BP 也有着一些奇妙的关系。
  • 当函数返回时,一切对 SP 和 BP 的操作都会被回退。

让我们来绘制出 add3() 是如何使用栈的:

RET

如果你喜欢这篇文章,或者从中学到了东西,请点赞,这样其他人也能看到它。


本文由 原创编译, 荣誉推出

更多资讯欢迎关注公众号:Go语言中文网