0%

浅谈 go 编译过程

前言

从 c 到可执行文件,包含了 编译 和 链接 这两步。通常在编译构建 c 项目的时候,也可以在 make 的过程中,看到 编译 和 链接 这种中间步骤。

然而,go 在这方面,有更进一步的封装,直接跑 go build 就行了,也不知道背后干了个啥。

最近因为搞 cgo 的优化,需要了解这里面的过程,记录一下的。

编译过程

底层还是分为 编译 和 链接 这两步,go build 可以类比为 go 标准的 make 工具。

对于 go 编译器而言,go 是提供给用户的统一的命令,实际上它还包含了很多其他的执行程序,比如 compile, asm, cgo, link 等。

go build 的执行过程,跟常见的 make 是类似的,大致有这么些事情:

  1. 调用 compile 将 go 文件(以及依赖文件)编译为 .a 文件
  2. 注意,这一步也是有缓存的,原文件没变更,则直接 copy .a 文件
  3. 如果期间有 .s 文件,则用 asm 来编译
  4. 如果有 import C,则调用 cgo 先生成一段 go 文件
  5. 最后,通过 link 链接成最终的可执行文件

如果想看具体的编译过程,可以指定 -x,比如:

1
2
# -work 表示保留编译时的临时目录
go build -x -work .

值得一提的是,go test 也是先 build 生成一个,封装了测试框架的可执行文件,所以,build 的参数也同样可用。
比如:

1
go test -x -work

PS: go test 默认编译出来的是不带调试符号的,如果需要调试,可以加上 -o file 指定可执行文件,这样可以启用调试符号。
(貌似这个在 MacOS 上并不有效,还是 Linux 上可靠 :<)

自举过程

go 语言是完成自举了的,自举大致过程:

首先需要一个老版本的 go,1.4+,先用老的 go,编译 cmd/dist,生成 dist 可执行文件;再用这个 dist 来完成新版本 go 的编译。

在输出日志中,可以看到一下主要步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 老的 go 编译 cmd/dist 
Building Go cmd/dist using /path/to/old/go. (go1.14.15 linux/amd64)
# 接下来的几步,都是 cmd/dist 来执行的
# 老的 go,编译新代码的工具链,compile, asm, link, cgo
Building Go toolchain1 using /path/to/old/go.
# 新工具链,编译 go 命令,这里叫 go_bootstrap
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
# 新的 go_bootstrap,重新编译工具链,以为 toolchain1 没有 buildid
Building Go toolchain2 using go_bootstrap and Go toolchain1.
# 再来一回,还是因为 buildid,为了更加一致
Building Go toolchain3 using go_bootstrap and Go toolchain2.
# 使用 go_bootstrap,编译完整的 go
Building packages and commands for linux/amd64.

简单解释一下:

  1. dist 只是一个封装的临时,编译的时候,还是用的 compile, link 这种编译工具(也就是 toolchain)
  2. 先用老的 go 重新编译新的 toolchain,然后再用新的 toolchain 编译新的 go(中间有一些重复编译 toolchain,不是那么重要)

如果想看具体的编译过程,可以指定 -v=5,比如:

1
bash -x all.bash -v=5

总结

go 对编译构建工具都提供了完整的封装,这个对于使用者而言,确实是更方便了,不需要自己折腾 Makefile,或者 bazel 这种构建工具了。

其具体过程,则跟常见的构建工具是类似的,分开编译,中间结果缓存,最后链接。

平常开发倒是用不着了解这些,不过要是修改 go 本身的话,对这些过程还是得比较清楚才行了;尤其是跑测试的时候,dist 命令中的 test,又是封装了一层编译的测试过程,这里面一环套一环的,还是比较多细节的。