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,又是封装了一层编译的测试过程,这里面一环套一环的,还是比较多细节的。