预备知识

1. AST(抽象语法树)

*AST是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构1。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,例如一个 if else 语句,我们可以从 2 3 + 7 这一表达式中解析出下图所示的抽象语法树。**

辅助编译器进行语义分析

golang编译原理_第1张图片

2.静态单赋值(Static Single Assignment, SSA)

SSA是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次。在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性

x := 1
x := 2
y := x

SSA的特性优化点

  • 常数传播(constant propagation)
  • 值域传播(value range propagation)
  • 稀疏有条件的常数传播(sparse conditional constant propagation)
  • 消除无用的程式码(dead code elimination)
  • 全域数值编号(global value numbering)
  • 消除部分的冗余(partial redundancy elimination)
  • 强度折减(strength reduction)
  • 寄存器分配(register allocation)

3.编译器

golang编译原理_第2张图片

4.go编译的四个阶段

golang编译原理_第3张图片

编译器入口文件:**src/cmd/compile/internal/gc/main.go **

一、词法分析与语法分析

lex

lex3 是用于生成词法分析器的工具,lex 生成的代码能够将一个文件中的字符分解成 Token 序列,很多语言在设计早期都会使用它快速设计出原型,lexer 通过正则匹配的方式将机器原本很难理解的字符串进行分解成很多的 Token。

golang编译原理_第4张图片

  1. 词法分析器Scanner

    对输入的字符流进行扫描,最终解析成token

入口文件(src/cmd/compile/internal/syntax/scanner.go )

token类型:(src/cmd/compile/internal/syntax/tokens.go )

Token 序列如下

package, json, import, (, io, ), …

2.语法分析器Parser

语法分析的输入就是词法分析器输出的 Token 序列,然后将编程语言的所有生产规则映射到对应的方法上,这些方法构成的树形结构最终会返回一个抽象语法树(go源文件)。

"json.go": SourceFile {
PackageName: "json",
ImportDecl: []Import{
"io",
},
TopLevelDecl: ...
}

二、类型检查

1. 静态类型检查

静态类型检查是基于对源代码的分析来确定运行程序类型安全的过程,如果我们的代码能够通过静态类型检查,那么当前程序在一定程度上就满足了类型安全的要求,它也可以被看作是一种代码优化的方式,能够减少程序在运行时的类型检查。

2. 动态类型检查

动态类型检查就是在运行时确定程序类型安全的过程,这个过程需要编程语言在编译时为所有的对象加入类型标签和信息,运行时就可以使用这些存储的类型信息来实现动态派发、向下转型、反射以及其他特性

3. golang 类型检查

Go 语言的编译器不仅使用静态类型检查来保证程序运行的类型安全,还会在编程期引入类型信息,让工程师能够使用反射来判断参数和变量的类型。

类型检查分别会按照以下的顺序对不同类型的节点进行验证和处理:

① 常量、类型和函数名及类型;

② 变量的赋值和初始化;

③ 函数和闭包的主体;

④ 哈希键值对的类型;

⑤ 导入函数体;

⑥外部的声明;

关键词OMAKE节点,根据make的第一个参数

在类型检查阶段对 make 进行改写

golang编译原理_第5张图片

三、中间代码生成

在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些并发执行的 Goroutine 会将所有函数对应的抽象语法树转换成中间代码。

golang编译原理_第6张图片

1. SSA配置初始化

SSA 配置的初始化过程其实就是做中间代码生成之前的准备工作,在这个过程中我们会缓存可能用到的类型指针、初始化 SSA 配置和一些之后会调用的运行时函数,还有** CPU 架构设置用于生成中间代码和机器码的函数,当前编译器使用的指针、寄存器大小、可用寄存器列表、掩码等编译选项**。

文件入口: initssaconfig (/src/cmd/compile/internal/gc/ssa.go)开始分析配置初始化的过程。

2. 遍历和替换

在生成中间代码之前,我们还需要对抽象语法树中节点的一些元素进行替换

func walk(fn *Node)
func walkappend(n *Node, init *Nodes, dst *Node) *Node
...
func walkrange(n *Node) *Node
func walkselect(sel *Node)
func walkselectcases(cases *Nodes) []*Node
func walkstmt(n *Node) *Node
func walkstmtlist(s []*Node)
func walkswitch(sw *Node)

golang编译原理_第7张图片

3. SSA代码生成

经过 walk 系列函数的处理之后,AST 的抽象语法树就不再会改变了,Go 语言的编译器会使用 compileSSA 函数(/src/cmd/compile/internal/gc/pgen.go**)将抽象语法树转换成中间代码,我们可以先看一下该函数的简要实现:

// #L297-L326
func compileSSA(fn *Node, worker int) {
    f := buildssa(fn, worker)
    pp := newProgs(fn, worker)
    genssa(f, pp)
    pp.Flush()
}

buildssa 就是用来具有 SSA 特性的中间代码的函数,我们可以使用命令行工具来观察当前中间代码的生成过程,假设我们有以下的 Go 语言源代码,其中只包含一个非常简单的 hello 函数:

package hello
func hello(a int) int {
    c := a + 2
    return c
}

我们可以使用 GOSSAFUNC 环境变量构建上述代码并获取从源代码到最终的中间代码经历的几十次迭代,所有的数据都被存储到了 ssa.html 文件中:

$ GOSSAFUNC=hello go build hello.go
# command-line-arguments
dumped SSA to ./ssa.html

ssa.html
golang编译原理_第8张图片

中间代码的生成过程其实就是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字在进行一次改写,改写后的语法树会经过多轮处理转变成最后的 SSA 中间代码

四、最终机器码生成

**机器码的生成过程其实就是对 SSA 中间代码的降级(lower)过程,在 SSA 中间代码降级的过程中,编译器将一些值重写成了目标 CPU 架构的特定值,降级的过程处理了所有机器特定的重写规则并对代码进行了一定程度的优化

1. 复杂指令集(CISC)和精简指令集(RISC)

  • 复杂指令集通过增加指令的数量减少需要执行的指令数;(x86)
  • 精简指令集能使用更少的指令完成目标的计算任务;(arm)

2. Go 语言支持的架构

golang编译原理_第9张图片
交叉编译

$ Mac 下编译 Linux 和 Windows 64位可执行程序
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
Linux 下编译 Mac 和 Windows 64位可执行程序
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
交叉编译不支持 CGO 所以要禁用它

3. 机器码生成

机器码的生成在 Go 的编译器中主要由两部分协同工作,其中一部分是负责 SSA 中间代码降级和根据目标架构进行特定处理的 src/cmd/compile/internal/ssa 包,另一部分是负责生成机器码的 src/cmd/internal/obj

  • src/cmd/compile/internal/ssa 主要负责对 SSA 中间代码进行降级、执行架构特定的优化和重写并生成 obj.Prog 指令;
  • src/cmd/internal/obj 作为一个汇编器会将这些指令最终转换成机器码完成这次的编译

// 通过命令输出汇编结果
GOOS=linux GOARCH=amd64 go tool compile -S hello.go

五、总结

golang编译原理_第10张图片

六、参考资料

  1. golang 编译原理 From 面向信仰编程:

https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-compile-intro/

  1. Golang 词法分析器浅析: https://blog.csdn.net/zhaoruixiang1111/article/details/89892435
  2. 走进golang编译原理:https://segmentfault.com/a/1190000020996545?utm_source=sf-related