Go语言是一种静态强类型、编译型开发语言,编译器扮演着核心角色,它不仅负责将Go源代码转换成机器代码,还涉及代码的优化、错误检查和性能分析等关键环节。
本文将为读者提供一个关于Go语言编译原理和编译过程的全面介绍。从编译器的基本工作原理讲起,逐步深入到Go语言特有的编译技术和优化策略。 帮助读者更好的学习Go语言的编译过程。
Go语言编译器是将Go源代码转化为可执行文件的关键工具。
Go编译器最初用C语言编写的,并且是基于Plan 9的C编译器。随着Go语言的发展,官方编译器逐渐转向自身语言实现,这一过程称为“自举”(bootstrapping)。
在Go 1.5版本中,编译器和运行时(runtime)被重写为Go语言,这标志着Go语言的成熟。此后,Go编译器不断优化,版本迭代中引入了多项性能改进和新特性,如更好的垃圾回收、更快的编译速度等。
Go 语言编译器的源代码在 src/cmd/compile 目录中。
Go语言的编译器主要由以下几个模块组成,每个模块对应Go源码中的不同部分:
src/cmd/compile/internal/syntax
目录。src/cmd/compile/internal/syntax
目录。src/cmd/compile/internal/types
和src/cmd/compile/internal/typecheck
目录。src/cmd/compile/internal/ssa
目录。src/cmd/compile/internal/ssa
目录,因为很多优化都是在SSA形式上进行的。src/cmd/compile/internal/ssa/gen
目录下的不同子目录中。src/cmd/compile/internal/ssa
目录。src/cmd/link
目录。这些模块共同工作,将Go源代码编译成可执行文件。
从源代码到可执行文件的过程,也称为编译过程,通常包含以下步骤:
词法分析是编译过程中的第一步,它的主要任务是读取源代码的字符序列,并将它们组织成有意义的序列,称为“词法单元”(tokens)。
tokens
Go语言源码src/cmd/compile/internal/syntax/tokens.go中的定义了tokens
的类型
const (
_ token = iota
_EOF // EOF
// names and literals : 标识符 和 字面量
_Name // name
_Literal // literal
// operators and operations : 操作符号
_Operator // op
_AssignOp // op=
...
// delimiters : : 操作符号
_Lparen // (
_Lbrack // [
...
// keywords : 关键字
_Break // break
_Case // case
...
// empty line comment to exclude it from .String
tokenCount //
)
词法解析器scanner定义如下:
type scanner struct {
source // 当前扫描的数据源文件
mode uint // 启用的模式
nlsemi bool // if set '\n' and EOF translate to ';'
// current token, valid after calling next()
line, col uint
blank bool // line is blank up to col
tok token // token 的类型
lit string // token 的字面量; valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
bad bool // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
kind LitKind // valid if tok is _Literal
op Operator // valid if tok is _Operator, _Star, _AssignOp, or _IncOp
prec int // valid if tok is _Operator, _Star, _AssignOp, or _IncOp
}
cmd/compile/internal/syntax.scanner 每次都会通过 cmd/compile/internal/syntax.source.nextch 函数获取文件中最近的未被解析的字符(遇到了空格和换行符这些空白字符会直接跳过),然后根据当前字符的不同执行不同的 case:
switch s.ch {
case -1:
...
case '\n':
s.nextch()
s.lit = "newline"
s.tok = _Semi
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
s.number(false)
case '"':
s.stdString()
case '`':
s.rawString()
case '\'':
s.rune()
case '(':
...
...
}
如果当前字符是’0’, ‘1’, …, ‘9’ 之一,则会调用s.number(false)
,尝试匹配一个数字。
如果当前字符是'"', 则调用s.stdString()
,尝试匹配一个字符串。
token解析完毕后,会将token的字面值保存在scanner.lit
中。
词法解析得到的 tokens 在 Go 语言的编译器中是即时生成并(被语法解析)消费的,而不是存储在某个列表或队列中。这种设计可以减少内存使用,并提高解析的效率。
语法分析负责将词法分析阶段生成的词法单元(tokens)根据语言的语法规则组织成抽象语法树(AST)。
抽象语法树 AST 是源代码逻辑结构的树状表示,它反映了程序的语法结构,而不包含代码中的空格、注释等无关信息。
以(a+b)*c
为例,最终生成的抽象语法树如下:
Go语言编译时,每个 Go 源代码文件最终都会被解析成一个独立的抽象语法树,所以语法树最顶层的结构或者开始符号都是 SourceFile:
// SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
每一个文件都包含一个 package
的定义以及可选的 import
声明和其他的顶层声明(TopLevelDecl
)
当解析一个 Go 源文件时,解析器会创建一个 syntax.File 节点,该节点代表整个文件的 AST。这个 syntax.File 节点包含了文件中所有顶级声明(如函数、类型和变量声明)的列表。
// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
Pragma Pragma
PkgName *Name
DeclList []Decl
EOF Pos
node
}
顶层声明有五大类型,分别是常量、类型、变量、函数和方法,在文件 src/cmd/compile/internal/syntax/nodes.go 中找到这五大类型的定义。
type (
Decl interface {
Node
aDecl()
}
...
ImportDecl struct {
...
}
ConstDecl struct {
...
}
// Name Type
TypeDecl struct {
...
}
VarDecl struct {
...
}
FuncDecl struct {
Pragma Pragma
Recv *Field // nil means regular function
Name *Name
TParamList []*Field // nil means no type parameters
Type *FuncType
Body *BlockStmt // nil means no body (forward declaration)
decl
}
)
解析技术指的是用于构建 AST 的具体算法。常见的解析技术包括:
在 src/cmd/compile/internal/syntax/parser.go
文件中定义了语法解析器parser
type parser struct {
file *PosBase
errh ErrorHandler
mode Mode
pragh PragmaHandler
scanner // 词法解析器
base *PosBase // current position base
first error // first error encountered
errcnt int // number of errors encountered
pragma Pragma // pragmas
fnest int // function nesting level (for error handling)
xnest int // expression nesting level (for complit ambiguity resolution)
indent []byte // tracing support
}
解析过程的入口点通常是一个解析文件或包的方法。例如,ParseFile 方法会解析一个完整的 Go 源文件,并返回一个包含了文件所有顶级声明的 AST。
语法解析中会调用词法解析的next()进行词法解析。
// If pragh != nil, it is called with each pragma encountered.
func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) {
...
p.init(base, src, errh, pragh, mode)
p.next()
return p.fileOrNil(), p.first
}
在parser.fileOrNil() 中根据token的不同类型配置响应的解析函数:
// { TopLevelDecl ";" }
for p.tok != _EOF {
switch p.tok {
case _Const:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.constDecl)
case _Type:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)
case _Var:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.varDecl)
case _Func:
p.next()
if d := p.funcDeclOrNil(); d != nil {
f.DeclList = append(f.DeclList, d)
}
default:
if p.tok == _Lbrace && len(f.DeclList) > 0 && isEmptyFuncDecl(f.DeclList[len(f.DeclList)-1]) {
// opening { of function declaration on next line
p.syntaxError("unexpected semicolon or newline before {")
} else {
p.syntaxError("non-declaration statement outside function body")
}
p.advance(_Const, _Type, _Var, _Func)
continue
}
这其中,函数的解析是最为复杂的部分。在前面五大声明类型的介绍中,我们看到函数声明类型的定义:
// 函数声明类型: https://github.com/golang/go/blob/619b8fd7d2c94af12933f409e962b99aa9263555/src/cmd/compile/internal/syntax/nodes.go#L102
FuncDecl struct {
Pragma Pragma
Recv *Field // nil means regular function
Name *Name
TParamList []*Field // nil means no type parameters
Type *FuncType
Body *BlockStmt // nil means no body (forward declaration)
decl
}
// 函数体 : https://github.com/golang/go/blob/619b8fd7d2c94af12933f409e962b99aa9263555/src/cmd/compile/internal/syntax/nodes.go#L347
BlockStmt struct {
List []Stmt // 语句块 Statements
Rbrace Pos
stmt
}
在go1.19中,go已经支持30+种类的Statements
不同语句块Statements定义
type (
Stmt interface {
Node
aStmt()
}
SimpleStmt interface {
Stmt
aSimpleStmt()
}
...
BlockStmt struct {
List []Stmt
Rbrace Pos
stmt
}
...
AssignStmt struct {
Op Operator // 0 means no operation
Lhs, Rhs Expr // Rhs == nil means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
simpleStmt
}
...
IfStmt struct {
Init SimpleStmt
Cond Expr
Then *BlockStmt
Else Stmt // either nil, *IfStmt, or *BlockStmt
stmt
}
...
)
与词法分析类似, 在 Go 语言的编译器中,语法分析后生成的抽象语法树(AST)通常不会存储在一个全局的位置或容器中。相反,AST 是动态构建的,并且通常在构建完成后立即被后续的编译阶段(如类型检查、优化和代码生成)所使用。
语义分析是编译过程中的一个关键阶段,它发生在语法分析之后,目的是确保源程序的语义符合语言定义的规则。语义分析主要包括类型检查、作用域解析、绑定标识符到声明、以及其他语义规则的检查。
类型检查是语义分析中的核心部分,它负责验证程序中的每个表达式和语句是否符合类型系统的规则。以下是类型检查的几个关键方面:
除了类型检查,语义分析还包括作用域解析和标识符绑定:
语义分析还包括检查程序是否遵守了语言的其他语义规则,例如:
// /cmd/compile/main.go
// 编译过程主入口
func main() {
...
gc.Main(archInit)
base.Exit(0)
}
// /cmd/compile/internal/gc.Main
// https://github.com/golang/go/blob/619b8fd7d2c94af12933f409e962b99aa9263555/src/cmd/compile/internal/gc/main.go#L55
// 编译主函数
func Main(archInit func(*ssagen.ArchInfo)) {
base.Timer.Start("fe", "init")
defer handlePanic()
...
// Parse and typecheck input.
noder.LoadPackage(flag.Args())
...
}
func LoadPackage(filenames []string) {
...
// Move the entire syntax processing logic into a separate goroutine to avoid blocking on the "sem".
go func() {
for i, filename := range filenames {
...
// 词法解析和语法解析
p.file, _ = syntax.Parse(fbase, f, p.error, p.pragma, syntax.CheckBranches) // errors are tracked via p.error
}()
}
}()
// Use types2 to type-check and generate IR.
// 类型检查 & 生成中间代码
check2(noders)
}
// check2 type checks a Go package using types2, and then generates IR
// using the results.
func check2(noders []*noder) {
// 类型检查
m, pkg, info := checkFiles(noders)
g := irgen{
target: typecheck.Target,
self: pkg,
info: info,
posMap: m,
objs: make(map[types2.Object]*ir.Name),
typs: make(map[types2.Type]*types.Type),
}
// 生成中间代码
g.generate(noders)
}
// checkFiles configures and runs the types2 checker on the given
// parsed source files and then returns the result.
func checkFiles(noders []*noder) (posMap, *types2.Package, *types2.Info) {
...
pkg, err := conf.Check(base.Ctxt.Pkgpath, files, info)
...
return m, pkg, info
}
// [/src/cmd/compile/internal/types2/](https://github.com/golang/go/blob/619b8fd7d2c94af12933f409e962b99aa9263555/src/cmd/compile/internal/types2/api.go#L414)api.go
// https://github.com/golang/go/blob/619b8fd7d2c94af12933f409e962b99aa9263555/src/cmd/compile/internal/types2/api.go#L414
func (conf *Config) Check(path string, files []*syntax.File, info *Info) (*Package, error) {
pkg := NewPackage(path, "")
return pkg, NewChecker(conf, pkg, info).Files(files)
}
// Files checks the provided files as part of the checker's package.
func (check *Checker) Files(files []*syntax.File) error { return check.checkFiles(files) }
在函数check.checkFiles即为类型检查的主逻辑。
func (check *Checker) checkFiles(files []*syntax.File) (err error) {
函数check.checkFiles会遍历每个文件对应的语法分析得到的AST(节点),进行类型检查等语义分析。
此处代码逻辑和函数调用较繁杂,本文先不展开介绍,有兴趣的读者可以自己阅读源码。
在编译器设计中,中间代码生成是将源代码的抽象语法树(AST)转换为一种中间表示(Intermediate Representation,简称 IR)的过程。
中间代码是编译器或者虚拟机使用的语言,是源代码与目标机器代码之间的桥梁,它旨在简化代码优化和目标代码生成的步骤。
IR 是编译器内部使用的一种代码表示形式,通常是与特定机器无关的,其忽略了编译器需要面对的各种复杂场景,并且设计得更容易进行分析和转换。
IR 可以有多种形式,如三地址代码(Three-Address Code),控制流图(Control Flow Graph,CFG),或静态单赋值形式(Static Single Assignment,SSA)等。
IR 应该满足以下几个目标:
SSA 是一种特别的 IR 形式,它在编译器优化中非常流行。在 SSA 形式中,每个变量只被赋值一次,这简化了变量的生命周期分析和许多优化技术的实现,如死代码消除、常量传播、循环不变式移动等。
在 Go 1.18 及以上版本的编译器中,SSA 被广泛应用于中间代码的生成和优化。Go 编译器的 SSA 包位于 src/cmd/compile/internal/ssa
中。这个包包含了将 AST 转换为 SSA 形式的代码,以及在 SSA 形式上执行的各种优化。
以下是 Go 编译器生成 SSA IR 的简化过程:
在 Go 编译器的源码中,SSA 包的 compile.go
文件通常包含了触发 SSA 构建和优化的代码。例如,buildssa
函数会将 AST 转换为 SSA 形式,而 optimize
函数则负责在 SSA 形式上执行优化。
要深入了解 Go 编译器中 SSA 的实现和应用,可以查阅 src/cmd/compile/internal/ssa
包中的源码。这个包中的代码负责将 Go 语言的程序转换为 SSA 形式,并在此基础上进行优化,最终生成高效的机器代码。
在 Go 语言编译器中,代码生成是编译过程的最后一个阶段,它负责将优化后的中间表示(IR)转换为目标机器代码。这个阶段涉及到将 IR 映射到具体的机器指令集,并进行平台相关的优化和调整。
目标代码生成阶段通常遵循以下步骤:
目标代码生成阶段还需要考虑平台相关的优化和调整,这些可能包括:
O2
或 O3
)来调整优化策略。在 Go 1.18 及以上版本的编译器中,代码生成的相关代码主要位于 src/cmd/compile/internal/ssa 包中。
在编程中,链接是将编译器生成的一个或多个目标文件(通常是 .o
文件)以及库文件合并成一个可执行文件的过程。链接可以是静态的或动态的,它们之间有几个关键的区别:
.so
或 .dll
文件)的引用。Go 语言的链接过程有一些独特的特点,主要体现在以下几个方面:
在 Go 1.18 及以上版本的编译器中,链接过程的相关代码位于 src/cmd/link
包中。这个包包含了将编译后的目标文件合并成一个可执行文件的代码。链接器处理符号解析、地址分配、重定位和其他必要的后处理步骤。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。