目录

plan9

查看汇编代码的几种方法

  • go tool compile -N -l -S ./go_file.go (优先)
  • go build -gcflags=-S go_file.go
  • go tool objdump go_file.o

参考文档

https://github.com/teh-cmc/go-internals/blob/master/chapter1_assembly_primer

https://golang.org/doc/asm

plan9

伪汇编

Go 编译器生成会输出一种抽象可移植的汇编代码,这种汇编并不对应某种真实的硬件架构。之后 Go 的汇编器使用这种伪汇编,为目标硬件生成具体的机器指令。

plan9 伪寄存器

  • SB Static base pointer: global symbols. 全局静态基指针,程序地址空间的开始地址。所有用户定义的符号都可以作为偏移量写入伪寄存器 FP(参数和局部变量)和 SB(全局变量)。SB 伪寄存器可以被认为是内存的起始位置 0x0,例如 runtime.newobject(SB) 就是函数 runtime.newobject 位于内存中的地址。

  • SP Stack pointer: the highest address within the local stack frame. 栈顶,指向当前栈帧的开始位置。使用形如 symbol+offset(SP) 的方式,引用函数的局部变量,例如 a+8(SP) 指相对于 SP,offset 为 +8 的地址,假如 SP 指向 0x000f0, 那么 a+8(SP) 指向 0x000f8。a 是 symbol,变量名称,用于提升代码可读性。

  • FP Frame pointer: arguments and locals. 类似 SP,实际使用非常少。

  • PC Program counter: jumps and branches. 存放 CPU 下一个执行指令的位置地址,PC 是一个抽象的概念,在 x86 上,通过 CS 段寄存器和 IP 寄存器共同计算出指令的地址,也就是PC的值。具体使用示例 JMP 2(PC) 以当前指令为基础,向后跳转 2 行

  • TLS thread local storage 存放了当前正在执行的 g 的结构体。例如 0(TLS) 表示 g.stack.lo,8(TLS) 表示 g.stack.hi

plan9 指令

plan9 指令格式 指令 源操作数 目标操作数

  • MOV 数据搬运,赋值

  • LEA 取址 &

  • TEST

  • CMP 比较

  • JLS ( jump less )小于时跳转

  • JMP( jump )无条件跳转

  • JEQ( jump equal )等于时跳转

  • SUB 相减,结果保存到目标操作数

  • ADD 相加,结果保存到目标操作数

  • RET 返回

  • NOP 空指令

  • CALL 调用函数

  • SHL( shift logical left ) 逻辑左移指令 <<

  • SHR( shift logical right ) 逻辑右移指令 >>,右移时不保留操作数的符号,用 0 代替

  • SAL( shift arithmetic left )算术左移指令

  • SAR( shift arithmetic right )算术右移指令,右移时保留操作数的符号

  • XADD:交换并相加

  • NEG:求补指令,就是取相反数

  • XORPS:源操作数(第二个操作数)与目标操作数(第一个操作数)进行异或。结果保存到目标操作数

  • MOVUPS:与 MOV 一样,操作对象的类型不一样,这里是包含四个压缩单精度浮点值的双四字

  • XCHG: 交换两个操作数内容,自带 LOCK 总线锁属性

示例

go 代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Data struct {
	Int    int
	String string
	Float  float64
}

func addInt(a, b int) *Data {
	return &Data{Int: a + b}
}

func main() {
	data := addInt(1, 2)
	fmt.Println(data)
}
对应汇编

不同版本对应的汇编可能不同,尤其是 golang 1.17 (含)之后使用寄存器传递参数

1
2
$ go version
go version go1.16.7 darwin/amd64
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"".addInt STEXT size=177 args=0x18 locals=0x20 funcid=0x0
	;; 0x0000 00000 均为当前指令相对于当前函数的偏移量,第一个是 16 进制,第二个是 10 进制
	;; TEXT 函数申明
    ;; TEXT				位于内存的 .text 段
    ;; "".addInt(SB)	""为包名,链接时会填充;函数名 addInt;SB (static base)虚拟寄存器,基于静态地址,可认为是程序内存起始位置,所有用户定义的符号(如 main.addInt) 都作为偏移量写入伪寄存器 FP 和 SB
    ;; ??? addInt 之所以使用 SB 是否是相对于 so 的函数而言,因为对于动态链接库的函数,函数的地址 = so 文件在进程内存空间的映射地址 + 函数偏移量,而静态链接的函数在编译器就可以确定地址
    ;; ABIInternal		编译器 flag,含义 application binary interface
    ;; 其他常见的编译器 flag 有 NOSPLIT
    ;; NOSPLIT			使编译器不要进行内联优化
    ;; $32-24			函数栈帧大小为 32 字节,其中参数+返回值占用 24 字节
	0x0000 00000 (./compile.go:11)	TEXT	"".addInt(SB), ABIInternal, $32-24
	;; MOV 数据搬运
	;; MOVB 	// 1 byte   B(Byte)
	;; MOVW 	// 2 bytes  W(Word)
	;; MOVL 	// 4 bytes  L(Long)
	;; MOVQ 	// 8 bytes  Q(Quadword)
	;; MOVSD    // 
	;; MOVUPS   // 16 bytes 4个不对准的单精度值
	;; MOVAPS   // 16 bytes 4个对准的单精度值
	;; TLS 是一个由 runtime 维护的虚拟寄存器,保存了指向当前 g 的指针
	;; g 前 24 位数据结构如下:
	;; 0(TLS) uintptr g.stack.lo;
	;; 8(TLS) uintptr g.stack.hi; stack describes the actual stack memory: [stack.lo,stack.hi)
    ;; 16(TLS) uintptr g.stackguard0; stackguard0 is the stack pointer compared in the Go stack growth prologue.
	0x0000 00000 (./compile.go:11)	MOVQ	(TLS), CX
	;; CMP 比较大小
	;; 与 MOV 类似,CMPL,CMPQ ... 中的 L,Q 为 size 大小
	;; CMPQ	SP,16(CX) 表示将以 8bytes 的大小比较 SP,*(CX + 16) 即 g.stackguard0 的大小
	0x0009 00009 (./compile.go:11)	CMPQ	SP, 16(CX)
	;; PCDATA GC 相关
	;; PC表格: 针对每个函数和函数的每个指令生成一个地址表格,记录代码所在的文件路径、行号和函数的信息
	;; PCDATA tableid,tableoffset 第一个参数为表格的类型,第二个是表格的地址
	0x000d 00013 (./compile.go:11)	PCDATA	$0, $-2
	;; JLS jump less 小于时跳转
	;; CMPQ	SP,16(CX); JLS	167 表示若 SP < *(CX + 16) 跳转到 00167
	;; 当 g 的 satck 不足时, 跳转到 00167 执行扩容函数
	0x000d 00013 (./compile.go:11)	JLS	167
	0x0013 00019 (./compile.go:11)	PCDATA	$0, $-1
	;; 栈调整
	;; SUBQ	$32,SP 对 SP 做减法,为函数分配函数栈帧
	0x0013 00019 (./compile.go:11)	SUBQ	$32, SP
	;; MOV 数据搬运
	;; 使用 MOV,CMP 的时候会有看到带括号和不带括号的区别
	;; MOVQ 16(AX),BX	// => BX = *(AX + 16)
	;; MOVQ AX,BX		// => BX = AX 将AX中存储的内容赋值给BX,注意区别
	;; CMPB	(AX),$48	// 以 byte 的 size 比较 *(AX) 48 
	;; MOVQ	BP,24(SP) 表示将 BP 赋值给 *(SP + 24),作用为保存调用者的 BP
	0x0017 00023 (./compile.go:11)	MOVQ	BP, 24(SP)
	;; LEA Load Effective Address 地址运算,类似 &
	;; LEAQ	24(SP),BP 表示将 (SP + 24) 赋值给 BP,作用为设置新的 BP
	0x001c 00028 (./compile.go:11)	LEAQ	24(SP), BP
	;; FUNCDATA GC相关
	;; FUNC 表格用于记录函数的参数、局部变量的指针信息
	0x0021 00033 (./compile.go:11)	FUNCDATA	$0, gclocals·54241e171da8af6ae173d69da0236748(SB)
	0x0021 00033 (./compile.go:11)	FUNCDATA	$1, gclocals·2a5305abe05176240e61b8620e19a815(SB)
	;; MOVQ	$0,"".~r2+56(SP)表示将 0 赋值给 *(56 + SP)
	;; .r2 是分配给引用地址的任意别名;尽管没有任何语义上的含义,但在使用虚拟寄存器和相对地址时,这种别名是需要强制使用的。
	;; "".~r2+56(SP) 格式 $pkg_name.~$var_name+size(reg)
	0x0021 00033 (./compile.go:11)	MOVQ	$0, "".~r2+56(SP)
	0x002a 00042 (./compile.go:12)	LEAQ	type."".Data(SB), AX
	0x0031 00049 (./compile.go:12)	MOVQ	AX, (SP)
	0x0035 00053 (./compile.go:12)	PCDATA	$1, $0
	;; CALL 调用函数
	;; 调用函数 runtime.newobject,依据上下文(SP)为参数,8(SP)为返回值
	;; 内存逃逸:runtime.newobject 会导致对象分配到堆上
	0x0035 00053 (./compile.go:12)	CALL	runtime.newobject(SB)
	0x003a 00058 (./compile.go:12)	MOVQ	8(SP), AX
	0x003f 00063 (./compile.go:12)	MOVQ	AX, ""..autotmp_3+16(SP)
	0x0044 00068 (./compile.go:12)	MOVQ	$0, (AX)
	0x004b 00075 (./compile.go:12)	MOVQ	$0, 16(AX)
	;; XORPS 源操作数(第二个操作数)与目标操作数(第一个操作数)进行异或。结果保存到目标操作数 
	;; XORPS	X0,X0 初始化寄存器 X0
	0x0053 00083 (./compile.go:12)	XORPS	X0, X0
	0x0056 00086 (./compile.go:12)	MOVSD	X0, 24(AX)
	0x005b 00091 (./compile.go:12)	LEAQ	8(AX), DI
	0x005f 00095 (./compile.go:12)	PCDATA	$0, $-2
	;; GC相关,是否需要写屏障
	0x005f 00095 (./compile.go:12)	CMPL	runtime.writeBarrier(SB), $0
	;; JEQ jump equal 等于时跳转
	0x0066 00102 (./compile.go:12)	JEQ	106
	;; JMP 无条件跳转
	;; JMP addr   // 跳转到地址,地址可为代码中的地址
	;; JMP label  // 跳转到标签,可以跳转到同一函数内的标签位置
	;; JMP 2(PC)  // 以当前指令为基础,向后跳转 2 行
	0x0068 00104 (./compile.go:12)	JMP	156
	0x006a 00106 (./compile.go:12)	MOVQ	$0, 8(AX)
	0x0072 00114 (./compile.go:12)	JMP	116
	0x0074 00116 (./compile.go:12)	PCDATA	$0, $-1
	0x0074 00116 (./compile.go:12)	MOVQ	""..autotmp_3+16(SP), AX
	0x0079 00121 (./compile.go:12)	TESTB	AL, (AX)
	0x007b 00123 (./compile.go:12)	MOVQ	"".a+40(SP), CX
	0x0080 00128 (./compile.go:12)	ADDQ	"".b+48(SP), CX
	0x0085 00133 (./compile.go:12)	MOVQ	CX, (AX)
	0x0088 00136 (./compile.go:12)	MOVQ	""..autotmp_3+16(SP), AX
	0x008d 00141 (./compile.go:12)	MOVQ	AX, "".~r2+56(SP)
	;; 栈调整
	;; ADDQ	$32,SP  对 SP 做加法,清除函数栈帧
	0x0092 00146 (./compile.go:12)	MOVQ	24(SP), BP
	0x0097 00151 (./compile.go:12)	ADDQ	$32, SP
	;; RET 返回
	0x009b 00155 (./compile.go:12)	RET
	0x009c 00156 (./compile.go:12)	PCDATA	$0, $-2
	0x009c 00156 (./compile.go:12)	XORL	AX, AX
	0x009e 00158 (./compile.go:12)	NOP
	0x00a0 00160 (./compile.go:12)	CALL	runtime.gcWriteBarrier(SB)
	0x00a5 00165 (./compile.go:12)	JMP	116
	;; NOP 空操作,占用一个时钟周期,用于延时或者程序指令的对齐
	0x00a7 00167 (./compile.go:12)	NOP
	0x00a7 00167 (./compile.go:11)	PCDATA	$1, $-1
	0x00a7 00167 (./compile.go:11)	PCDATA	$0, $-2
	;; CALL runtime.morestack satck 扩容
	0x00a7 00167 (./compile.go:11)	CALL	runtime.morestack_noctxt(SB)
	0x00ac 00172 (./compile.go:11)	PCDATA	$0, $-1
	0x00ac 00172 (./compile.go:11)	JMP	0
	;; ... 省略部分内容                                              .
	rel 5+4 t=17 TLS+0
	rel 45+4 t=16 type."".Data+0
	rel 54+4 t=8 runtime.newobject+0
	rel 97+4 t=16 runtime.writeBarrier+-1
	rel 161+4 t=8 runtime.gcWriteBarrier+0
	rel 168+4 t=8 runtime.morestack_noctxt+0
"".main STEXT size=186 args=0x0 locals=0x78 funcid=0x0
	0x0000 00000 (./compile.go:15)	TEXT	"".main(SB), ABIInternal, $120-0
	0x0000 00000 (./compile.go:15)	MOVQ	(TLS), CX
	0x0009 00009 (./compile.go:15)	CMPQ	SP, 16(CX)
	0x000d 00013 (./compile.go:15)	PCDATA	$0, $-2
	0x000d 00013 (./compile.go:15)	JLS	176
	0x0013 00019 (./compile.go:15)	PCDATA	$0, $-1
	0x0013 00019 (./compile.go:15)	SUBQ	$120, SP
	0x0017 00023 (./compile.go:15)	MOVQ	BP, 112(SP)
	0x001c 00028 (./compile.go:15)	LEAQ	112(SP), BP
	0x0021 00033 (./compile.go:15)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0021 00033 (./compile.go:15)	FUNCDATA	$1, gclocals·1d0ed49f611d7e40a62328b5976a2ede(SB)
	0x0021 00033 (./compile.go:15)	FUNCDATA	$2, "".main.stkobj(SB)
	;; CALL 参数:(SP),8(SP)
	0x0021 00033 (./compile.go:16)	MOVQ	$1, (SP)
	0x0029 00041 (./compile.go:16)	MOVQ	$2, 8(SP)
	0x0032 00050 (./compile.go:16)	PCDATA	$1, $0
	;; CALL
	0x0032 00050 (./compile.go:16)	CALL	"".addInt(SB)
	;; CALL 返回值 16(SP)
	0x0037 00055 (./compile.go:16)	MOVQ	16(SP), AX
	0x003c 00060 (./compile.go:16)	MOVQ	AX, "".data+48(SP)
	0x0041 00065 (./compile.go:17)	MOVQ	AX, ""..autotmp_1+64(SP)
	0x0046 00070 (./compile.go:17)	XORPS	X0, X0
	0x0049 00073 (./compile.go:17)	MOVUPS	X0, ""..autotmp_2+72(SP)
	0x004e 00078 (./compile.go:17)	LEAQ	""..autotmp_2+72(SP), AX
	0x0053 00083 (./compile.go:17)	MOVQ	AX, ""..autotmp_4+56(SP)
	0x0058 00088 (./compile.go:17)	TESTB	AL, (AX)
	0x005a 00090 (./compile.go:17)	MOVQ	""..autotmp_1+64(SP), CX
	0x005f 00095 (./compile.go:17)	LEAQ	type.*"".Data(SB), DX
	0x0066 00102 (./compile.go:17)	MOVQ	DX, ""..autotmp_2+72(SP)
	0x006b 00107 (./compile.go:17)	MOVQ	CX, ""..autotmp_2+80(SP)
	0x0070 00112 (./compile.go:17)	TESTB	AL, (AX)
	0x0072 00114 (./compile.go:17)	JMP	116
	0x0074 00116 (./compile.go:17)	MOVQ	AX, ""..autotmp_3+88(SP)
	0x0079 00121 (./compile.go:17)	MOVQ	$1, ""..autotmp_3+96(SP)
	0x0082 00130 (./compile.go:17)	MOVQ	$1, ""..autotmp_3+104(SP)
	0x008b 00139 (./compile.go:17)	MOVQ	AX, (SP)
	0x008f 00143 (./compile.go:17)	MOVQ	$1, 8(SP)
	0x0098 00152 (./compile.go:17)	MOVQ	$1, 16(SP)
	0x00a1 00161 (./compile.go:17)	CALL	fmt.Println(SB)
	0x00a6 00166 (./compile.go:64)	MOVQ	112(SP), BP
	0x00ab 00171 (./compile.go:64)	ADDQ	$120, SP
	0x00af 00175 (./compile.go:64)	RET
	0x00b0 00176 (./compile.go:64)	NOP
	0x00b0 00176 (./compile.go:15)	PCDATA	$1, $-1
	0x00b0 00176 (./compile.go:15)	PCDATA	$0, $-2
	0x00b0 00176 (./compile.go:15)	CALL	runtime.morestack_noctxt(SB)
	0x00b5 00181 (./compile.go:15)	PCDATA	$0, $-1
	0x00b5 00181 (./compile.go:15)	JMP	0
	;; ... 省略部分内容
type..importpath.fmt. SRODATA dupok size=6
	0x0000 00 00 03 66 6d 74                                ...fmt
gclocals·54241e171da8af6ae173d69da0236748 SRODATA dupok size=9
	0x0000 01 00 00 00 03 00 00 00 00                       .........
gclocals·2a5305abe05176240e61b8620e19a815 SRODATA dupok size=9
	0x0000 01 00 00 00 01 00 00 00 00                       .........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
	0x0000 01 00 00 00 00 00 00 00                          ........
gclocals·1d0ed49f611d7e40a62328b5976a2ede SRODATA dupok size=9
	0x0000 01 00 00 00 08 00 00 00 00                       .........
"".main.stkobj SRODATA static size=24
	0x0000 01 00 00 00 00 00 00 00 d8 ff ff ff ff ff ff ff  ................
	0x0010 00 00 00 00 00 00 00 00                          ........
	rel 16+8 t=1 type.[1]interface {}+0
gclocals·dc9b0298814590ca3ffc3a889546fc8b SRODATA dupok size=10
	0x0000 02 00 00 00 02 00 00 00 03 00                    ..........
gclocals·2589ca35330fc0fce83503f4569854a0 SRODATA dupok size=10
	0x0000 02 00 00 00 02 00 00 00 00 00                    ..........
CALL addInt() 时的栈布局

./stack.png

为什么 addInt 的栈帧部分和参数部分出现空隙 +32~40(SP)

原因未知,但确实存在,可能因为调用惯例。通过 dlv 追踪 SP BP,过程如下:

  1. 进入 main.main 函数
1
main.main

rsp 0x000000c00009df80

rbp 0x000000c00009dfd0

  1. 为 main.main 函数分配栈帧
1
2
3
add rsp, -0x80
mov qword ptr [rsp+0x78], rbp
lea rbp, ptr [rsp+0x78]

rsp 0x000000c00009df00

rbp 0x000000c00009df78

  1. main 调用 addInt 函数,发现在进入 main.addInt 时 rsp - 8
1
call $main.addInt

rsp 0x000000c00009def8

rbp 0x000000c00009df78

  1. 为 main.addInt 函数分配栈帧
1
2
3
sub rsp, 0x20
mov qword ptr [rsp+0x18], rbp
lea rbp, ptr [rsp+0x18]

rsp 0x000000c00009ded8

rbp 0x000000c00009def0

  1. main.addInt 函数返回,并回收栈帧,回到 main.main 栈帧,rsp rbp 与过程 2相同
1
return

rsp 0x000000c00009df00

rbp 0x000000c00009df78

Q&A

使用 go tool compile -N -l -S *.go 报错 can't find import

原因: link 存在问题

解决办法:

第一种办法:更换命令,使用 go build -gcflag=-S 1>tmp.asm 2>&1 查看 tmp.asm,神奇的操作, asm 在 stderr 中

第二种办法:

go build -x -n -v *.go 2>&1 | sed -n "/^# import config/,/EOF$/p" |grep -v EOF > importcfg
go tool compile -importcfg importcfg -N -l -S *.go