本文主要收集一些例子,以后阅读 Go 汇编时遇到忘记的指令可以查询。
例子 1
package main
func main() {
l := []int{9, 45, 23, 67, 78}
t := 0
for _, v := range l {
t += v
}
println(t)
}
截取了一段汇编如下
0x0026 00038 (3.go:4) MOVUPS X15, ""..autotmp_5+40(SP)
0x002c 00044 (3.go:4) MOVUPS X15, ""..autotmp_5+48(SP)
0x0032 00050 (3.go:4) MOVUPS X15, ""..autotmp_5+64(SP)
0x0038 00056 (3.go:4) LEAQ ""..autotmp_5+40(SP), AX
0x003d 00061 (3.go:4) MOVQ AX, ""..autotmp_4+80(SP)
0x0042 00066 (3.go:4) TESTB AL, (AX)
其中
MOVUPS
是 Intel 平台的 SIMD 指令,通过 X15 代表的固定的零寄存器对起始地址为SP + 40 的连续 128 bit (16个字节)进行清零。如果是作用在 slice 结构体上,则是 len 和 cap 为0。LEAQ
取 SP+40 内存单元的地址,存入 AX 寄存器。TESTB
把 AL 与 AX 寄存器中的值做逻辑与操作,但不会改变寄存器的值,只是设置相关标志位。这里是用做 nil check,如果加载 AX 失败会触发段错误信号 SIGSEGV,触发 Go Runtime 抛出 Panic。选择TESTB
仅仅是因为指令短小。
接下来就是循环体部分
0x00da 00218 (3.go:8) JMP 220
0x00dc 00220 (3.go:8) MOVQ ""..autotmp_6+32(SP), AX
0x00e1 00225 (3.go:8) CMPQ ""..autotmp_7+24(SP), AX
0x00e6 00230 (3.go:8) JGT 234
0x00e8 00232 (3.go:8) JMP 286
0x00ea 00234 (3.go:8) MOVQ ""..autotmp_6+32(SP), AX
0x00ef 00239 (3.go:8) SHLQ $3, AX
0x00f3 00243 (3.go:8) ADDQ ""..autotmp_3+112(SP), AX
0x00f8 00248 (3.go:8) MOVQ (AX), AX
0x00fb 00251 (3.go:8) MOVQ AX, "".v+8(SP)
0x0100 00256 (3.go:9) MOVQ "".t+16(SP), CX
0x0105 00261 (3.go:9) ADDQ CX, AX
0x0108 00264 (3.go:9) MOVQ AX, "".t+16(SP)
0x010d 00269 (3.go:9) JMP 271
0x010f 00271 (3.go:8) MOVQ ""..autotmp_6+32(SP), AX
0x0114 00276 (3.go:8) INCQ AX
0x0117 00279 (3.go:8) MOVQ AX, ""..autotmp_6+32(SP)
0x011c 00284 (3.go:8) JMP 220
0x011e 00286 (3.go:12) PCDATA $1, $0
0x011e 00286 (3.go:12) NOP
CMPQ
, 比较 SP+24 和 AX 所存值得大小,实际上,这个操作是把 SP+24 的值减去 AX,得到的值存在另一个寄存器中,供JGT
、JLT
指令使用JGT
: 有了前面的结果,JGT 只要判断如果值大于 0 ,则跳到 234 行指令。- 接下里 234 - 271 行,就是从数组拿出一个元素,赋值给变量 v,然后加上 t 并把结果存在 t 中。
INCQ
: 增加数组 index 的值,这个index 存在 AX 中 。SHLQ
是左移的意思,我没有看懂这里需要左移 3 位的意思?
例子 2
下面的代码一个用 new,一个直接构建结构体,那么最终生成的代码有区别吗?
package main
type Person struct {
Age int
}
func main() {
p1 := new(Person)
p2 := &Person{}
p1.Age = 11
p2.Age = 89
}
我们来看一下生成的汇编:
0x000e 00014 (t.go:10) MOVQ $0, ""..autotmp_2+8(SP)
0x0017 00023 (t.go:10) LEAQ ""..autotmp_2+8(SP), AX
0x001c 00028 (t.go:10) MOVQ AX, "".p1+24(SP)
0x0021 00033 (t.go:13) MOVQ $0, ""..autotmp_4(SP)
0x0029 00041 (t.go:13) LEAQ ""..autotmp_4(SP), AX
0x002d 00045 (t.go:13) MOVQ AX, ""..autotmp_3+32(SP)
0x0032 00050 (t.go:13) TESTB AL, (AX)
0x0034 00052 (t.go:13) MOVQ $0, ""..autotmp_4(SP)
0x003c 00060 (t.go:13) MOVQ AX, "".p2+16(SP)
0x0041 00065 (t.go:15) MOVQ "".p1+24(SP), AX
0x0046 00070 (t.go:15) TESTB AL, (AX)
0x0048 00072 (t.go:15) MOVQ $11, (AX)
0x004f 00079 (t.go:16) MOVQ "".p2+16(SP), AX
0x0054 00084 (t.go:16) TESTB AL, (AX)
0x0056 00086 (t.go:16) MOVQ $89, (AX)
可见,两者在本质上没什么差别。
例子 3
再来看一个结构体带方法的例子。
package main
type Dog struct {
Name string
Age int
}
func (t Dog) Sleep() string {
return t.Name
}
func (t *Dog) Say() string {
return t.Name
}
func main() {
var d1 = &Dog{"WangWang", 43}
a := d1.Say()
b := d1.Sleep()
_, _ = a, b
}
用 go tool compile -S -l -N main.go
生成未优化的汇编,如果去掉了 l, N 这样的参数,生成的汇编会比较难懂。
首先看 main 函数的变量初始化,在这之前,需要了解Go 是如何表示 string 类型的
type StringHeader struct {
Data uintptr
Len int
}
0x0018 00024 (t3.go:17) MOVQ $0, main..autotmp_4+96(SP) ;StringHeader.Data = 0
0x0021 00033 (t3.go:17) MOVUPS X15, main..autotmp_4+104(SP);StringHeader.Len= 0
0x0027 00039 (t3.go:17) LEAQ main..autotmp_4+96(SP), AX ;AX=&StringHeader.Data
0x002c 00044 (t3.go:17) MOVQ AX, main..autotmp_3+32(SP);
0x0031 00049 (t3.go:17) TESTB AL, (AX) ; nil check
0x0033 00051 (t3.go:17) MOVQ $8, main..autotmp_4+104(SP);StringHeader.Len = 8
0x003c 00060 (t3.go:17) LEAQ go.string."WangWang"(SB), CX; CX="WangWang"
0x0043 00067 (t3.go:17) MOVQ CX, main..autotmp_4+96(SP)
0x0048 00072 (t3.go:17) TESTB AL, (AX); nil check
0x004a 00074 (t3.go:17) MOVQ $43, main..autotmp_4+112(SP); Age=43
接下来 main 函数调用 (*Dog).Say
, 注意,这个 Say 函数是结构体 **Dog 的指针接受函数,**而 Sleep是 **Dog 的结构体接受函数,**我们先看两者的汇编,之后再来比较异同。
Dog 的指针接受函数
0x0053 00083 (t3.go:17) MOVQ AX, main.d1+24(SP); SP+24 = d1
0x0058 00088 (t3.go:18) CALL main.(*Dog).Say(SB)
;以下是 main.(*Dog).Say 的代码
0x0000 00000 (t3.go:12) TEXT main.(*Dog).Say(SB), NOSPLIT|ABIInternal, $24-8
0x0000 00000 (t3.go:12) SUBQ $24, SP
0x0004 00004 (t3.go:12) MOVQ BP, 16(SP)
0x0009 00009 (t3.go:12) LEAQ 16(SP), BP
...
0x000e 00014 (t3.go:12) MOVQ AX, main.t+32(SP); 将d1 的地址放在 SP+32处
0x0013 00019 (t3.go:12) MOVUPS X15, main.~r0(SP); r0 是返回值,清零
0x0018 00024 (t3.go:13) MOVQ main.t+32(SP), CX ; CX = d1 地址
0x001d 00029 (t3.go:13) TESTB AL, (CX)
0x001f 00031 (t3.go:13) MOVQ (CX), AX ; d1.Data 的值给 AX
0x0022 00034 (t3.go:13) MOVQ 8(CX), BX ; d1.Len 的值给 BX
0x0026 00038 (t3.go:13) MOVQ AX, main.~r0(SP) ; 赋值给返回值
0x002a 00042 (t3.go:13) MOVQ BX, main.~r0+8(SP)
0x002f 00047 (t3.go:13) MOVQ 16(SP), BP
0x0034 00052 (t3.go:13) ADDQ $24, SP
0x0038 00056 (t3.go:13) RET
通过 $24-8
可知,Say 函数栈使用了 24 字节,调用者传入的参数是 8 字节,看来是一个指针。
Dog的结构体接收函数
我们先看 main 函数在调用 Sleep 函数前所做的操作
0x0067 00103 (t3.go:19) MOVQ main.d1+24(SP), CX
0x006c 00108 (t3.go:19) TESTB AL, (CX)
0x006e 00110 (t3.go:19) MOVQ (CX), AX ; 把d1 的值赋值给 AX,BX,CX
0x0071 00113 (t3.go:19) MOVQ 8(CX), BX
0x0075 00117 (t3.go:19) MOVQ 16(CX), CX
0x0079 00121 (t3.go:19) MOVQ AX, main..autotmp_5+72(SP)
0x007e 00126 (t3.go:19) MOVQ BX, main..autotmp_5+80(SP)
0x0083 00131 (t3.go:19) MOVQ CX, main..autotmp_5+88(SP)
0x0088 00136 (t3.go:19) CALL main.Dog.Sleep(SB)
0x008d 00141 (t3.go:19) MOVQ AX, main.b+40(SP)
0x0092 00146 (t3.go:19) MOVQ BX, main.b+48(SP)
对比 Say 和 Sleep 函数,我们可以发现
- 在调用 Say 函数前,main 只需要把 d1 的地址放到特定的位置即可。
- 在调用 Sleep 函数前,main 需要把 d1 的各个值都复制到寄存器和栈上。
接下来再看 Sleep 函数的汇编
0x0000 00000 (t3.go:8) TEXT main.Dog.Sleep(SB), NOSPLIT|ABIInternal, $24-24
0x0000 00000 (t3.go:8) SUBQ $24, SP
0x0004 00004 (t3.go:8) MOVQ BP, 16(SP)
0x0009 00009 (t3.go:8) LEAQ 16(SP), BP
0x000e 00014 (t3.go:8) MOVQ AX, main.t+32(SP)
0x0013 00019 (t3.go:8) MOVQ BX, main.t+40(SP)
0x0018 00024 (t3.go:8) MOVQ CX, main.t+48(SP)
0x001d 00029 (t3.go:8) MOVUPS X15, main.~r0(SP)
0x0022 00034 (t3.go:9) MOVQ main.t+32(SP), AX
0x0027 00039 (t3.go:9) MOVQ main.t+40(SP), BX
0x002c 00044 (t3.go:9) MOVQ AX, main.~r0(SP)
0x0030 00048 (t3.go:9) MOVQ BX, main.~r0+8(SP)
可以看到 $24-24
,说明 Sleep 函数自己的栈空间是 24 字节,同时调用者传入的参数也占用了 24 字节。
可以看到即使 d1
是指针,Go 在编译时也会转化为值进行调用,并且 Go 会把接收者的数据拷贝一遍,当作参数传递给被调用函数。
自动生成的函数
查看生成的汇编代码,会看到一个这样的函数 main.(*Dog).Sleep(SB)
,但是问题是我们并没有定义这个函数啊?
其实这是Go 编译器自动生成的函数,从 autogenerated 就能看出,其他正常的是显示源码文件名。
0x0000 00000 (<autogenerated>:1) TEXT main.(*Dog).Sleep(SB), DUPOK|WRAPPER|ABIInternal, $88-8
0x0000 00000 (<autogenerated>:1) CMPQ SP, 16(R14)
0x0032 00050 (<autogenerated>:1) MOVQ main..this+96(SP), DX
0x0037 00055 (<autogenerated>:1) TESTB AL, (DX)
0x0039 00057 (<autogenerated>:1) MOVQ (DX), AX
0x003c 00060 (<autogenerated>:1) MOVQ 8(DX), BX
0x0040 00064 (<autogenerated>:1) MOVQ 16(DX), CX
0x0044 00068 (<autogenerated>:1) MOVQ AX, main..autotmp_3+56(SP)
0x0049 00073 (<autogenerated>:1) MOVQ BX, main..autotmp_3+64(SP)
0x004e 00078 (<autogenerated>:1) MOVQ CX, main..autotmp_3+72(SP)
0x0053 00083 (<autogenerated>:1) PCDATA $1, $1
0x0053 00083 (<autogenerated>:1) CALL main.Dog.Sleep(SB)
0x0058 00088 (<autogenerated>:1) MOVQ AX, main..autotmp_2+40(SP)
0x005d 00093 (<autogenerated>:1) MOVQ BX, main..autotmp_2+48(SP)
0x0062 00098 (<autogenerated>:1) MOVQ AX, main.~r0+24(SP)
0x0067 00103 (<autogenerated>:1) MOVQ BX, main.~r0+32(SP)
0x006c 00108 (<autogenerated>:1) MOVQ 80(SP), BP
0x0071 00113 (<autogenerated>:1) ADDQ $88, SP
也不难看出,自动生成的 (*Dog).Sleep(SB)
也是把指针地址存的值拷贝出来,然后调用 Dog.Sleep(SB),整体逻辑与 main 函数中的一致。
那么,为什么没有自动生成 (*Dog).Say()
对应的 (Dog).Say()
呢? 参考资料 3 中给出的解释是这样的
答案是 不行。原因也很简单,值接收者是接收拷贝数据的,并不能找到原始数据的具体地址,因此也无法生成指针接受者。
我的理解是,如果已经知道了变量 d 的地址,那么也可以获取它的数据的拷贝,但是反过来,已知 d 的值,无法反推出 d 的地址。