本文主要收集一些例子,以后阅读 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)      

其中

  1. MOVUPS 是 Intel 平台的 SIMD 指令,通过 X15 代表的固定的零寄存器对起始地址为SP + 40 的连续 128 bit (16个字节)进行清零。如果是作用在 slice 结构体上,则是 len 和 cap 为0。
  2. LEAQ 取 SP+40 内存单元的地址,存入 AX 寄存器。
  3. 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,得到的值存在另一个寄存器中,供 JGTJLT 指令使用
  • 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 的地址。

参考资料

  1. https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice/
  2. https://lrita.github.io/2017/12/12/golang-asm/
  3. https://hadyang.com/2021/04/golang-struct-interface/