什么是托管语言(Managed Language)?
什么是托管语言(Managed Language)?
与非托管语言(即仅执行程序员所编写逻辑、不进行额外处理的语言)不同,托管语言通过在运行时(Runtime)执行垃圾回收(GC)、运行时优化、绿色线程(Green Threads)、并发处理等机制,使开发者无需进行高风险的低级内存管理。
此类语言的优势在于开发者可以专注于业务逻辑,从而全身心投入开发;然而,由于程序的实际运行方式可能与程序员的直觉存在差异,有时需要进行精细的运行时调优。
首先,我们将探讨托管语言中最为遵循极简主义哲学且汇编代码最为直观的 Go 语言。
Go 语言的二进制结构
| .text | .data | .gopclntab, .typelink 等 |
|---|---|---|
| 待执行的机器码 | 待存储的数据 | 语言运行时段 |
由于 Go 语言并非将用户代码 1:1 翻译为机器码,因此 .text 段中的逻辑与语言运行时段紧密相关。
此外,诸如 runtime.printnl() 等用户未显式编写的函数也会被添加到 .text 段的汇编代码中。通过这种自动化的代码插入,Go 语言协助开发者摆脱了手动管理的负担。
仅查看 Go 中的 main 函数部分
首先,编写一个简单的示例源文件 main.go,并在 AMD64 机器上观察从 main 函数开始的执行情况。
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
随后执行以下命令进行构建:
1go build main.go
为了便于进行低级调试,Go 提供了 go tool 支持。若要在 go tool 中仅查看 main 包中 main 函数的汇编代码,请输入以下指令:
1go tool objdump -s "main\.main" ./main
汇编代码
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)
- 通过
CMPQ比较当前是否已进入线程,若符合条件则跳转至入口点 0x468f95。 - 通过
PUSHQ BP将入口点压入栈中。 - 在函数开始时,将栈起始位置指定给最近加载数据的寄存器 SP,以固定引用局部变量时的入口点。
- 此后预留 16 字节的局部变量栈空间 (
SUBQ $0x10, SP),并利用NOPL填充字节以进行 CPU 缓存对齐。 - 调用
runtime.printlock(SB),在 Go Runtime 中对字符串缓冲区的输出加锁。 - 使用
LEAQ指令将所分配字符串的起始地址存储到 AX(作为累加器,常用于数据存储的通用寄存器)。 - 随后将字符串长度 11 存储到 BX 寄存器(用于辅助运算及临时数据存储)。(
MOVL $0Xb, BX) - 通过
runtime.printstring(SB)将累加器信息输出至 SB。 - 使用
runtime.printnl(SB)向 SB 写入换行符。 - 使用
runtime.printunlock(SB)解除字符串缓冲区锁定。 - 通过
ADDQ $0x10, SP归还所借用的 16 字节栈内存。由于最初已将入口点压入栈中,现通过POPQ BP将其从栈中移除并发出返回信号。 - 此后通过
runtime.morestack_noctxt.abi0(SB)分配足够的栈空间并设置 GC 等运行时环境,体现了托管语言的特性。 - 跳转至受管理的
main.main(SB)地址。
如上所示,业务逻辑的汇编代码相当清晰,仅附加了轻量级的运行时管理。
无优化的情况
上述形式是 Go 编译器自动将两个独立函数进行内联(Inlining)优化后的结果。然而,为了学习目的,在此情况下我们将禁止 sayHello 函数的内联。
为此,需使用以下标志编译源码:
1 go build -gcflags="-l" main.go
在 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)
27yjlee@elegant:~/compare-assembly/go$ go tool objdump -s "main\.sayHello" ./main
28TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
29 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
30 main.go:3 0x468f64 7636 JBE 0x468f9c
31 main.go:3 0x468f66 55 PUSHQ BP
32 main.go:3 0x468f67 4889e5 MOVQ SP, BP
33 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
34 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
35 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
36 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
37 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
38 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
39OVQ 0x20(SP), AX
40 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
41 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
42 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
43 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
44 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
45 main.go:5 0x468f9a 5d POPQ BP
46 main.go:5 0x468f9b c3 RET
47 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
48 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
49 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
50 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
51 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
52 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
53yjlee@elegant:~/compare-assembly/go$
由此确认,编译器所进行的优化目标正是此类重复运算及低效的循环展开(Loop Unrolling)等。
下期预告
在下一期中,我们将探讨 Go 语言中的 if 语句和 switch 语句。若后续时间允许,亦将对 Go 运行时段进行分析。