> * 原文地址：[Anatomy of a function call in Go](https://syslog.ravelin.com/anatomy-of-a-function-call-in-go-f6fc81b80ecc#.povigaliw)
> * 原文作者：[Phil Pearl](https://syslog.ravelin.com/@philpearl?source=post_header_lockup)
> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 译者：[xiaoyusilen](http://xiaoyu.world)
> * 校对者：[1992chenlu](https://github.com/1992chenlu)，[Zheaoli](https://github.com/Zheaoli)

# 解析 Go 中的函数调用 #

让我们来看一些简单的 Go 的函数，然后看看我们能否明白函数调用是怎么回事。我们将通过分析 Go 编译器根据函数生成的汇编来完成这件事。对于一个小小的博客来讲，这样的目标可能有点不切实际，但是别担心，汇编语言很简单。哪怕是 CPU 都能读懂。

<img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/800/1*CKK4XrLm3ylzsQzNbOaroQ.png">

图片来自 Rob Baines [https://github.com/telecoda/inktober-2016](https://github.com/telecoda/inktober-2016)

这是我们的第一个函数。对，我们只是让两个数相加。

```
func add(a, b int) int {
        return a + b
}
```

我们编译的时候需要关闭优化，这样方便我们去理解生成的汇编代码。我们用 `go build -gcflags 'N -l'` 这个命令来完成上述操作。然后我们可以用 `go tool objdump -s main.add func` 输出我们函数的具体细节（这里的 func 是我们的包名，也就是我们刚刚用 go build 编译出的可执行文件）。

如果你之前没有学过汇编，那么恭喜你，你将接触到一个全新的事物。另外我会在 Mac 上完成这篇博客的代码，因此所生成的是 Intel 64-bit 汇编。

```
 main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP)
 main.go:21 0x22c9 488b442408  MOVQ 0x8(SP), AX
 main.go:21 0x22ce 488b4c2410  MOVQ 0x10(SP), CX
 main.go:21 0x22d3 4801c8   ADDQ CX, AX
 main.go:21 0x22d6 4889442418  MOVQ AX, 0x18(SP)
 main.go:21 0x22db c3   RET
```

现在我们看到了什么？如下所示，每一行被分为了4部分：

- 源文件的名称和行号（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 所完成的事情了。

现在让我们从第一条指令开始看每一条内容。别忘了我们需要从内存中加载两个参数 `a` 和 `b`，把它们相加，然后返回至调用函数。

1. `MOVQ $0x0, 0x18(SP)` 将 0 置于存储单元 SP+0x18 中。 这句代码看起来有点抽象。
2. `MOVQ 0x8(SP), AX` 将存储单元 SP+0x8 中的内容放到 CPU 寄存器 AX 中。也许这就是从内存中加载的我们所使用的参数之一？
3. `MOVQ 0x10(SP), CX` 将存储单元 SP+0x10 的内容置于 CPU 寄存器 CX 中。 这可能就是我们所需的另一个参数。
4. `ADDQ CX, AX` 将 CX 与 AX 相加，将结果存到 AX 中。好，现在已经把两个参数相加了。
5. `MOVQ AX, 0x18(sp)` 将寄存器 AX 的内容存储在存储单元 SP+0x18 中。这就是在存储相加的结果。
6. `RET` 将结果返回至调用函数。

记住我们的函数有两个参数 `a` 和 `b`，它计算了 `a+b` 并且返回了结果。`MOVQ 0x8(SP), AX` 将参数 `a` 移到 AX 中，在 SP+0x8 的堆栈中 `a` 将被传给函数。`MOVQ 0x10(SP), CX` 将参数 `b` 移到 CX 中，在 SP+0x10 的堆栈中 `b` 将被传给函数。`ADDQ CX, AX` 使 `a` 和 `b` 相加。`MOVQ AX, 0x18(SP)` 将结果存储到 SP+0x18 中。 现在相加的结果被存储在 SP+0x18 的堆栈中，当函数返回调用函数时，可以从栈中读取结果。

我假设 `a` 是第一个参数，`b` 是第二个参数。我不确定是不是这样。我们需要花一点时间来完成这件事，但是这篇文章已经很长了。

那么有点神秘的第一行代码究竟是做什么用的？`MOVQ $0X0, 0X18(SP)` 将 0 存储至 SP+0x18 中，而 SP+0x18 是我们存储相加结果的地方。我们可以猜测，这是因为 Go 把没有初始化的值设置为 0 ，我们已经关闭了优化，即使没有必要，编译器也会执行这个操作。

所以我们从中明白了什么：

- 好，看起来参数都存在堆栈中，第一个参数存储在 SP+0x8 中，另一个在更高编号的地址中。
- 并且看上去返回的结果存储在参数后边，一个更高编号的地址中。

现在让我们看另一个函数。这个函数有一个局部变量，不过我们依然会让它看起来很简单。

```
func add3(a int) int {
    b := 3
    return a + b
}
```

我们用和刚才一样的过程来获取程序集列表。

```
TEXT main.add3(SB) 
/Users/phil/go/src/github.com/philpearl/func/main.go
 main.go:15 0x2280 4883ec10  SUBQ $0x10, SP
 main.go:15 0x2284 48896c2408  MOVQ BP, 0x8(SP)
 main.go:15 0x2289 488d6c2408  LEAQ 0x8(SP), BP
 main.go:15 0x228e 48c744242000000000 MOVQ $0x0, 0x20(SP)
 
 main.go:16 0x2297 48c7042403000000 MOVQ $0x3, 0(SP)
 
 main.go:17 0x229f 488b442418  MOVQ 0x18(SP), AX
 main.go:17 0x22a4 4883c003  ADDQ $0x3, AX
 main.go:17 0x22a8 4889442420  MOVQ AX, 0x20(SP)
 main.go:17 0x22ad 488b6c2408  MOVQ 0x8(SP), BP
 main.go:17 0x22b2 4883c410  ADDQ $0x10, SP
 main.go:17 0x22b6 c3   RET
```

喔！看起来有点复杂。让我们来试试。

前4条指令是根据源代码中的第15行列出的。这行代码是这样的：

```
func add3(a int) int {
```

这一行代码似乎没有做什么。所以这可能是一种声明函数的方法。让我们分析一下。

- `SUBQ $0x10, SP` 从 SP 减去 0x10=16。这个操作为我们释放了 16 字节的堆栈空间
- `MOVQ BP, 0x8(SP)` 将寄存器 BP 中的值存储至 SP+8 中，然后 `LEAQ 0x8(SP), BP` 将地址 SP+8 中的内容加载到 BP 中。现在我们已经有空间可以存储 BP 中之前所存的内容，然后将 BP 中的内容存储至刚刚分配的存储空间中，这有助于建立堆栈区域链（或者堆栈框架）。这有点神秘，不过在这篇文章中我们恐怕不会解决这个问题。
- 在这一部分的最后是 `MOVQ $ 0x0, 0x20 (SP)`，它和我们刚刚分析的最后一句类似，就是将返回值初始化为0。

下一行对应的是源码中的 `b := 3`，`MOVQ $03x, 0(SP)` 把 3 放到 SP+0 中。这解决了我们的一个疑惑。当我们从 SP 中减去 0x10 = 16 时，我们得到了可以存储两个 8 字节值的空间：我们的局部变量 `b` 存储在 SP+0 中，而 BP 之前的值存储在 SP+0x08 中。

接下来的 6 行程序集对应于 `return a + b`。这需要从内存中加载 `a` 和 `b`，然后将它们相加，并且返回结果。让我们依次看看每一行。

- `MOVQ 0x18(SP), AX` 将存储在 SP+0x18 的参数 `a` 移动到寄存器 AX 中
- `ADDQ $0x3, AX` 将 3 加到 AX（由于某些原因，它不使用我们存储在 SP+0 的局部变量 `b`，尽管编译时优化被关闭了）
- `MOVQ AX, 0x20(SP)` 将 `a+b` 的结果存储到 SP+0x20 中，也就是我们返回结果所存的地方。
- 接下来我们得到的是 `MOVQ 0x8(SP), BP` 以及 `ADDQ $0x10, SP`，这些将恢复BP的旧值，然后将 0x10 添加到 SP，将其设置为该函数开始时的值。
- 最后我们得到了 `RET`，将要返回给调用函数的。

所以我们从中学到了什么呢？

- 调用函数在堆栈中为返回值和参数分配空间。返回值的存储地址比参数的存储地址高。
- 如果被调用函数有局部变量，则通过减少堆栈指针 SP 的值为它们分配空间。它也和寄存器 BP 做了一些神秘的事情。
- 当函数返回任何对 SP 和 BP 的操作都会相反。

让我们看看堆栈在 add3() 方法中如何使用：

```
SP+0x20: the return value


SP+0x18: the parameter a


SP+0x10: ??


SP+0x08: the old value of BP

SP+0x0: the local variable b
```

如果你觉得文章中没有提到 SP+0x10，所以不*知道*这是干什么用的。我可以告诉你，这是存储返回地址的地方。这是为了让 `RET` 指令知道返回到哪里去。

这篇文章已经足够了。 希望如果以前你不知道这些东西如何工作，但是现在你觉得你已经有了一些了解，或者如果你被汇编吓倒了，那么也许它不那么晦涩难懂了。 如果你想了解有关汇编的更多信息，请在评论中告诉我，我会考虑在之后的文章中写出来。

既然你已经看到这儿了，如果喜欢我的这篇文章或者可以从中学到一点什么的话，那么请给我点个赞这样这篇文章就可以被更多人看到了。