GoSuda

什么是托管语言(Managed Language)?

By Lee Yunjin
views ...

什么是托管语言(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 运行时段进行分析。