什么是托管语言(Managed Language)?
By Lee Yunjin
views ...
什么是托管语言(Managed Language)?
与非托管语言(即仅执行程序员编写的逻辑而不进行过多干预的语言)不同,托管语言是在运行时执行垃圾回收(GC)、运行时优化、绿色线程(Green Threads)、并发处理等机制,从而使开发者无需进行高风险的底层管理的语言。对于此类语言,其优势在于开发者可以专注于业务逻辑,全身心投入开发;但另一方面,由于程序的实际运行方式可能与程序员的直觉存在差异,有时需要进行精细的运行时调优。首先,我们将探讨在托管语言中最为遵循极简主义哲学且汇编代码最为直观的 Go 语言。
Go 语言的二进制结构
| .text | .data | .gopclntab, .typelink 等 |
|---|---|---|
| 待执行的机器码 | 待存储的数据 | 语言运行时部分 |
| 由于 Go 语言并非将用户代码 1:1 翻译为机器码,因此 .text 段中的逻辑与语言运行时部分紧密相关。 | ||
| 此外,诸如 runtime.printnl() 等用户未显式编写的函数也会被添加到 .text 段的汇编代码中。 | ||
| 通过这种自动化的代码插入,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。若要仅查看 main 包中 main 函数的汇编代码,需在 go tool 中输入以下指令。
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 指令,根据 Go ABI 规范,将作为第一个参数的常量字符串("Hello World")的起始地址存储到通用寄存器 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 编译器自动对两个分离的函数进行内联(Inlining)优化后的结果。然而,为了学习目的,在此情况下我们将禁止 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 运行时部分。