这是我参与「第五届青训营 」笔记创作活动的第14天
本篇文章中,我们着重探索一个Go程序是怎样从Go源文件编程机器码的. 作者还会将Go语言的编译过程同Clang Gcc进行比较,总结出它们之间的异同点.
首先,我们使用一个简单的Go程序作为我们的实验对象:
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
我们使用go命令的-n选项打印go驱动的子命令信息:
skyrain@KawaiiLaptop:/tmp$ go build -n hello.go
#
# command-line-arguments
#
mkdir -p $WORK/b001/
cd /tmp
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid MI-wWvKR39KcrhVjoUjb/MI-wWvKR39KcrhVjoUjb -goversion go1.19.5 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./hello.go
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=vdrcmsFuKh7_tXQ1b8Xu/MI-wWvKR39KcrhVjoUjb/MI-wWvKR39KcrhVjoUjb/vdrcmsFuKh7_tXQ1b8Xu -extld=gcc $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out hello
上面给出了简略后的命令输出.可以看到,Go的编译过程和C/C++类似,都存在编译和链接两个过程.
接下来我们研究一下Go代码编译中发生的几个具体步骤.笔者之前了解过两种针对C/C++的编译方案,分别是GCC和CLang方案. GCC方案将编译器前端和后端融合在了一起,即前端生成AST后,经过多层优化,直接传递给后端生成汇编. CLang编译方案则是编译器前端生成AST后转换为LLVM IR,传递给更通用的LLVM后端,进行优化并编译成平台特定的汇编代码. GNU C采用的是GCC方案,而Rust等一些新兴语言则采用了LLVM方案,只需要编写编译器前端即可.
我们知道,主要的编译过程是将源代码翻译成抽象代码树,(可能会生成中间表示)对其进行多轮优化后生成机器码或字节码.Go 语言的中间表示使用了名为SSA的特性,拥有这种特性的中间代码的变量只会被赋值一次. Go的编译过程会生成中间代码,经过50多轮的优化,最终送到机器码生成包中生成适配各类硬件架构的机器码. 值得一提的是,不同于GCC和Clang编译方案,Go编译器输出的是二进制目标文件,而不是汇编代码. 输出的目标文件直接进入链接器和库文件链接成为可执行程序.