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