为什么写这个系列

这个系列的文章是希望能帮助读者从Go汇编的层面更好的理解一个Golang程序。笔者刚工作的时候用的是C语言,深刻的感到理解一个程序的汇编代码对于编程的是非常有帮助的(这个也是读了深入理解计算机系统之后才意识到这个问题)。所以,笔者觉得,要想更好的使用Golang,也应该理解其汇编层面的东西(这里说的汇编是指Golang工具链使用的汇编格式)。

环境

Golang自带的工具就能够帮助我们理解其汇编代码了,我们只需要建立一个简单的编程环境即可。 Golang上个月刚刚发布了1.7版本,所以本系列也会使用go1.7。主要的环境设置如下:

➜ ~  $ go version
go version go1.7 linux/amd64
➜ ~  $ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/diabloneo/go"
GORACE=""
GOROOT="/opt/go"
GOTOOLDIR="/opt/go/pkg/tool/linux_amd64"

然后,我们建议一个本地的代码目录用来存放实验代码:

➜ ~  $ cd go/src
➜ ~/go/src  $ mkdir -p goexamples/hello
➜ ~/go/src  $ cd goexamples/hello
➜ ~/go/src/goexamples/hello  $

后面的实验代码都会存放在 ${GOPATH}/goexamples/src这个目录下。

编译和反汇编

编译Golang程序,一般是用 go build工具,不过为了查看汇编代码,我们需要使用 go tool compile工具。其实 go build也是使用了 go tool compilego tool link工具,一个负责编译,一个负责链接。

先写个 hello, world 程序 goexamples/hello/hello.go ,在程序里定义一个看起来没用的 foo()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
	"fmt"
)

func foo() int {
	var a int
	return a
}

func main() {
	fmt.Printf("hello, world\n")
}

接下来,我们可以执行编译该文件来得到该文件的汇编代码,执行命令go tool compile -S hello.go

➜ ~/go/src/goexamples/hello  $ go tool compile -S hello.go
"".foo t=1 size=10 args=0x8 locals=0x0
        0x0000 00000 (hello.go:7)       TEXT    "".foo(SB), $0-8
        0x0000 00000 (hello.go:7)       NOP
        0x0000 00000 (hello.go:7)       NOP
        0x0000 00000 (hello.go:7)       FUNCDATA        $0, gclocals·5184031d3a32a42d85027f073f873668(SB)
        0x0000 00000 (hello.go:7)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (hello.go:9)       MOVQ    $0, "".~r0+8(FP)
        0x0009 00009 (hello.go:9)       RET
        0x0000 48 c7 44 24 08 00 00 00 00 c3                    H.D$......
"".main t=1 size=98 args=0x0 locals=0x48
        0x0000 00000 (hello.go:12)      TEXT    "".main(SB), $72-0
        0x0000 00000 (hello.go:12)      MOVQ    (TLS), CX
        0x0009 00009 (hello.go:12)      CMPQ    SP, 16(CX)
        0x000d 00013 (hello.go:12)      JLS     91
        0x000f 00015 (hello.go:12)      SUBQ    $72, SP
        0x0013 00019 (hello.go:12)      MOVQ    BP, 64(SP)
        0x0018 00024 (hello.go:12)      LEAQ    64(SP), BP
        0x001d 00029 (hello.go:12)      FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (hello.go:12)      FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (hello.go:13)      LEAQ    go.string."hello, world\n"(SB), AX
        0x0024 00036 (hello.go:13)      MOVQ    AX, (SP)
        0x0028 00040 (hello.go:13)      MOVQ    $13, 8(SP)
        0x0031 00049 (hello.go:13)      MOVQ    $0, 16(SP)
        0x003a 00058 (hello.go:13)      MOVQ    $0, 24(SP)
        0x0043 00067 (hello.go:13)      MOVQ    $0, 32(SP)
        0x004c 00076 (hello.go:13)      PCDATA  $0, $0
        0x004c 00076 (hello.go:13)      CALL    fmt.Printf(SB)
        0x0051 00081 (hello.go:14)      MOVQ    64(SP), BP
        0x0056 00086 (hello.go:14)      ADDQ    $72, SP
        0x005a 00090 (hello.go:14)      RET
        0x005b 00091 (hello.go:14)      NOP
        0x005b 00091 (hello.go:12)      CALL    runtime.morestack_noctxt(SB)
        0x0060 00096 (hello.go:12)      JMP     0
        0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 4c 48  dH..%....H;a.vLH
        0x0010 83 ec 48 48 89 6c 24 40 48 8d 6c 24 40 48 8d 05  ..HH.l$@H.l$@H..
        0x0020 00 00 00 00 48 89 04 24 48 c7 44 24 08 0d 00 00  ....H..$H.D$....
        0x0030 00 48 c7 44 24 10 00 00 00 00 48 c7 44 24 18 00  .H.D$.....H.D$..
        0x0040 00 00 00 48 c7 44 24 20 00 00 00 00 e8 00 00 00  ...H.D$ ........
        0x0050 00 48 8b 6c 24 40 48 83 c4 48 c3 e8 00 00 00 00  .H.l$@H..H......
        0x0060 eb 9e                                            ..
        rel 5+4 t=15 TLS+0
        rel 32+4 t=14 go.string."hello, world\n"+0
        rel 77+4 t=7 fmt.Printf+0
        rel 92+4 t=7 runtime.morestack_noctxt+0
"".init t=1 size=61 args=0x0 locals=0x0
        0x0000 00000 (hello.go:15)      TEXT    "".init(SB), $0-0
        0x0000 00000 (hello.go:15)      MOVQ    (TLS), CX
        0x0009 00009 (hello.go:15)      CMPQ    SP, 16(CX)
        0x000d 00013 (hello.go:15)      JLS     54
        0x000f 00015 (hello.go:15)      NOP
        0x000f 00015 (hello.go:15)      NOP
        0x000f 00015 (hello.go:15)      FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000f 00015 (hello.go:15)      FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000f 00015 (hello.go:15)      MOVBLZX "".initdone·(SB), AX
        0x0016 00022 (hello.go:15)      CMPB    AL, $1
        0x0018 00024 (hello.go:15)      JLS     $0, 27
        0x001a 00026 (hello.go:15)      RET
        0x001b 00027 (hello.go:15)      JNE     $0, 34
        0x001d 00029 (hello.go:15)      PCDATA  $0, $0
        0x001d 00029 (hello.go:15)      CALL    runtime.throwinit(SB)
        0x0022 00034 (hello.go:15)      MOVB    $1, "".initdone·(SB)
        0x0029 00041 (hello.go:15)      PCDATA  $0, $0
        0x0029 00041 (hello.go:15)      CALL    fmt.init(SB)
        0x002e 00046 (hello.go:15)      MOVB    $2, "".initdone·(SB)
        0x0035 00053 (hello.go:15)      RET
        0x0036 00054 (hello.go:15)      NOP
        0x0036 00054 (hello.go:15)      CALL    runtime.morestack_noctxt(SB)
        0x003b 00059 (hello.go:15)      JMP     0
        0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 27 0f  dH..%....H;a.v'.
        0x0010 b6 05 00 00 00 00 3c 01 76 01 c3 75 05 e8 00 00  ......<.v..u....
        0x0020 00 00 c6 05 00 00 00 00 01 e8 00 00 00 00 c6 05  ................
        0x0030 00 00 00 00 02 c3 e8 00 00 00 00 eb c3           .............
        rel 5+4 t=15 TLS+0
        rel 18+4 t=14 "".initdone·+0
        rel 30+4 t=7 runtime.throwinit+0
        rel 36+4 t=14 "".initdone·+-1
        rel 42+4 t=7 fmt.init+0
        rel 48+4 t=14 "".initdone·+-1
        rel 55+4 t=7 runtime.morestack_noctxt+0
gclocals·33cdeccccebe80329f1fdbee7f5874cb t=9 dupok size=8
        0x0000 01 00 00 00 00 00 00 00                          ........
gclocals·5184031d3a32a42d85027f073f873668 t=9 dupok size=12
        0x0000 01 00 00 00 01 00 00 00 00 00 00 00              ............
go.string.hdr."hello, world\n" t=9 dupok size=16
        0x0000 00 00 00 00 00 00 00 00 0d 00 00 00 00 00 00 00  ................
        rel 0+8 t=1 go.string."hello, world\n"+0
go.string."hello, world\n" t=9 dupok size=13
        0x0000 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a           hello, world.
"".initdone· t=34 size=1
"".foo·f t=9 dupok size=8
        0x0000 00 00 00 00 00 00 00 00                          ........
        rel 0+8 t=1 "".foo+0
"".main·f t=9 dupok size=8
        0x0000 00 00 00 00 00 00 00 00                          ........
        rel 0+8 t=1 "".main+0
"".init·f t=9 dupok size=8
        0x0000 00 00 00 00 00 00 00 00                          ........
        rel 0+8 t=1 "".init+0
type..importpath.fmt. t=9 dupok size=6
        0x0000 00 00 03 66 6d 74                                ...fmt

现在我们得到了这个文件的汇编代码,可以看到这个代码里不仅有汇编指令,还有很多Golang实现相关的东西。现在,作为起步,我们先来看一些基本的内容:

"".foo t=1 size=10 args=0x8 locals=0x0
...
"".main t=1 size=98 args=0x0 locals=0x48
...

上面两行就是我们定义的两个函数:main()foo(),跟在这两行后面的是两个函数的内容。为了方便阐述,这里会把要研究的代码都写在foo()函数内。

接下来,我们需要 关系编译优化。Golang的编译器在编译的时候会执行一些默认的优化,这会提升性能和节约空间,但是会使得汇编代码和Golang代码对应不起来,所以我们要先关闭这个优化,然后才能更好的研究汇编代码。关闭优化需要传递 -N选项给编译器:

➜ ~/go/src/goexamples/hello  $ go tool compile -N -S hello.go

这样得到的汇编输出会和前面略有不同,可以通过对比foo()函数的汇编代码来看到这些不同点:

有优化

"".foo t=1 size=10 args=0x8 locals=0x0
        0x0000 00000 (hello.go:7)       TEXT    "".foo(SB), $0-8
        0x0000 00000 (hello.go:7)       NOP
        0x0000 00000 (hello.go:7)       NOP
        0x0000 00000 (hello.go:7)       FUNCDATA        $0, gclocals·5184031d3a32a42d85027f073f873668(SB)
        0x0000 00000 (hello.go:7)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (hello.go:9)       MOVQ    $0, "".~r0+8(FP)
        0x0009 00009 (hello.go:9)       RET
        0x0000 48 c7 44 24 08 00 00 00 00 c3                    H.D$......

无优化

"".foo t=1 size=50 args=0x8 locals=0x10
        0x0000 00000 (hello.go:7)       TEXT    "".foo(SB), $16-8
        0x0000 00000 (hello.go:7)       SUBQ    $16, SP
        0x0004 00004 (hello.go:7)       MOVQ    BP, 8(SP)
        0x0009 00009 (hello.go:7)       LEAQ    8(SP), BP
        0x000e 00014 (hello.go:7)       FUNCDATA        $0, gclocals·5184031d3a32a42d85027f073f873668(SB)
        0x000e 00014 (hello.go:7)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (hello.go:7)       MOVQ    $0, "".~r0+24(FP)
        0x0017 00023 (hello.go:8)       MOVQ    $0, "".a(SP)
        0x001f 00031 (hello.go:9)       MOVQ    $0, "".~r0+24(FP)
        0x0028 00040 (hello.go:9)       MOVQ    8(SP), BP
        0x002d 00045 (hello.go:9)       ADDQ    $16, SP
        0x0031 00049 (hello.go:9)       RET
        0x0000 48 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 48 c7  H...H.l$.H.l$.H.
        0x0010 44 24 18 00 00 00 00 48 c7 04 24 00 00 00 00 48  D$.....H..$....H
        0x0020 c7 44 24 18 00 00 00 00 48 8b 6c 24 08 48 83 c4  .D$.....H.l$.H..
        0x0030 10 c3                                            ..

可以看出,无优化的代码更长,包含了函数完整的栈帧操作。后续我们会一步一步的研究Golang的代码和无优化的汇编代码之间的关系,以帮助读者更好的理解Golang。

Golang汇编基础

Golang的汇编器采用的是Plan 9的汇编语法,官方有一篇关于Golang汇编器的快速入门:A Quick Guide to Go’s Assembler,其中介绍了一些简单的语法。在开始研究Golang的汇编代码之前,我们也需要先了解一下基本的语法。

伪寄存器

Golang汇编中有4个伪寄存器,这些伪寄存器是由Golang工具链维护的。这4个伪寄存器是:

  • FP: Frame pointer:栈帧寄存器,指向参数和本地变量。
  • PC:Program counter:程序计数器,用来进行跳转处理。
  • SB:Static base pointer:全局符号表。
  • SP:Stack pointer:栈顶指针。

SB寄存器的常用语法有以下几种:

  • foo(SB)是全局变量foo的地址
  • foo<>(SB)是只在当前源文件可见的变量foo的地址
  • foo+4(SB)是全局变量foo的地址偏移4字节的地址

FP寄存器是用来访问函数参数的,理论上来说,0(FP)是第一个参数,8(FP)是第二个参数(64位系统)。但是,实际使用时,需要把参数名加进去:

  • first_arg+0(FP)
  • second_arg+8(FP)

SP寄存器是一个虚拟的栈指针,用于指向当前栈帧的栈顶,用于访问局部变量和函数参数(除了 FP,这个也可以访问到函数参数)。SP是指向栈顶的,所以访问数据通常是做减法:

  • x-8(SP)
  • y-4(SP)
  • -8(SP)

注意,这里的两个不同语法。在有 SP寄存器的机器上,带有一个名称前缀的语法表示访问Golang虚拟的SP寄存器,而不带名称前缀的则是访问硬件的SP寄存器。

寻址模式

说到汇编,就不得不提到寻址模式。Plan 9汇编的寻址模式挺多的,具体的可以看A Manual for Plan 9 assembler。这里我们列举一些常用的:

  • $con 常量
  • $fcon 浮点数常量
  • name+o(SB) 外部符号
  • name<>+o(SB) 本地符号
  • name+o(FP) 函数参数
  • $name+o(SB) 外部符号地址
  • $name<>+o(SB) 本地符号地址

o表示整数偏移量。

指令

下面来介绍一下上文代码中的foo()函数的汇编代码中用到的汇编指令:

"".foo t=1 size=50 args=0x8 locals=0x10
        0x0000 00000 (hello.go:7)       TEXT    "".foo(SB), $16-8
        0x0000 00000 (hello.go:7)       SUBQ    $16, SP
        0x0004 00004 (hello.go:7)       MOVQ    BP, 8(SP)
        0x0009 00009 (hello.go:7)       LEAQ    8(SP), BP
        0x000e 00014 (hello.go:7)       FUNCDATA        $0, gclocals·5184031d3a32a42d85027f073f873668(SB)
        0x000e 00014 (hello.go:7)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (hello.go:7)       MOVQ    $0, "".~r0+24(FP)
        0x0017 00023 (hello.go:8)       MOVQ    $0, "".a(SP)
        0x001f 00031 (hello.go:9)       MOVQ    $0, "".~r0+24(FP)
        0x0028 00040 (hello.go:9)       MOVQ    8(SP), BP
        0x002d 00045 (hello.go:9)       ADDQ    $16, SP
        0x0031 00049 (hello.go:9)       RET
TEXT "".foo(SB), $16-8

TEXT是一个伪指令,表示定义一个入口点,也就是定义一个函数。它可以带三个参数:

  1. 第一个参数表示函数名,就是这里的"".foo(SB)
  2. 第二个参数是flag,这里没有体现出来,以后的文章再说。
  3. 第三个参数是frame size,表示当前函数的栈帧大小以及参数大小,形式如同$framesize-argsize,减号前面是栈帧大小,减号后面是参数大小(包括返回值)。比如$16-8表示该函数栈帧大小为16,参数大小为8。
SUBQ $16, SP
MOVQ BP, 8(SP)
LEAQ 8(SP), BP

这是一段入栈的代码。在这段代码执行前,SP指针指向调用函数,这里分配栈帧,并且保存函数返回地址,也就是 BP寄存器的内容到栈的开始部分,然后设置新的 BP寄存器的值。随后就可以开始执行这个函数了。

BP寄存器是 base pointer ,一般用于指向当前栈帧的某个位置,和 SP 寄存器配合完成函数调用的操作。

这里的三个指令都是常用指令,格式都是源操作数在前,目标操作数在后:

  1. SUBQ
  2. MOVQ 拷贝
  3. LEAQ 载入有效地址。LEA的全称是 Load Effective Address,LEA指令计算源操作数所表示的地址的值,然后保存在目标操作数中。有时候LEA指令也会被用来进行简单的无符号计算(支持加、减、乘)。关于LEA指令的更多资料,可以看这里X86 Assembly/Data Transfer
FUNCDATA        $0, gclocals·5184031d3a32a42d85027f073f873668(SB)
FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)

这两行是用于保存GC所需要的数据。相关指定以后再介绍。

MOVQ $0, "".~r0+24(FP)
MOVQ $0, "".a(SP)
MOVQ $0, "".~r0+24(FP)

这里没有新的指令,赋值操作实现了返回a变量。

MOVQ 8(SP), BP
ADDQ $16, SP

这些是出栈的代码,通过把 SP 寄存器和 BP 寄存器设置为调用函数的值来完成返回。


知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。