func add(a, b int) int {
return a + b
}
通过 go build -gcflags ‘-N -l’,我们禁用了编译优化,以使生成的汇编代码更加容易读懂。然后我们就可以用 go 工具 objdump -s main.add func (func是我们用的包名,也是 go build 生成的可执行文件的名称),将这个函数对应的汇编代码导出来。
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
每一行都分成如下四个部分:
源文件名和行号 (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 所做的全部。
MOVQ $0x0, 0x18(SP) 在内存地址 SP+0x18 处放入 0。返回值初始化为0
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。a 通过栈的 SP+0x8 位置传进函数。MOVQ 0x10(SP), CX 将参数 b 移动到 CX。b 通过栈的 SP+0x10 位置传进函数。ADDQ CX, AX 将 a 和 b 相加。MOVQ AX, 0x18(SP) 将结果存到内存地址 SP+0x18。运算结果通过放在栈的 SP+0x18 处传出给调用函数。当被调用函数返回,调用函数将从栈上读取返回值。
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
SUBQ $0x10, SP 将 SP 的值减去 0x10 即 16。这样栈空间增加了 16 字节。
MOVQ BP, 0x8(SP) 将寄存器 BP 中的值存储在 SP+8 的位置,LEAQ 0x8(SP), BP 将 SP+8 所对应的地址存储在 BP 中。这帮助我们建立了栈空间(栈帧) 的链。
汇编的下一行对应于源码的 b := 3。这个命令 MOVQ $0x3, 0(SP) 将 3 放入内存 SP+0 处。这个解决了我们的疑问。当我们把 SP 的值减去 0x10=16,我们空出了能容纳 2 个 8字节 变量的空间:局部变量 b 存储于 SP+0,而 BP 的值存储于 SP+0x8。
后面的 6 行对应于 return a + b。这包括从内存加载 a 和 b, 将它们相加,以及返回计算结果。让我们按顺序来看每一行。
MOVQ 0x18(SP), AX 将存储于 SP+0x18 处的参数 a 移动到寄存器 AX。
ADDQ $0x3, AX 将 AX 的值加 3(尽管我们关闭了优化选项, 但由于某种原因这里还是没有用到存储于 SP+0 的局部变量 b)。
MOVQ AX, 0x20(SP) 将 a+b 的结果存储于 SP+0x20,这里即是我们的返回值存储的位置。
接下来是 MOVQ 0x8(SP), BP 和 ADDQ $0x10, SP。首先恢复 BP 的值,然后将 SP 的值增加 0x10,这样就恢复到了函数刚开始时 SP 的值。
最后是 RET,返回到调用函数。
调用者函数为返回值和参数在栈上申请空间。返回值在栈上的地址高于参数。
如果被调用函数有局部变量,它将通过减小栈指针 SP 的值来申请空间。这与寄存器 BP 也有着一些奇妙的关系。
当函数返回时,一切对 SP 和 BP 的操作都会被回退。
让我们来绘制出 add3() 是如何使用栈的:
SP+0x20: 返回值
SP+0x18: 参数 a
SP+0x10: ??
SP+0x08: BP 原来的值
SP+0x0: 局部变量 b
LEA指令将存储器操作数mem的4位16进制偏移地址送到指定的寄存器。这里,源操作数必须是存储器操作数,目标操作数必须是16位通用寄存器。因该寄存器常用来作为地址指针,故在此最好选用四个间址寄存器BX,BP,SI,DI之一。
LEA 取有效地址指令 (Load Effective Address )