---
id: 191ebec04cf97317ba418a120521d30b
author: Lee Yunjin
title: 托管语言的运行时结构与 Go 二进制文件 AMD64 汇编分析
description: 比较托管语言与非托管语言的执行模型,并通过 go tool objdump 提取的 AMD64 汇编,分析 Go 二进制文件中的栈检查及运行时函数调用结构。
language: zh
date: 2026-05-17T12:00:00Z
path: /blog/posts/managed-languages-intro-656b7470
category: lowlevel
---

## 什么是托管语言?

托管语言是指这样一类语言:与非托管语言(即只是执行程序员所编写的逻辑、基本不偏离该逻辑的语言)不同,它们在运行时中执行 GC、运行时优化、绿色线程、并发处理等功能,使用户无需进行危险的底层管理。
这类语言的优点在于,开发者可以专注于业务逻辑,从而沉浸于开发之中;但另一方面,程序的实际行为有时可能与程序员的直觉不同,因此有时需要进行精细的运行时调优。
首先,我们来看一下在托管语言中最忠实于极简主义哲学、且汇编最为直观的 Go 语言。

### Go 语言的二进制结构

| .text              | .data         | .gopclntab, .typelink 等 |
| ------------------ | ------------- | ------------------------ |
| 将要执行的机器码   | 将要存储的数据 | 语言运行时段             |

Go 语言并不会将用户所写的内容一对一地翻译为机器码,因此 .text 段中的逻辑与语言运行时段также紧密相关。
此外,在 .text 段的汇编中还会添加用户并未自行编写的函数,例如 runtime.printnl()。
通过这种自动化的代码插入,Go 语言帮助开发者摆脱手动管理的负担。

### 仅查看 Go 中的 main 函数部分

首先,编写一个简单的示例源文件 main.go,从 main 开始在 **AMD64 机器上** 查看。

```go
package main

func sayHello(msg string) {
    println(msg)
}

func main() {
    sayHello("Hello World")
}

```

随后按如下方式构建。

```bash
go build main.go

```

为了便于进行底层调试,Go 提供了 go tool。
要在 go tool 中仅查看主包中 main 函数对应的汇编,需输入以下命令。

```bash
go tool objdump -s "main\.main" ./main

```

### 汇编

```go
TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
  main.go:7             0x468f64                762f                    JBE 0x468f95
  main.go:7             0x468f66                55                      PUSHQ BP
  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
  main.go:8             0x468f6e                90                      NOPL
  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
  main.go:9             0x468f93                5d                      POPQ BP
  main.go:9             0x468f94                c3                      RET
  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)

```

- 通过 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。
为此,使用以下标志编译源文件。

```bash
go build -gcflags="-l" main.go

```

在 shell 中输出结果后可以发现重复的汇编代码。

```bash
yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go

go tool objdump -s "main\.sayHello" ./main
TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
  main.go:3             0x468f64                7636                   JBE 0x468f9c
  main.go:3             0x468f66                55                     PUSHQ BP
  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
  main.go:5             0x468f9a                5d                     POPQ BP
  main.go:5             0x468f9b                c3                     RET
  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)

```

取消内联后,为了按照函数调用规范保留参数(AX, BX),会插入 MOVQ 指令,将值重新加载到栈指针偏移量 0x20(SP) 等位置。
由此可见,编译器优化的对象正是这些不必要的内存移动操作以及调用开销。

### 下一次

下一次我们将讨论 Go 语言中的 if 语句和 switch 语句。如果以后有时间,也计划分析 Go 运行时段。
