本文的目标是希望读者对go语言的编译过程有一个全面的理解
一段程序要运行起来,需要将go代码生成机器能够识别的二进制代码
go代码生成机器码需要编译器经历:
词法分析 => 语法分析 => 类型检查 => 中间代码 => 代码优化 => 生成机器码
Go语言的编译器入口是 src/cmd/compile/internal/gc 包中的 main.go 文件,此函数会先获取命令行传入的参数并更新编译的选项和配置
随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析
func Main(archInit func(*Arch)) {
// …
lines := parseFiles(flag.Args()) 接下来我们将对各个阶段做深入介绍
词法分析
所有的编译过程都是从解析代码的源文件开始的
词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成Token序列,方便后面的处理和解析
我们一般会把执行词法分析的程序称为词法解析器(lexer)
Token可以是关键字,字符串,变量名,函数名
有效程序的”单词”都由Token表示,具体来说,这意味着”package”,”main”,”func” 等单词都为Token
Go语言允许我们使用go/scanner和go/token包在Go程序中执行解析程序,从而可以看到类似被编译器解析后的结构
如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止
helloworld程序解析后如下所示
1:1 package “package”
1:9 IDENT “main”
1:13 ; “\n”
2:1 import “import”
2:8 STRING “"fmt"”
2:13 ; “\n”
3:1 func “func”
3:6 IDENT “main”
3:10 ( “”
3:11 ) “”
3:13 { “”
4:3 IDENT “fmt”
4:6 . “”
4:7 IDENT “Println”
4:14 ( “”
4:15 STRING “"Hello, world!"”
4:30 ) “”
4:31 ; “\n”
5:1 } “”
5:2 ; “\n”
5:3 EOF “”
我们可以看到,词法解析器添加了分号,分号常常是在C语言等语言中一条语句后添加的
这解释了为什么Go不需要分号:词法解析器可以智能地加入分号
语法分析
语法分析的输入就是词法分析器输出的 Token 序列,这些序列会按照顺序被语法分析器进行解析,语法的解析过程就是将词法分析生成的 Token 按照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被归纳成一个 SourceFile 结构:
SourceFile = PackageClause “;” { ImportDecl “;” } { TopLevelDecl “;” }
标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果生成了抽象语法树(Abstract Syntax Tree,AST)
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。
与AST相对应的是CST(Concrete Syntax Trees),读者可以在参考资料中拓展阅读二者的差别
在AST中,我们能够看到程序结构,例如函数和常量声明
Go为我们提供了用于解析程序和查看AST的软件包:go/parser 和 go/ast
helloworld程序生成的AST如下所示
0 *ast.File {
1 . Package: 1:1
2 . Name: *ast.Ident {
3 . . NamePos: 1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: 3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: 5:6
26 . . . . Name: "main"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "main"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: 5:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: 5:10
37 . . . . . Closing: 5:11
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: 5:13
42 . . . . List: []ast.Stmt (len = 1) {
43 . . . . . 0: *ast.ExprStmt {
44 . . . . . . X: *ast.CallExpr {
45 . . . . . . . Fun: *ast.SelectorExpr {
46 . . . . . . . . X: *ast.Ident {
47 . . . . . . . . . NamePos: 6:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: 6:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: 6:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: 6:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello, world!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: 6:29
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: 7:1
69 . . . }
70 . . }
71 . }
.. . .. // Left out for brevity
83 } 如上的输出中我们能够看出一些信息
在Decls字段中,包含文件中所有声明的列表,例如import,常量,变量和函数
为了进一步理解,我们看一下对其的图形化抽象表示
image
红色表示与节点相对应的代码
main函数包含了3个部分,名称, 声明, 主体
名称是单词main的标识
由Type字段指定的声明将包含参数列表和返回类型
主体由一系列语句组成,其中包含程序的所有行。在本例中,只有一行
fmt.Println语句由AST中的很多部分组成,由ExprStmt声明。
ExprStmt代表一个表达式,其可以是本例中的函数调用,也可以是二进制运算(例如加法和减法等)
我们的ExprStmt包含一个CallExpr,这是我们的实际函数调用。这又包括几个部分,其中最重要的是Fun和Args
Fun包含对函数调用的引用,由SelectorExpr声明。在AST中,编译器尚未知道fmt是一个程序包,它也可能是AST中的变量
Args包含一个表达式列表,这些表达式是该函数的参数。在本例中,我们已将文字字符串传递给函数,因此它由类型为STRING的BasicLit表示。
类型检查
构造AST之后,将会对所有import的包进行解析
接着Go语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:
常量、类型和函数名及类型
变量的赋值和初始化
函数和闭包的主体
哈希键值对的类型
导入函数体
外部的声明
通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。
类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。
类型检查不止对类型进行了验证工作,还对 AST 进行了改写以及处理Go语言内置的关键字
生成中间代码
在上面的步骤完成之后,可以明确代码是正确有效的
接着将AST转换为程序的低级表示形式,即静态单一赋值形式(Static Single Assignment Form,SSA)形式,核心代码位于gc/ssa.go
SSA不是程序的最终状态,其可以更轻松地应用优化,其中最重要的是始终在使用变量之前定义变量,并且每个变量只分配一次
例如下面的代码我们可以看到第一个x的赋值没有必要的
x = 1
x = 2
y = 7
编辑器会将上面的代码变为如下,从而会删除x_1
x_1 = 1
x_2 = 2
y_1 = 7
生成SSA的初始版本后,将应用许多优化过程。这些优化应用于某些代码段,这些代码段可以使处理器执行起来更简单或更快速。
例如下面的代码是永远不会执行的,因此可以被消除。
if (false) {
fmt.Println(“test”)
}
优化的另一个示例是可以删除某些nil检查,因为编译器可以证明这些检查永远不会出错
在对SSA进行优化的过程中使用了S表达式(S-expressions)进行描述, S-expressions 是嵌套列表(树形结构)数据的一种表示法,由编程语言Lisp发明并普及
SSA优化过程中对于S表达式的应用如下所示,将8位的常量乘法组合起来
(Mul8 (Const8 [c]) (Const8 [d])) -> (Const8 [int64(int8(c*d))])
具体的优化包括
常数传播(constant propagation)
值域传播(value range propagation)
稀疏有条件的常数传播(sparse conditional constant propagation)
消除无用的程式码(dead code elimination)
全域数值编号(global value numbering)
消除部分的冗余(partial redundancy elimination)
强度折减(strength reduction)
寄存器分配(register allocation)
SSA优化
我们可以用下面的简单代码来查看SSA及其优化过程
对于如下程序
package main
import “fmt”
func main() {
fmt.Println(2)
}
我们需要在命令行运行如下指令来查看SSA
GOSSAFUNC环境变量代表我们需要查看SSA的函数并创建ssa.html文件
GOOS、GOARCH代表编译为在Linux 64-bit平台运行的代码
go build用-ldflags给go编译器传入参数
-S 标识将打印汇编代码
$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags “-S” simple.go
下面的命令等价
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
当打开ssa.html时,将显示许多代码片段,其中一些片段是隐藏的
image
Start片段是从AST生成的SSA。genssa片段是最终生成的Plan9汇编代码
Start片段如下
start
b1:-
v1 (?) = InitMem
v2 (?) = SP
v3 (?) = SB
v4 (?) = Addr <*uint8> {type.int} v3
v5 (?) = Addr <*int> {""..stmp_0} v3
v6 (6) = IMake <interface {}> v4 v5 (~arg0[interface {}])
v7 (?) = ConstInterface <interface {}>
v8 (?) = ArrayMake1 <[1]interface {}> v7
v9 (6) = VarDef
v10 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v9
v11 (6) = Store
v12 (6) = LocalAddr <*[1]interface {}> {.autotmp_11} v2 v11
v13 (6) = NilCheck
v14 (?) = Const64
v15 (?) = Const64
v16 (6) = PtrIndex <*interface {}> v12 v14
v17 (6) = Store
v18 (6) = NilCheck
v19 (6) = Copy <*interface {}> v12
v20 (6) = IsSliceInBounds
v25 (?) = ConstInterface
v28 (?) = OffPtr <*io.Writer> [0] v2
v29 (?) = Addr <*uint8> {go.itab.*os.File,io.Writer} v3
v30 (?) = Addr <**os.File> {os.Stdout} v3
v34 (?) = OffPtr <*[]interface {}> [16] v2
v37 (?) = OffPtr <*int> [40] v2
v39 (?) = OffPtr <*error> [48] v2
If v20 → b2 b3 (likely) (6)
b2: ← b1-
v23 (6) = Sub64
v24 (6) = SliceMake <[]interface {}> v19 v23 v23 (fmt.a[[]interface {}])
v26 (6) = Copy
v27 (+6) = InlMark
v31 (274) = Load <*os.File> v30 v26
v32 (274) = IMake
v33 (274) = Store
v35 (274) = Store
v36 (274) = StaticCall
v38 (274) = Load
v40 (274) = Load
Plain → b4 (+6)
b3: ← b1-
v21 (6) = Copy
v22 (6) = PanicBounds
Exit v22 (6)
b4: ← b2-
v41 (7) = Copy
Ret v41
name ~arg0[interface {}]: v6
name fmt.a[[]interface {}]: v24
name fmt.n[int]: v14 v38
name fmt.err[error]: v25 v40
name fmt..autotmp_3[int]: v14 v38
name fmt..autotmp_4[error]: v25 v40
每个v是一个新变量,可以单击以查看使用它的位置。
b是代码块,本例中我们有3个代码块:b1, b2和 b3
b1将始终被执行,b2和b3是条件块,如b1最后一行所示:If v20 → b2 b3 (likely) (6),只有v20为true会执行b2,v20为false会执行b3
我们可以点击v20查看其定义,其定义是v20 (6) = IsSliceInBoundsv14 v15
IsSliceInBounds 会执行如下检查:0 <= v14 <= v15 是否成立
我们可以单击v14和v15来查看它们的定义:v14 = Const64[0] ,v15 = Const64[1]
Const64为64位常量,因此 0 <= 0 <= 1 始终成立,因此v20始终成立
当我们在opt片段查看v20时,会发现v20 (6) = ConstBool[true],v20变为了始终为true
因此,我们会看到在opt deadcode片段中,b3块被删除了
代码优化
生成SSA之后,Go编译器还会进行一系列简单的优化,例如无效和无用代码的删除
我们将用同样的ssa.html文件,比较lower 和 lowered deadcode片段
image
在HTML文件中,某些行显示为灰色,这意味着它们将在下一阶段之一中被删除或更改
例如v15 = MOVQconst[1]为灰色,因为其在后面根本没有被使用。MOVQconst与我们之前看到的指令Const64相同,仅适用于amd64平台
机器码生成
完成以上步骤,最终还会生成跨平台的plan9汇编指令,并进一步根据目标的 CPU 架构生成二进制机器代码
Go语言源代码的 cmd/compile/internal 目录中包含了非常多机器码生成相关的包
不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm
Go语言能够在几乎全部常见的 CPU 指令集类型上运行。
问:go的编译速度相对于java为什么更快
快速编译是go的设计目标之一
go语法紧凑且规则因此更容易解析
go具有严格的依赖管理,没有循环依赖问题,计算依赖树非常高效
语言的设计易于分析,无需符号表即可进行解析
Go语言本身比Java简单得多,编辑器本身做的事不多
Go编译器较新,其中的无用代码更少
总结
在本文中详细介绍了go语言从源代码编译为机器码的过程
涉及了词法分析、语法分析、类型检查、SSA生成与代码优化、生成机器码等过程
以期帮助读者全面深入的了解go语言的编译过程
参考资料
作者知乎
blog
抽象语法树
静态单赋值︎
Abstract vs. Concrete Syntax Trees︎
Go compiler internals: adding a new statement to Go
Go compiler: SSA optimization rules description language
STEAM How a Go Program Compiles down to Machine Code
Go 编译原理
go语言编译为机器码经历的:词法分析 => 语法分析 => 类型检查 => 中间代码 => 代码优化 => 生成机器码
但是在源代码生成执行程序的过程中,其实还经历了链接等过程。总的来说一个程序的生命周期可以概括为: 编写代码 => 编译 => 链接 => 加载到内存 => 执行
在第5章我们将对其进行逐一解释
链接(link)
我们编写的程序可能会使用其他程序或程序库( library ) 正如我们在helloworld程序中使用的fmt package
我们编写的程序必须与这些程序或程序库一起才能够执行
链接是将我们编写的程序与我们需要的外部程序组合在一起的过程
链接器是系统软件,在系统开发中起着至关重要的作用,因为它可以进行单独的编译。您可以将它分解为更小,更易管理的块,然后分别进行修改和编译,而不是将一个大型应用程序组织为一个整体的源文件。当您更改其中一个模块时,只需重新编译它并重新链接应用程序,而无需重新编译其他源文件。
链接分为两种,静态链接与动态链接
静态链接的特点在于链接器会将程序中使用的所有库程序复制到最后的可执行文件中。而动态链接只会在最后的可执行文件中存储动态链接库的位置,并在运行时调用。
因此静态链接要更快,可移植,因为它不需要在运行它的系统上存在该库。但是在磁盘和内存上占用更多的空间
链接发生的过程会在两个地方,一种是静态链接会在编译时的最后一步发生,一种是动态链接在程序加载到内存时发生。
下面我们简单对比一下静态链接与动态链接
go语言是静态链接还是动态链接?
有时会看到一些比较老的文章说go语言是静态链接的,但这种说法是不准确的
现在的go语言不仅支持静态链接也支持动态编译
总的来说,go语言在一般默认情况下是静态链接的,但是一些特殊的情况,例如使用了CGO(即引用了C代码)的地方,则会使用操作系统的动态链接库。例如go语言的net/http包在默认情况下会应用libpthread与 libc 的动态链接库,这种情况会导致go语言程序虚拟内存的增加(下一文介绍)
go语言也支持在go build编译时传递参数来指定要生成的链接库的方式,我们可以使用go help buildmode命令查看
» go help buildmode jackson@192
-buildmode=archive
Build the listed non-main packages into .a files. Packages named
main are ignored.
-buildmode=c-archive
Build the listed main package, plus all packages it imports,
into a C archive file. The only callable symbols will be those
functions exported using a cgo //export comment. Requires
exactly one main package to be listed.
-buildmode=c-shared
Build the listed main package, plus all packages it imports,
into a C shared library. The only callable symbols will
be those functions exported using a cgo //export comment.
Requires exactly one main package to be listed.
-buildmode=default
Listed main packages are built into executables and listed
non-main packages are built into .a files (the default
behavior).
-buildmode=shared
Combine all the listed non-main packages into a single shared
library that will be used when building with the -linkshared
option. Packages named main are ignored.
-buildmode=exe
Build the listed main packages and everything they import into
executables. Packages not named main are ignored.
-buildmode=pie
Build the listed main packages and everything they import into
position independent executables (PIE). Packages not named
main are ignored.
-buildmode=plugin
Build the listed main packages, plus all packages that they
import, into a Go plugin. Packages not named main are ignored. archive: 将非 main package构建为 .a 文件. main 包将被忽略。
c-archive: 将 main package构建为及其导入的所有package构建为构建到 C 归档文件中
c-shared: 将mainpackage构建为,以及它们导入的所有package构建到C 动态库中。
shared: 将所有非 main package合并到一个动态库中,当使用-linkshared参数后,能够使用此动态库
exe: 将main package和其导入的package构建为成为可执行文件
本文不再介绍go如何手动使用动态库这一高级功能,读者只需现在知道go可以实现这一功能即可
编译与链接的具体过程
下面我们以helloworld程序为例,来说明go语言编译与链接的过程,我们可以使用go build命令,-x参数代表了打印执行的过程
go build -x main.go
输出如下:
WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg « ‘EOF’ # internal
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
EOF
cd /Users/jackson/go/src/viper/XXX
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/pkg.a -trimpath “$WORK/b001=>” -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D /Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg.a # internal
cp $WORK/b001/pkg.a /Users/jackson/Library/Caches/go-build/cf/cf0dc65f39f01c8494192fa8af14570b445f6a25b762edf0b7258c22d6e10dc8-d # internal
cat >$WORK/b001/importcfg.link « ‘EOF’ # internal
packagefile command-line-arguments=$WORK/b001/pkg.a
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
…
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/pkg.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/
下面我们对输出进行逐行分析
创建了一个临时目录,用于存放临时文件。默认情况下命令结束时自动删除此目录,如果需要保留添加-work参数。
WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg « ‘EOF’ # internal
生成编译配置文件,主要为编译过程需要的外部依赖(如:引用的其他包的函数定义)
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
编译,生成中间结果$WORK/b001/pkg.a,
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/pkg.a -trimpath “$WORK/b001=>” -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D _/Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go
.a文件由compile命令生成,也可以通过go tool compile进行调用
.a类型的文件又叫做目标文件(object file),其是一个压缩包,内部包含了_.PKGDEF、
_go.o 两个文件,分别为编译目标文件和链接目标文件
$ file pkg.a # 检查文件格式
pkg.a: current ar archive # 说明是ar格式的打包文件
$ ar x pkg.a #解包文件
$ ls
_.PKGDEF _go.o
文件内容由代码导出的函数、变量以及引用的其他包的信息组成。为了弄清这两个文件包含的信息需要查看go编译器实现的相关代码,相关代码在src/cmd/compile/internal/gc/obj.go文件中(源码中的文件内容可能随版本更新变化,本系列文章以Go1.13.5版本为准)
下面代码中生成ar文件,ar文件 是一种非常简单的打包文件格式,广泛用于linux中静态链接库文件中,文件以 字符串”!\n”开头。随后跟着60字节的文件头部(包含文件名、修改时间等信息),之后跟着文件内容。因为ar文件格式简单,Go编译器直接在函数中实现了ar打包过程。
startArchiveEntry用于预留ar文件头信息位置(60字节),finishArchiveEntry用于写入文件头信息,因为文件头信息中包含文件大小,在写入完成之前文件大小未知,所以分两步完成。
func dumpobj1(outfile string, mode int) {
bout, err := bio.Create(outfile)
if err != nil {
flusherrors()
fmt.Printf(“can’t create %s: %v\n”, outfile, err)
errorexit()
}
defer bout.Close()
bout.WriteString(“!
if mode&modeCompilerObj != 0 {
start := startArchiveEntry(bout)
dumpCompilerObj(bout)
finishArchiveEntry(bout, start, "__.PKGDEF")
}
if mode&modeLinkerObj != 0 {
start := startArchiveEntry(bout)
dumpLinkerObj(bout)
finishArchiveEntry(bout, start, "_go_.o")
} } 生成链接配置文件,主要为需要链接的其他依赖
cat >$WORK/b001/importcfg.link « ‘EOF’ # internal
packagefile command-line-arguments=$WORK/b001/pkg.a
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
…
EOF
执行链接器,生成最终可执行文件main,同时可执行文件会拷贝到当前路径,最后删除临时文件
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/pkg.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/
总结
在本文中,我们介绍了go程序从源代码到运行需要经历的重要一环——链接,并介绍了静态链接与动态链接
在本文中,我们用一个例子介绍了编译与链接的具体过程
在下文中,我们将介绍go语言的内存分配
参考资料
项目链接
作者知乎
blog
wiki obj code
golang Command compile
golang Command Link
初探 Go 的编译命令执行过程
How does the go build command work ?
Golang编译器漫谈(1)编译器和连接器
What are the differences between static and dynamic (shared) library linking?