前言
从 c 到可执行文件,包含了 编译 和 链接 这两步。通常在编译构建 c 项目的时候,也可以在 make 的过程中,看到 编译 和 链接 这种中间步骤。
然而,go 在这方面,有更进一步的封装,直接跑 go build 就行了,也不知道背后干了个啥。
最近因为搞 cgo 的优化,需要了解这里面的过程,记录一下的。
编译过程
底层还是分为 编译 和 链接 这两步,go build 可以类比为 go 标准的 make 工具。
对于 go 编译器而言,go 是提供给用户的统一的命令,实际上它还包含了很多其他的执行程序,比如 compile, asm, cgo, link 等。
go build 的执行过程,跟常见的 make 是类似的,大致有这么些事情:
- 调用
compile将 go 文件(以及依赖文件)编译为 .a 文件 - 注意,这一步也是有缓存的,原文件没变更,则直接 copy
.a文件 - 如果期间有
.s文件,则用asm来编译 - 如果有
import C,则调用cgo先生成一段 go 文件 - 最后,通过
link链接成最终的可执行文件
如果想看具体的编译过程,可以指定 -x,比如:
1 | -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 | # 老的 go 编译 cmd/dist |
简单解释一下:
dist只是一个封装的临时,编译的时候,还是用的compile,link这种编译工具(也就是 toolchain)- 先用老的 go 重新编译新的 toolchain,然后再用新的 toolchain 编译新的 go(中间有一些重复编译 toolchain,不是那么重要)
如果想看具体的编译过程,可以指定 -v=5,比如:
1 | bash -x all.bash -v=5 |
总结
go 对编译构建工具都提供了完整的封装,这个对于使用者而言,确实是更方便了,不需要自己折腾 Makefile,或者 bazel 这种构建工具了。
其具体过程,则跟常见的构建工具是类似的,分开编译,中间结果缓存,最后链接。
平常开发倒是用不着了解这些,不过要是修改 go 本身的话,对这些过程还是得比较清楚才行了;尤其是跑测试的时候,dist 命令中的 test,又是封装了一层编译的测试过程,这里面一环套一环的,还是比较多细节的。