托管语言的运行时结构与 Go 二进制文件 AMD64 汇编分析
By Lee Yunjin views ...

什么是托管语言?
托管语言是指这样一类语言:与非托管语言(即只是执行程序员所编写的逻辑、基本不偏离该逻辑的语言)不同,它们在运行时中执行 GC、运行时优化、绿色线程、并发处理等功能,使用户无需进行危险的底层管理。这类语言的优点在于,开发者可以专注于业务逻辑,从而沉浸于开发之中;但另一方面,程序的实际行为有时可能与程序员的直觉不同,因此有时需要进行精细的运行时调优。首先,我们来看一下在托管语言中最忠实于极简主义哲学、且汇编最为直观的 Go 语言。
Go 语言的二进制结构
| .text | .data | .gopclntab, .typelink 等 |
|---|---|---|
| 将要执行的机器码 | 将要存储的数据 | 语言运行时段 |
Go 语言并不会将用户所写的内容一对一地翻译为机器码,因此 .text 段中的逻辑与语言运行时段также紧密相关。此外,在 .text 段的汇编中还会添加用户并未自行编写的函数,例如 runtime.printnl()。通过这种自动化的代码插入,Go 语言帮助开发者摆脱手动管理的负担。
仅查看 Go 中的 main 函数部分
首先,编写一个简单的示例源文件 main.go,从 main 开始在 AMD64 机器上 查看。
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
10
随后按如下方式构建。
1go build main.go
2
为了便于进行底层调试,Go 提供了 go tool。要在 go tool 中仅查看主包中 main 函数对应的汇编,需输入以下命令。
1go tool objdump -s "main\.main" ./main
2
汇编
1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
2 main.go:7 0x468f60 493b6610 CMPQ SP, 0x10(R14)
3 main.go:7 0x468f64 762f JBE 0x468f95
4 main.go:7 0x468f66 55 PUSHQ BP
5 main.go:7 0x468f67 4889e5 MOVQ SP, BP
6 main.go:7 0x468f6a 4883ec10 SUBQ $0x10, SP
7 main.go:8 0x468f6e 90 NOPL
8 main.go:4 0x468f6f e8cca3fcff CALL runtime.printlock(SB)
9 main.go:4 0x468f74 488d05da290100 LEAQ 0x129da(IP), AX
10 main.go:4 0x468f7b bb0b000000 MOVL $0xb, BX
11 main.go:4 0x468f80 e83bacfcff CALL runtime.printstring(SB)
12 main.go:4 0x468f85 e8f6a5fcff CALL runtime.printnl(SB)
13 main.go:4 0x468f8a e811a4fcff CALL runtime.printunlock(SB)
14 main.go:9 0x468f8f 4883c410 ADDQ $0x10, SP
15 main.go:9 0x468f93 5d POPQ BP
16 main.go:9 0x468f94 c3 RET
17 main.go:7 0x468f95 e8e6afffff CALL runtime.morestack_noctxt.abi0(SB)
18 main.go:7 0x468f9a ebc4 JMP main.main(SB)
19
- 通过 CMPQ 将当前 goroutine 控制块寄存器(R14)中的栈守卫值与当前栈指针(SP)进行比较,以检查当前 goroutine 的栈帧空间是否充足;若空间不足,则跳转(JBE)至用于栈扩展的入口点 0x468f95 地址。
- 使用 PUSHQ BP 将先前的基址指针压入栈中。
- 将当前栈指针(SP)复制到基址指针(BP)寄存器中,从而固定函数开始时的栈基准点。
- 随后分配 16 字节的局部变量栈空间(SUBQ $0x10, SP),并利用 NOPL 填充虚拟指令以进行 CPU 缓存对齐。
- 为了同步 Go 运行时内部字符串的标准输出,调用 runtime.printlock(SB) 加锁。
- 使用 LEAQ 指令将常量字符串("Hello World")的起始地址按照 Go ABI 规范存入作为第一个参数使用的通用寄存器 AX 中。
- 随后将表示字符串长度的值存入第二个参数寄存器 BX 中。(MOVL $0xb, BX,即十进制的 11)
- 调用 runtime.printstring(SB),根据传入的 AX(数据地址)和 BX(长度)信息将其输出到控制台。
- 为了处理换行,调用 runtime.printnl(SB)。
- 输出完成后,通过 runtime.printunlock(SB) 释放锁。
- 使用 ADDQ $0x10, SP 恢复先前分配的 16 字节栈内存。
- 使用 POPQ BP 恢复原先的基址指针。
- 通过 RET 将控制权返回至调用该函数的位置。
- 如果在最初的栈检查中空间不足,则调用位于 0x468f95 地址的 runtime.morestack_noctxt.abi0(SB),以托管语言的方式动态扩展栈运行时。
- 栈扩展完成后,重新跳转(JMP)回 main.main(SB) 的入口点。如上所示,业务逻辑的汇编相当清晰,仅附加了薄薄一层运行时管理代码。
未进行优化的情况
上述形式是 Go 编译器自动内联两个独立函数后优化的结果。然而,出于学习目的,在这种情况下我们将不内联 sayHello。为此,使用以下标志编译源文件。
1go build -gcflags="-l" main.go
2
在 shell 中输出结果后可以发现重复的汇编代码。
1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
2
3go tool objdump -s "main\.sayHello" ./main
4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
5 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
6 main.go:3 0x468f64 7636 JBE 0x468f9c
7 main.go:3 0x468f66 55 PUSHQ BP
8 main.go:3 0x468f67 4889e5 MOVQ SP, BP
9 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
10 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
11 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
12 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
13 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
14 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
15 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
16 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
17 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
18 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
19 main.go:5 0x468f9a 5d POPQ BP
20 main.go:5 0x468f9b c3 RET
21 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
22 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
23 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
24 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
25 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
26 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
27
取消内联后,为了按照函数调用规范保留参数(AX, BX),会插入 MOVQ 指令,将值重新加载到栈指针偏移量 0x20(SP) 等位置。由此可见,编译器优化的对象正是这些不必要的内存移动操作以及调用开销。
下一次
下一次我们将讨论 Go 语言中的 if 语句和 switch 语句。如果以后有时间,也计划分析 Go 运行时段。