9 个通用寄存器的分配顺序(ABIInternal 寄存器分配)
在 Go 1.17.1 的源码中,AMD64 架构下这 9 个通用寄存器的分配顺序(Assignment Order)主要定义在以下文件中:
C:\Go\src\cmd\compile\internal\ssa\gen 这个文件是安装时候,自动编译生成的。
虽然产物是
在
三、C:\Go\src\cmd\compile\internal\ssa\config.go
C:\Go\src\cmd\compile\internal\ssa\opGen.go(此文件由AMD64Ops.go,根据同目录下的main.go工具生成) 中
一、 为什么是这个顺序?
这个顺序并非随机选择,而是基于以下考量:
- ABI 兼容性参考:部分参考了标准的 SysV ABI(Linux 常用),但为了 Go 语言的特殊需求(如高效的垃圾回收和快速栈扩容)进行了调整。
- 避免特殊寄存器:避开了
R14(用于存放当前的g指针)和R15(常用于内部辅助)。 - 指令编码优化:
RAX,RBX,RCX等传统寄存器在某些汇编指令中的编码长度更短。
二、registersAMD64C:\Go\src\cmd\compile\internal\ssa\gen 这个文件是安装时候,自动编译生成的。
registersAMD64 并不是由人工手写的,而是由 cmd/compile/internal/ssa/gen 包下的代码生成工具main.go 自动生成的虽然产物是
registersAMD64,但它的生成源头依然是你之前看到的 gen/AMD64Ops.go。在
AMD64Ops.go 中,有一个变量(通常也叫 regNamesAMD64 或类似的字符串列表),gen/main.go 脚本会遍历这个字符串列表, 按顺序为它们分配
如果你的架构是Intel,则使用AMD64Ops.go 文件产生 C:\Go\src\cmd\compile\internal\ssa\opGen.go ,
reg: 0, 1, 2... 并生成registersAMD64 结构体。如果你的架构是Intel,则使用AMD64Ops.go 文件产生 C:\Go\src\cmd\compile\internal\ssa\opGen.go ,
registersAMD64 就在这个文件当中var registersAMD64 = [...]Register{ {0, x86.REG_AX, 0, "AX"}, {1, x86.REG_CX, 1, "CX"}, {2, x86.REG_DX, 2, "DX"}, {3, x86.REG_BX, 3, "BX"}, {4, x86.REGSP, -1, "SP"}, {5, x86.REG_BP, 4, "BP"}, {6, x86.REG_SI, 5, "SI"}, {7, x86.REG_DI, 6, "DI"}, {8, x86.REG_R8, 7, "R8"}, {9, x86.REG_R9, 8, "R9"}, {10, x86.REG_R10, 9, "R10"}, {11, x86.REG_R11, 10, "R11"}, {12, x86.REG_R12, 11, "R12"}, {13, x86.REG_R13, 12, "R13"}, {14, x86.REGG, -1, "g"}, {15, x86.REG_R15, 13, "R15"}, {16, x86.REG_X0, -1, "X0"}, {17, x86.REG_X1, -1, "X1"}, {18, x86.REG_X2, -1, "X2"}, {19, x86.REG_X3, -1, "X3"}, {20, x86.REG_X4, -1, "X4"}, {21, x86.REG_X5, -1, "X5"}, {22, x86.REG_X6, -1, "X6"}, {23, x86.REG_X7, -1, "X7"}, {24, x86.REG_X8, -1, "X8"}, {25, x86.REG_X9, -1, "X9"}, {26, x86.REG_X10, -1, "X10"}, {27, x86.REG_X11, -1, "X11"}, {28, x86.REG_X12, -1, "X12"}, {29, x86.REG_X13, -1, "X13"}, {30, x86.REG_X14, -1, "X14"}, {31, x86.REG_X15, -1, "X15"}, {32, 0, -1, "SB"}, }
为什么是那 9 个寄存器,以及为什么是那个顺序,实际都是人为设置的,唯一人工维护的地方就在这里。当然理由上面已经阐述了,请看下面的文件设置
C:\Go\src\cmd\compile\internal\ssa\gen\AMD64Ops.go
// copied from ../../amd64/reg.go var regNamesAMD64 = []string{ "AX", "CX", "DX", "BX", "SP", "BP", "SI", "DI", "R8", "R9", "R10", "R11", "R12", "R13", "g", // a.k.a. R14 "R15", "X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9", "X10", "X11", "X12", "X13", "X14", "X15", // constant 0 in ABIInternal // If you add registers, update asyncPreempt in runtime // pseudo-registers "SB", }
三、C:\Go\src\cmd\compile\internal\ssa\config.go
// A Config holds readonly compilation information. // It is created once, early during compilation, // and shared across all compilations. type Config struct { arch string // "amd64", etc. PtrSize int64 // 4 or 8; copy of cmd/internal/sys.Arch.PtrSize RegSize int64 // 4 or 8; copy of cmd/internal/sys.Arch.RegSize Types Types lowerBlock blockRewriter // lowering function lowerValue valueRewriter // lowering function splitLoad valueRewriter // function for splitting merged load ops; only used on some architectures registers []Register // machine registers gpRegMask regMask // general purpose integer register mask fpRegMask regMask // floating point register mask fp32RegMask regMask // floating point register mask fp64RegMask regMask // floating point register mask specialRegMask regMask // special register mask intParamRegs []int8 // register numbers of integer param (in/out) registers floatParamRegs []int8 // register numbers of floating param (in/out) registers ABI1 *abi.ABIConfig // "ABIInternal" under development // TODO change comment when this becomes current ABI0 *abi.ABIConfig GCRegMap []*Register // garbage collector register map, by GC register index FPReg int8 // register number of frame pointer, -1 if not used LinkReg int8 // register number of link register if it is a general purpose register, -1 if not used hasGReg bool // has hardware g register ctxt *obj.Link // Generic arch information optimize bool // Do optimization noDuffDevice bool // Don't use Duff's device useSSE bool // Use SSE for non-float operations useAvg bool // Use optimizations that need Avg* operations useHmul bool // Use optimizations that need Hmul* operations SoftFloat bool // Race bool // race detector enabled BigEndian bool // UseFMA bool // Use hardware FMA operation } // NewConfig returns a new configuration object for the given architecture. func NewConfig(arch string, types Types, ctxt *obj.Link, optimize bool) *Config { c := &Config{arch: arch, Types: types} c.useAvg = true c.useHmul = true switch arch { case "amd64": c.PtrSize = 8 c.RegSize = 8 c.lowerBlock = rewriteBlockAMD64 c.lowerValue = rewriteValueAMD64 c.splitLoad = rewriteValueAMD64splitload c.registers = registersAMD64[:] c.gpRegMask = gpRegMaskAMD64 c.fpRegMask = fpRegMaskAMD64 c.specialRegMask = specialRegMaskAMD64 c.intParamRegs = paramIntRegAMD64 c.floatParamRegs = paramFloatRegAMD64 c.FPReg = framepointerRegAMD64 c.LinkReg = linkRegAMD64 c.hasGReg = buildcfg.Experiment.RegabiG ... }
C:\Go\src\cmd\compile\internal\abi\abiutils.go 文件中定义了 ABIConfig 这个结构体
它有几个方法 Copy、FloatIndexFor、NumParamRegs、ABIAnalyzeTypes、ABIAnalyzeFuncType、ABIAnalyze、updateOffset
...
type ABIConfig struct { // Do we need anything more than this? offsetForLocals int64 // e.g., obj.(*Link).FixedFrameSize() -- extra linkage information on some architectures. regAmounts RegAmounts regsForTypeCache map[*types.Type]int }
...
四、ssagen目录 负责生成 SSA
更具体地说,它是“带有 ABI 意识”的 SSA 生成器。它不仅把代码变简单了,还顺便把这 9 个寄存器按照 C:\Go\src\cmd\compile\internal\ssa\gen\AMD64Ops.go 里定好的顺序,分配给了函数的各个参数。
在 Go 1.17 及其后续版本(包括当前的 1.24/1.25)中,
src/cmd/compile/internal/ssagen 包确实起到了“承上启下”的关键作用,它是从 抽象语法树 (AST) 向 静态单赋值 (SSA) 转换的指挥中心。 ABIInternal 寄存器分配,ssagen 这个包负责了最核心的转换逻辑:
1. 负责 ABI 策略的执行
虽然寄存器的名字是在
amd64 包里定义的,但“如何使用这些寄存器”的规则是在 ssagen 中实现的: ssagen/abi.go:这个文件里的NewConfig函数会根据不同架构(如 AMD64)提供的寄存器列表,构建出一套具体的分配策略。- 它决定了:如果一个参数是
int64,占用 1 个寄存器;如果是一个complex128,则需要拆分占用 2 个寄存器。
2. 参数从“逻辑”到“物理”的映射
当
ssagen 开始处理一个函数时,它会执行以下操作: - 分析函数签名:统计有多少个参数,每个参数是什么类型。
- 分配槽位 (Assign Slots):调用
abi逻辑,将参数依次放进那 9 个寄存器(AX, BX, CX...)中。 - 生成 SSA 节点:在转换过程中,它会生成
OpArgIntReg或OpArgFloatReg指令。这些指令明确告诉后续的优化器:“这个变量是从 AX 寄存器里拿出来的”。
3. 衔接“栈”与“寄存器” (ABI Wrappers)
ssagen 还负责生成 ABI 适配器 (Wrappers)。 - 如果一个旧的汇编函数(使用 ABI0,即通过栈传参)想要调用新的 Go 函数(使用 ABIInternal,即通过寄存器传参),
ssagen里的逻辑会生成一段“胶水代码”:- 从栈上取出值→放入 AX→ 调用函数。
4. 关键函数追踪
如果你在
ssagen 目录下查找,以下代码片段是 1.17.1 中最值得看的: ssa.go中的buildSSA:这是将整个函数体转换为 SSA 的入口。abi.go中的AssignForCalls:这是计算参数应该分配到哪个寄存器还是分配到栈上的核心算法。
总结
你说
ssagen 负责生成 SSA 是对的,但更具体地说,它是“带有 ABI 意识”的 SSA 生成器。它不仅把代码变简单了,还顺便把这 9 个寄存器按照 AMD64Ops.go 里定好的顺序,分配给了函数的各个参数。 既然你已经摸清了
ssagen 的地位,是否想看一看在 ssagen/abi.go 中,它是如何判断一个寄存器是否已经被占满,从而决定将第 10 个参数“踢”到栈上的代码逻辑?五、
在 Go 1.17.1 的架构中,如果说
六、
在 Go 1.17.1 的源码中,
这个文件定义了 SSA 抽象层的通用操作(如
opGen.go 可以看到变成了 OpAdd64
七、验证过程
如果你想亲自验证这 9 个寄存器的顺序,你可以尝试在
在
src/cmd/compile/internal/ssa 就是整个编译器的“大脑”和“工厂”在 Go 1.17.1 的架构中,如果说
ssagen 是“翻译官”(负责把 Go 代码翻译成 SSA 形式),那么 src/cmd/compile/internal/ssa 就是整个编译器的“大脑”和“工厂”。 它负责执行编译器中最核心、最复杂的优化(Optimization)和代码转换(Code Transformation)工作。
具体职责包括以下四个维度:
1. 核心数据结构定义
该包定义了 SSA 形式的基础构件:
- Value:代表一个计算值(比如两个数相加的结果)。
- Block:代表一个基本块(由
if、for等控制流分割的代码块)。 - Op:代表具体的操作码(例如
AMD64ADDQ、OpArgIntReg等)。 - 你之前在
AMD64Ops.go看到的寄存器定义,最终都会在这里转化为 SSA 能够识别的Register结构体。
2. 执行几十轮的“优化通行证”(Optimization Passes)
这是
ssa 包最繁忙的部分。在生成最终汇编代码前,它会对代码进行反复优化,确保运行速度最快。常见的 Pass 包括:- 死代码消除 (Dead Code Elimination):删掉永远不会运行的代码。
- 常量折叠 (Constant Folding):把
1 + 2直接变成3。 - 公用表达式消除 (CSE):如果计算过两次同样的值,就只算一次。
- Nil Check Elimination:根据逻辑推断,去掉多余的指针空检查。
3. 寄存器分配(Register Allocation)—— 与ABI 密切相关
这是将“抽象变量”变成“物理寄存器”的关键阶段:
- 逻辑变量 vs 物理寄存器:在
ssagen阶段,参数可能只是被称为“参数1”。而在ssa包的regalloc.go阶段,它会根据你查到的 AX, BX, CX... 顺序,强行将这些变量绑定到物理硬件上。 - 溢出处理 (Spilling):如果函数里的局部变量太多,这 9 个寄存器不够用了,
ssa负责决定把哪些变量暂时存回内存(栈)里,哪些留在寄存器里。
4. 降级与代码生成(Lowering)
这是将通用操作转换为机器特定指令的过程:
- 例如,将通用的“加法”操作(
OpAdd64)根据目标架构降级为 Intel 的ADDQ。 - 它会读取
AMD64Ops.go等文件生成的规则,确保生成的代码符合 Intel 64 位 CPU 的口味。
总结:它们之间的分工
| 包名 | 角色 | 重点任务 |
| C:\Go\src\cmd\compile\internal\ssagen目录 | 指挥官 (Frontend) | 决定 ABI 规则,把 AST 拆解为 SSA 初始节点,分配参数寄存器。 |
| C:\Go\src\cmd\compile\internal\ssa目录 | 工厂 (Backend) | 执行死代码消除、寄存器着色、指令降级,真正把变量放进 AX, BX 等寄存器中。 |
| C:\Go\src\cmd\compile\internal\ssa\gen目录 | 依据不同架构产生Opgen.go文件,提供编译器调用 Intel架构依据AMD64Ops.go来产生Opgen.go文件 |
一句话总结:
ssagen 制定了参数应该进哪 9 个寄存器的计划,而 ssa 包负责在复杂的函数逻辑中执行这个计划,并管理这些寄存器在函数运行过程中的生老病死。六、
cmd/compile/internal/ssa/gen 包下的代码生成工具在 Go 1.17.1 的源码中,
cmd/compile/internal/ssa/gen 包下的代码生成工具并不是一个单一的文件,而是一个由多个架构定义文件组成的生成系统。
这个工具的“启动引擎”和“核心逻辑”分别由以下文件负责:
1. 启动引擎 (执行入口)
当你运行 go run ... 时,它实际上是运行了该包下的:
src/cmd/compile/internal/ssa/gen/main.go 这个
main.go 包含了 main 函数,它负责:- 调用各个架构(AMD64, ARM64 等)的定义,例如AMD64Ops.go。
- 将定义好的寄存器、操作码(Op)渲染成具体的 Go 代码。
- 生成最终的 C:\Go\src\cmd\compile\internal\ssa\
opGen.go(位于ssa目录下,提供编译器调用)。
查看文件,你会看到里面有 // Code generated from gen/*Ops.go; DO NOT EDIT. (*.Ops.go 表示根据不同架构进行操作,比如AMD64架构,则是 AMD64Ops.go 负责)
2. 架构定义文件 (数据源)
针对 Intel (AMD64) 架构,逻辑定义在:
src/cmd/compile/internal/ssa/gen/AMD64Ops.go- 它的作用:它像一张“配置清单”,里面人工维护了
regNamesAMD64(包含你看到的 AX, BX, CX...R11)。 - 它的逻辑:它定义了哪些寄存器可以用于哪些操作,以及寄存器的物理属性。
3. 生成过程的“粘合剂”
src/cmd/compile/internal/ssa/gen/generic.go这个文件定义了 SSA 抽象层的通用操作(如
Add64),所有的架构定义(如 AMD64Ops.go)都会参考并实现这些通用操作。opGen.go 可以看到变成了 OpAdd64
如何运行这个工具?
在 Go 1.17.1 的源码目录下,你可以通过以下方式手动触发生成流程:
当然Go 编译器的开发者为了防止在常规编译(如
注意,如果在 CMD 下执行失败,请检查是否漏掉了某个
同时也会更新 C:\Go\src\cmd\compile\internal\ssa\rewrite*.go 文件
go build 整个项目)时误运行这些工具脚本,通常会在这些生成工具的文件头部加上C:\Go\src\cmd\compile\internal\ssa\gen\AMD64Ops.go 头部会有//go:build ignore
所以你可以通过下面的,来执行
C:\Go\src\cmd\compile\internal\ssa\gen>go run main.go AMD64Ops.go ARM64Ops.go ARMOps.go MIPS64Ops.go MIPSOps.go S390XOps.go RISCV64Ops.go WasmOps.go genericOps.go dec64Ops.go decOps.go PPC64Ops.go 386Ops.go rulegen.go
*Ops.go 文件, 执行完毕后,你会发现上一级目录(src/cmd/compile/internal/ssa/)中的 opGen.go 被更新了。同时也会更新 C:\Go\src\cmd\compile\internal\ssa\rewrite*.go 文件
总结:关系链路图
- 用户修改:C:\Go\src\cmd\compile\internal\
gen\AMD64Ops.go(修改那 9 个寄存器的顺序)。
这个文件是寄存器名单的所在地,也是指令映射的所在地,在这个文件中你可以修改9个寄存器的顺序。
main.go文件在执行生成时,会读取这个文件中定义的变量。
它的产物就是 生成src/cmd/compile/internal/ssa/opGen.go 文件。src/cmd/compile/internal/amd64/ssa.go中的InitConfig最终读取opGen.go内这些生成好的数据。 - 运行工具:C:\Go\src\cmd\compile\internal\
gen\main.go(读取修改)。 - 产出结果:C:\Go\src\cmd\compile\internal\
ssa\opGen.go(生成包含regNamesAMD64数组的机器代码)。 - 编译器使用:
amd64/ssa.go的InitConfig引用ssa.RegNamesAMD64。
关键点: 如果你想理解这个结构,你会发现
main.go 是控制台,而 AMD64Ops.go 是具体的业务逻辑插件。 生成了新的
opGen.go, 那么你已经完成了对 Go 编译器“设计图纸”的底层修改。七、验证过程
如果你想亲自验证这 9 个寄存器的顺序,你可以尝试在
gen/AMD64Ops.go 中:
- 找到
regNamesAMD64数组。 - 把
"AX"和"BX"的位置互换。 - 在
gen目录下执行go run ...。 - 重新编译 Go 编译器(利用源码包里面的 make.bat)。
如果要在 Go 1.17.1 中通过编译器打印出寄存器分配(RegAlloc)的具体顺序,我们可以利用GOSSAFUNC环境变量。这将生成一个 HTML 文件,展示编译器从高级代码到最终寄存器绑定的每一个阶段。
创建一个简单的文件call.go,编写一个拥有多个参数的函数:
package main //go:noinline func manyArgs(a, b, c, d, e, f, g, h, i int) int { return a + b + c + d + e + f + g + h + i } func main() { manyArgs(1, 2, 3, 4, 5, 6, 7, 8, 9) }执行带追踪的编译
在终端运行以下命令(适用于 Linux/macOS,Windows 请使用set设置环境变量):
执行以下命令:C:\Go\src\myProject2\buffer>set GOSSAFUNC=manyArgs C:\Go\src\myProject2\buffer>go tool compile call.go dumped SSA to C:\Go\src\myProject2\buffer\ssa.html //在浏览器中打开即可
在
src/cmd/compile/internal/ssa/gen/AMD64Ops.go 中,init 函数扮演着 “编译器架构说明书”注册员 的角色。
它是整个代码生成系统的逻辑配置中心。其核心作用如下:
1. 注册寄存器列表 (Registration of Registers)
你在
AMD64Ops.go 中看到的 regNamesAMD64 等变量,就是在这个 init 函数里被“激活”的。- 它定义了物理寄存器的名字、在编译器内部的唯一编号(ID)以及它们的顺序。
- ABIInternal 关联:正因为
init函数将 AX, BX, CX... 按此顺序注册,后续生成的 C:\Go\src\cmd\compile\internal\ssa\opGen.go 才会严格保留这个索引。
2. 定义指令语义 (Instruction Semantics)
init 函数会将该文件定义的 AMD64ops(指令列表)和 AMD64blocks(控制流块)注入到生成器的全局配置中。- 它告诉编译器:Intel 的
ADDQ指令需要两个操作数,结果存回第一个操作数。 - 它还规定了每条指令可以使用哪些寄存器。例如,某些指令可能被限制只能使用
AX,这些约束都在init里完成绑定。
3. 建立 SSA 操作与机器指令的映射
init 函数定义了 Rewrite Rules(重写规则) 的元数据。- 它规定了编译器如何将抽象的
OpAdd64(跨架构通用加法)转化为具体的AMD64ADDQ。 - 一等公民的体现:在映射过程中,它会标记哪些指令的结果应该优先放进那 9 个通用寄存器中。
4. 驱动代码生成引擎
由于这个文件带了
//go:build ignore 且在 gen 包下,当你在执行 go run main.go ... 时:- Go 运行时会先执行所有被加载文件的
init函数。 AMD64Ops.go的init把 AMD64 的所有寄存器和指令数据填入gen包的全局 Map 中。main.go随后读取这些 Map,利用 模板(Template) 批量吐出opGen.go。
5.在
对于 9个通用寄存器,
比如: ADDQ指令:
AMD64Ops.go 的 init 函数中,regInfo 结构体是 寄存器约束(Register Constraints) 的核心定义对于 9个通用寄存器,
regInfo 决定了指令在运行时如何挑选这些寄存器。var ( ... gp21sp = regInfo{
inputs: []regMask{gpsp, gp}, //两个输入参数,一必须是通用寄存器(gp)/SP ,另一个是gp通过寄存器
outputs: gponly} // // 输出结果,必须是通用寄存器(gp)
...
)
{name: "ADDQ", argLength: 2, reg: gp21sp, asm: "ADDQ", commutative: true, clobberFlags: true},
//这里的gp21sp就是一个regInfo变量, 而 gp 是 通用寄存器掩码 , 在 1.17.1 中,它通过位运算包含了:AX CX DX BX BP SI DI R8 R9 R10 R11 R12 R13 R15 一共14个寄存器
总结:为什么改了这里顺序就变了?
因为
真正的“选出来的9个寄存器”是你在
main.go 只是一个复读机,它并不关心谁是“哪些寄存器是否按照ABI标准”。真正的“选出来的9个寄存器”是你在
AMD64Ops.go 的 init 流程中提供的那个字符串数组。你改了数组顺序,init 提交给生成引擎的数据顺序就变了,最终生成的物理编号也就变了。九、AMD64Ops.go 中的archs 说明
...
archs = append(archs, arch{ name: "AMD64", pkg: "cmd/internal/obj/x86", //x86使用哪个包 genfile: "../../amd64/ssa.go", // genSIMDfile: "../../amd64/simdssa.go", ops: append(AMD64ops, simdAMD64Ops(v11, v21, v2k, vkv, v2kv, v2kk, v31, v3kv, vgpv, vgp, vfpv, vfpkv, w11, w21, w2k, wkw, w2kw, w2kk, w31, w3kw, wgpw, wgp, wfpw, wfpkw, wkwload, v21load, v31load, v11load, w21load, w31load, w2kload, w2kwload, w11load, w3kwload, w2kkload, v31x0AtIn2)...), // AMD64ops, blocks: AMD64blocks, regnames: regNamesAMD64, ParamIntRegNames: "AX BX CX DI SI R8 R9 R10 R11", //初始化的9个寄存器 ParamFloatRegNames: "X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14", gpregmask: gp, fpregmask: fp, specialregmask: mask, framepointerreg: int8(num["BP"]), linkreg: -1, // not used })
...
同时参考
a.out.go 、opGen.go和AMD64Ops.go之间的联系(go底层分析) - jinzi - 博客园
go lang的编译 - jinzi - 博客园

浙公网安备 33010602011771号