1.13 机器码生成——汇编器
在SSA阶段,编译器先执行与特定指令集无关的优化,再执行与特定指令集有关的优化,并最终生成与特定指令集有关的指令和寄存器分配方式。在ssa/gen/genericOps.go中,包含了和特定指令集无关的Op操作。在ssa/gen/AMD64Ops.go中,包含了和AMD64指令集相关的操作。
在SSA lower阶段之后,就开始执行与特定指令集有关的重写与优化,在genssa阶段,编译器会生成与单个指令对应的obj/link.go中的Prog结构。
例如,最终生成的指令MOVL R1,R2会被Prog表示为As=MOVL,From=R1,To=R2。Pcond代表跳转指令,除此之外,还有一些与特定指令集相关的结构。
在SSA后,编译器将调用与特定指令集有关的汇编器(Assembler)生成obj文件,obj文件作为链接器(Linker)的输入,生成二进制可执行文件。internal/obj目录中包含了汇编与链接的核心逻辑,内部有许多与机器码生成相关的包。不同类型的指令集(amd64、arm64、mips64等)需要使用不同的包生成。Go语言目前能在所有常见的CPU指令集类型上编译运行。
汇编和链接是编译器后端与特定指令集有关的阶段。由于历史原因,Go语言的汇编器基于了不太常见的plan9汇编器的输入形式[5]。需要注意的是,输入汇编器中的汇编指令不是机器码的表现形式,其仍然是人类可读的底层抽象。在Go语言runtime及math/big标准库中,可以看到许多特定指令集的汇编代码,Go语言也提供了一些方式用于查看编译器生成的汇编代码。
对于上面的简单程序,其输出的汇编代码如下所示(笔者删除了FUNCDATA与PCDATA这两个与垃圾回收有关的操作),这段汇编代码显示了main函数栈帧的大小与代码的行号及其对应的汇编指令。其中,$88-0表明了栈帧的大小及函数参数的大小,在第9章中会详细介绍栈大小、栈扩容函数runtime.morestack_noctxt的知识。关于汇编代码最前方获取TLS线程本地存储的操作,会在第15章介绍。
在本书后面的章节中,还会经常通过查看汇编代码的方式来研究Go语言中某些特性的实现方式,正所谓汇编之下无秘密。