- 原文地址:Anatomy of a function call in Go
- 原文做者:Phil Pearl
- 译文出自:掘金翻译计划
- 译者:xiaoyusilen
- 校对者:1992chenlu,Zheaoli
让咱们来看一些简单的 Go 的函数,而后看看咱们可否明白函数调用是怎么回事。咱们将经过分析 Go 编译器根据函数生成的汇编来完成这件事。对于一个小小的博客来说,这样的目标可能有点不切实际,可是别担忧,汇编语言很简单。哪怕是 CPU 都能读懂。前端
图片来自 Rob Baines github.com/telecoda/in…react
这是咱们的第一个函数。对,咱们只是让两个数相加。android
func add(a, b int) int { return a + b }复制代码
咱们编译的时候须要关闭优化,这样方便咱们去理解生成的汇编代码。咱们用 go build -gcflags 'N -l'
这个命令来完成上述操做。而后咱们能够用 go tool objdump -s main.add func
输出咱们函数的具体细节(这里的 func 是咱们的包名,也就是咱们刚刚用 go build 编译出的可执行文件)。ios
若是你以前没有学过汇编,那么恭喜你,你将接触到一个全新的事物。另外我会在 Mac 上完成这篇博客的代码,所以所生成的是 Intel 64-bit 汇编。git
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部分:github
让咱们将注意力集中在最后一部分,汇编语言。c#
所以,咱们的工做的内容包含存储单元,CPU 寄存器,用于在存储器和寄存器之间移动值的指令以及寄存器上的操做。 这几乎就是一个 CPU 所完成的事情了。后端
如今让咱们从第一条指令开始看每一条内容。别忘了咱们须要从内存中加载两个参数 a
和 b
,把它们相加,而后返回至调用函数。markdown
MOVQ $0x0, 0x18(SP)
将 0 置于存储单元 SP+0x18 中。 这句代码看起来有点抽象。MOVQ 0x8(SP), AX
将存储单元 SP+0x8 中的内容放到 CPU 寄存器 AX 中。也许这就是从内存中加载的咱们所使用的参数之一?MOVQ 0x10(SP), CX
将存储单元 SP+0x10 的内容置于 CPU 寄存器 CX 中。 这可能就是咱们所需的另外一个参数。ADDQ CX, AX
将 CX 与 AX 相加,将结果存到 AX 中。好,如今已经把两个参数相加了。MOVQ AX, 0x18(sp)
将寄存器 AX 的内容存储在存储单元 SP+0x18 中。这就是在存储相加的结果。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 ,咱们已经关闭了优化,即便没有必要,编译器也会执行这个操做。
因此咱们从中明白了什么:
如今让咱们看另外一个函数。这个函数有一个局部变量,不过咱们依然会让它看起来很简单。
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
,将要返回给调用函数的。因此咱们从中学到了什么呢?
让咱们看看堆栈在 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
指令知道返回到哪里去。
这篇文章已经足够了。 但愿若是之前你不知道这些东西如何工做,可是如今你以为你已经有了一些了解,或者若是你被汇编吓倒了,那么也许它不那么晦涩难懂了。 若是你想了解有关汇编的更多信息,请在评论中告诉我,我会考虑在以后的文章中写出来。
既然你已经看到这儿了,若是喜欢个人这篇文章或者能够从中学到一点什么的话,那么请给我点个赞这样这篇文章就能够被更多人看到了。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。