Skip to main content

Go: compile 编译器

📅 2026-02-05 ✏️ 2026-03-10 Inside Go CS GO

1 · Go 编译器#

1.1 · 如何使用 dlv 调试 complier#

为什么需要构建本地go和设置GOROOT环境变量?

让「用来编译的 go 命令」和「被编译的 cmd/compile」共用同一套内部包,避免混用不同版本的包导致编译失败或行为异常

# 1. 下载go源码,进入src目录,构建工具链后回到上层目录
./make.bash && cd ..

# 2. 设置GOROOT,指向本地源码根目录
export GOROOT=$(pwd)

cd $GOROOT/src
# 3. 编译(禁止优化)
$GOROOT/bin/go build -a -gcflags="all=-N -l" -o /tmp/compile cmd/compile
dlv exec /tmp/compile -- test.go

# 4. 使用dlv操作
# b gc.Main

1.2 · gopls setup#

# https://github.com/golang/tools/blob/master/gopls/doc/advanced.md#working-on-the-go-source-distribution
#
# 1. 先执行 ./src/make.bash 生成 bin/go
./make.bash && cd ..
# 2. 把自己构建的 go 放在 PATH 最前面,优先使用本地构建的 go
export PATH=/home/liu/dev/go_src/bin:$PATH
# 3. 设置GOROOT,指向本地源码根目录
export GOROOT=$(pwd)
# 4. 在 GOROOT/src 下创建 go.work: 把 std 和 cmd 都加入工作区
go work init . cmd
# 5. GOROOT/src 作为工作区

1.3 · 快速概览:编译流程

https://github.com/golang/go/blob/master/src/cmd/compile/README.md

Go 编译器将源代码转换为可执行文件,共8个主要阶段(官方分为前端、中端、后端):

源代码 → 词法/语法分析 → 类型检查 → IR生成 → 中端优化 → Walk → SSA → 机器码 → 可执行文件
         └────── 前端 ──────┘        └─── 中端 ───────────┘     └──────── 后端 ─────┘

关键阶段

  1. 词法/语法分析:源代码 → Token 流;Token → 抽象语法树AST(syntax.File)
  2. 类型检查:AST → 符号表、类型信息(使用 types2 包)
  3. 中间代码生成(Noding + typecheck):通过 Unified IR 将 语法树(syntax)和类型信息(types2) 转换为编译器的 IR(完成类型传播和隐式转换)
  4. 中端优化:内联、逃逸分析、死代码消除、去虚化等
  5. Walk 阶段:保证求值顺序、反糖、拆解复杂语句、转换高级构造
  6. SSA 生成与优化:IR → SSA → 机器无关优化
  7. 机器码生成:SSA 降级 → 机器相关优化 → 汇编 → 目标文件(→ 链接 → 可执行文件 这两步属于 link 阶段)
  8. Export:生成 export data,供下游包编译使用

IR vs. AST: IR也是树形结构,IR根据AST+type信息生成

IR vs. SSA: IR的优化是语言层面,SSA的优化是指令/值层面

NOTE: 类型检查分散在两处:

    1. types2:语义类型检查、接口实现、符号解析等
    1. typecheck:类型传播、插入隐式转换(如 CONVIFACE)等

1.4 · go tool compile#

go tool compile --help
# -N -l 禁用优化,禁用内联

# 不同参数,打印不同阶段的信息
# 1/2 阶段无
#
# walk 前后ir tree的区别 W是函数级/w是expr级
# -W    debug parse tree after type checking
# -w    debug type checking

# 中端优化
# -m    print optimization decisions

# 大多数中端的操作,也有部分SSA/指令平台相关的操作
# -d value
# enable debugging settings; try -d help
# SSA相关操作
# -d=ssa/help

# 汇编
# -S

1.5 · 不同阶段的函数入口

b cmd/compile/internal/syntax.Parse

b types2.Config.Check

b noder.unified
# ir树按函数划分;后续优化也是以函数为基本单位
b noder.readBodies

# ir 类型检查,以节点为单位
# typecheck.Stmt / typecheck.Expr
b typecheck.typecheck

b DevirtualizeAndInlinePackage
b escape.Funcs
b deadlocals.Funcs

b walk.Walk

b ssagen.Compile
b ssa.Compile

b obj.Flushplist
gc.Main()                                                // gc/main.go:61
  ──── 1. 解析 ────
  noder.LoadPackage(filenames)                           // gc/main.go:208
    ├── syntax.Parse(...)                                // noder/noder.go:59
    │     词法分析 + 语法分析 → syntax.File

    ──── 2. types2 类型检查 + 3. IR 构建 ────
    └── unified(m, noders)                               // noder/noder.go:77
          ├── writePkgStub(m, noders)                    // noder/unified.go:195 → 318
          │     └── checkFiles(m, noders)                // noder/unified.go:319 → irgen.go:26
          │           └── conf.Check(...)                // irgen.go:95   types2 类型检查
          ├── readPackage(...)                           // noder/unified.go:200 !先写后读,使得本地/导入包共用一个读
          ├── r.pkgInit(...)                             // noder/unified.go:203
          │     └── r.pkgDecls(target)                   // noder/reader.go:3326
          │           └── target.Funcs = append(...)     // noder/reader.go:3339  加入函数列表
          └── readBodies(target, false)                  // noder/unified.go:205
                └── pri.funcBody(fn)                     // noder/reader.go:1294
                      ├── r.declareParams()              // noder/reader.go:1309
                      └── r.stmts()                      // noder/reader.go:1654
                            └── typecheck.Stmt(n)        // noder/reader.go:1667  IR 类型检查

  ──── 4. 中端优化 ────
  interleaved.DevirtualizeAndInlinePackage(...)          // gc/main.go:240  去虚化 + 内联(交替运行:去虚化后可内联,内联暴露更多去虚化机会)
  noder.MakeWrappers(...)                                // gc/main.go:242
  loopvar.ForCapture(fn)                                 // gc/main.go:247  循环变量捕获修正
  pkginit.MakeTask()                                     // gc/main.go:252  生成包初始化任务
  symABIs.GenABIWrappers()                               // gc/main.go:256  生成 ABI 包装函数 ABI0<->ABIInternal
  deadlocals.Funcs(...)                                  // gc/main.go:258  死局部变量消除
  escape.Funcs(...)                                      // gc/main.go:269  逃逸分析

  ──── 5-8. 后端(per function)────
  for ... {                                              // gc/main.go:290
    enqueueFunc(fn)                                      // gc/main.go:307
      └── prepareFunc(fn)                                // gc/compile.go:90
            └── walk.Walk(fn)                            // gc/compile.go:115  Walk

    compileFunctions(profile)                            // gc/main.go:316
      └── ssagen.Compile(fn, worker, profile)            // ssagen/pgen.go:303
            ├── buildssa(fn, ...)                        // ssagen/pgen.go:304  IR → SSA
            │     └── ssa.Compile(f)                     //                     SSA 优化
            ├── genssa(f, pp)                            // ssagen/pgen.go:314  SSA → 机器码
            │     └── liveness.Compute(...)              // ssagen/ssa.go:6341  GC stack map
            └── pp.Flush()                               // ssagen/pgen.go:329  汇编、写目标文件
  }

  ──── 9. 写目标文件 ────
  dumpdata()                                             // gc/main.go:352 收集元数据
  dumpobj()                                              // gc/main.go:354 写出 .o 文件

1.6 · 编译细节

1.6.1 · 1. 词法与语法分析#

  • 包:cmd/compile/internal/syntax

词法分析(Lexical Analysis)

  • 工具:syntax.Tokensyntax.Scanner
  • 过程:源代码 → Token 流
  • 位置信息:用于错误报告和调试

语法分析(Syntax Analysis)

  • 工具:syntax.Parsersyntax.File

  • 入口函数:syntax.Parse()

  • 源码文件结构组织

    SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
    
    TopLevelDecl = Declaration | FunctionDecl | MethodDecl .
    Declaration  = ConstDecl | TypeDecl | VarDecl .

最终得到:

// compile/internal/syntax/nodes.go
//
// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
	Pragma    Pragma // 存储文件开头的 //go: 指令信息
	PkgName   *Name  // 包名,对应 `package xxx` 声
	DeclList  []Decl // 所有顶层声明的列表:导入、常量、变量、类型、函数(方法)
	GoVersion string // 文件要求的最低 Go 版本
}

1.6.2 · 2. 类型检查(Semantic Analysis)#

入口types2/check.go 中的 Checker.checkFiles() 方法,由 noder/irgen.go 中的 checkFiles() 函数调用

类型检查的三个核心任务

  1. 标识符解析(Identifier Resolution):标识符 → Object

    • 将 AST 中的标识符映射到声明的对象
  2. 类型推导(Type Deduction):表达式 → Type

    • 确定每个表达式的类型
  3. 常量求值(Constant Evaluation):常量表达式 → Value

    • 在编译期计算常量的值

这三个任务相互依赖:

  • 常量值的确定可能依赖类型
  • 表达式的类型可能依赖常量的值
  • 标识符的解析可能依赖类型信息(如结构体字段)

关键数据结构

Object(符号对象)types2/object.go

  • 代表所有命名实体(const、var、type、func 等)
  • 每个 AST 标识符映射到一个 Object
  • 接口定义:
    type Object interface {
    	Name() string   // 包级别名称
    	Exported() bool // 是否以大写字母开头
    	Type() Type     // 对象类型
    	Pos() token.Pos // 声明中标识符的位置
    	Parent() *Scope // 声明该对象的作用域
    	Pkg() *Package  // 所在包,nil 表示 Universe scope
    	Id() string     // 对象 ID(用于唯一性)
    }

Scope(作用域)types2/scope.go

  • 静态作用域(词法作用域)
  • 分层:全局 → 包 → 文件 → 代码块
  • 存放 Object,支持名称查找

Type(类型)types2/type.go 及相关文件

  • 基础类型、复合类型、结构体、接口、Named、Tuple 等
  • 类型等价性:Identical() 函数(结构一致则等价)
  • 类型比较:可比较(等/不等)、可排序(大小等)

Checker(检查器)types2/check.go

  • 核心方法:checkFiles()
  • 维护类型检查的状态和信息

类型检查流程

checkFiles()
  ├─ initFiles(验证同一包)
  ├─ collectObjects(AST → Object + declInfo)
  ├─ packageObjects(递归检查 check.objMap 中的对象)
  │  └─ check.objDecl() 对每个对象进行检查
  │     ├─ 类型表达式:Checker.typ()
  │     ├─ 求值表达式:Checker.exprInternal()
  │     └─ 类型兼容性:Checker.assignment()
  ├─ processDelayed(处理延迟队列)
  │  └─ 函数体检查、循环依赖、接口整理
  └─ initOrder(确定全局初始化顺序)

包加载与导出数据

  • 通过 Importer 接口的 Import 方法导入其他包
  • 读取编译输出的对象文件(.a 或 .o)中的 export data
  • 静态链接:链接器根据重定位信息规划内存

1.6.3 · 3. 中间代码生成(IR Tree)#

src/cmd/compile/internal/ir

作用

  • AST 是源代码的直接表示,信息不完整
  • IR Tree 是信息更丰富的数据结构,包含编译信息
  • 便于后续优化和代码生成

关键接口和结构

  • Node 接口:实现 Node 的各种类型

    • expr.go:表达式节点
    • stmt.go:语句节点
    • func.go:函数节点
    • name.go:名称节点
  • Name 结构:变量/常量名称及其信息

  • Func 结构:函数定义及其信息

  • Op 类型:操作类型(如加法、调用等)

构建逻辑:由 irgen.generate() 驱动


1.6.4 · 中端:优化阶段

Go 编译器在多个阶段进行优化,包括:

1.6.4.1 · 前期优化(在 Unified IR 写入阶段)#

  • 早期死代码消除:在 unified IR 写入过程中集成

1.6.4.2 · 中期优化(IR 级别,Walk 前)#

  • 内联逃逸分析死代码消除
  • 去虚拟化:用实际类型替换接口调用

1.6.5 · Walk 阶段#

cmd/compile/internal/walk

目的

  1. 拆解复杂语句:将复杂语句分解为更简单的语句,引入临时变量
  2. 反糖(Desugaring):将高级构造转换为低级构造
    • switch → 二分查找或跳转表
    • for-range → 简单 for 循环
    • map/channel 操作 → 运行时调用

执行时间:在 SSA 生成前,是 IR 上的最后一个遍历


1.6.6 · 包初始化

  • src/cmd/compile/internal/pkginit:初始化逻辑
  • src/cmd/compile/internal/staticinit:静态 vs 动态初始化判断

初始化顺序

  1. 依赖包的初始化
  2. 全局变量初始化(编译时确定 vs 运行时)
  3. init 函数初始化

优化:尽可能在编译期完成变量初始化


1.6.7 · 优化阶段

1.6.7.1 · 1. Dead Code 消除#

src/cmd/compile/internal/deadcode

  • 入口:deadcode.Func()
  • 作用:
    • 减小可执行文件大小
    • 提升缓存命中率(增加局部性)
  • 例:移除 if 分支中永不执行的代码、布尔运算的冗余分支

1.6.7.2 · 2. 函数内联(Inline)#

src/cmd/compile/internal/inline

原理

  • 函数调用有固定开销(上下文切换)
  • 使用函数体替换函数调用以消除开销

优缺点

  • 优点:缩短调用链,提升性能
  • 缺点:增大可执行文件体积,错误信息展示不友好

过程

  1. 遍历函数调用链(ir/scc.goVisitFuncsBottomUp
  2. 计算函数复杂度(hairyVisitor 结构体)
  3. 判断是否可以内联
  4. 进行内联(InlineCalls 函数)

优化策略

  • 一个函数被频繁调用但复杂度高,不会被内联
  • 解决方案:抽出复杂代码为单独函数,降低原函数复杂度,提高内联机会

1.6.7.3 · 3. 逃逸分析(Escape Analysis)#

src/cmd/compile/internal/escape

目标

  • 确定变量应该分配在栈还是堆上
  • 栈:快,生命周期与函数一致,自动释放
  • 堆:慢,需要 GC 管理

分析规则

  1. 指向栈上对象的指针不能在堆上
  2. 指向栈上对象的指针的生命周期不能大于其指向的对象
  3. 大对象分配到堆上

算法:基于 AST 的静态数据流分析


1.6.8 · 后端详解

1.6.8.1 · 1. SSA(Static Single Assignment)生成#

src/cmd/compile/internal/{ssagen,ssa}

入口ssagen.Compile() 函数和 ssa.Compile() 函数

SSA 特点

  • 每个变量只赋值一次
  • 低级中间表示,便于实现优化
  • 便于进行数据流分析

过程

  • IR → buildssa() 函数(ssagen/ssa.go)将 IR 转换为 SSA
  • 通过 insertPhis 函数处理 phi 节点
  • 调用 ssa.Compile() 进行 SSA 优化
  • 输出:SSA 值(Value)和块(Block)形成的 SSA 图

1.6.8.2 · 2. SSA 优化#

机器无关优化(Generic Passes):

  • 死代码消除:移除未使用的计算
  • 空指针检查移除:消除冗余的 nil 检查
  • 未使用分支移除:删除未被取用的控制流分支
  • 表达式优化:
    • 常量折叠:1 + 23
    • 乘法优化:x * 2x << 1
    • 浮点运算优化

实现方式

  • 直接在 Go 代码中实现的 passes
  • 通过 rewrite rules(cmd/compile/internal/ssa/_gen/*.rules)代码生成的优化规则

关键机制

  • Lower Pass:将通用 SSA 转换为机器相关的变体(如 amd64 特定操作)
  • Rewrite Rules:使用 S-expression 形式的 DSL 描述,自动生成优化代码
  • 完整优化流程:从高级 SSA 开始,经过多个 passes,最终得到机器相关的 SSA

1.6.8.3 · 3. 机器码生成#

流程

SSA → Lower Pass(SSA → 机器相关变体)

最终优化(dead code、值移动、寄存器分配)

栈帧布局(分配栈偏移给局部变量)

obj.Prog 指令流

汇编器(cmd/internal/obj)

机器码 → 目标文件

最终文件内容

  • 机器码
  • Reflect 数据(用于反射)
  • Export 数据(用于导入)
  • 调试信息(DWARF)

1.6.8.4 · 4. ABI(应用二进制接口)#

  • 基于寄存器的 ABI(替代栈传参)
  • 定义参数/返回值如何通过寄存器传递
  • 优化函数调用性能

1.7 · 编译流程总结

1.7.1 · 完整编译链

go build main.go

Go 命令行调度编译

go tool compile main.go → main.o
    ├─ 词法分析:source → tokens
    ├─ 语法分析:tokens → AST (syntax.File)
    ├─ 类型检查:AST → Object + Type (types2)
    ├─ IR 生成(Noding):types2 → Unified IR → compiler IR
    ├─ 中端优化:内联、逃逸分析、死代码消除(IR 级)
    ├─ Walk:反糖、拆解、高级构造转换
    ├─ SSA 生成:IR → SSA
    ├─ SSA 优化:机器无关优化 (Generic Passes)
    ├─ SSA 降级:通用 SSA → 机器相关 SSA (Lower Pass)
    ├─ 最终优化:死代码消除、寄存器分配、指令调度
    └─ 机器码:SSA → obj.Prog → 目标文件 main.o

go tool link main.o → main
    ├─ 读取所有 .o 文件和 export data
    ├─ 符号解析和重定位
    ├─ 内存布局
    └─ 生成可执行文件

main(可执行文件)

1.7.2 · 优化发生的位置

  • Unified IR 写入阶段:早期死代码消除
  • IR 级(Walk 前):内联、逃逸分析、死代码消除、去虚拟化
  • SSA 级(Generic Passes):机器无关优化(常量折叠、分支消除、nil 检查移除等)
  • SSA 降级后:机器相关优化(如 amd64 特定的指令组合)
  • 最终优化:寄存器分配、栈帧布局、指令调度、活跃指针分析

1.8 · 参考资源

https://internals-for-interns.com/tags/compiler