9 个通用寄存器的分配顺序(ABIInternal 寄存器分配)

在 Go 1.17.1 的源码中,AMD64 架构下这 9 个通用寄存器的分配顺序(Assignment Order)主要定义在以下文件中:
  C:\Go\src\cmd\compile\internal\ssa\opGen.go(此文件由AMD64Ops.go,根据同目录下的main.go工具生成) 中
一、    为什么是这个顺序?
          这个顺序并非随机选择,而是基于以下考量:
  1. ABI 兼容性参考:部分参考了标准的 SysV ABI(Linux 常用),但为了 Go 语言的特殊需求(如高效的垃圾回收和快速栈扩容)进行了调整。
  2. 避免特殊寄存器:避开了 R14(用于存放当前的 g 指针)和 R15(常用于内部辅助)。
  3. 指令编码优化:RAXRBXRCX 等传统寄存器在某些汇编指令中的编码长度更短。
 
二、registersAMD64
      C:\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 脚本会遍历这个字符串列表,
      按顺序为它们分配 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 开始处理一个函数时,它会执行以下操作: 
  1. 分析函数签名:统计有多少个参数,每个参数是什么类型。
  2. 分配槽位 (Assign Slots):调用 abi 逻辑,将参数依次放进那 9 个寄存器(AX, BX, CX...)中。
  3. 生成 SSA 节点:在转换过程中,它会生成 OpArgIntRegOpArgFloatReg 指令。这些指令明确告诉后续的优化器:“这个变量是从 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 个参数“踢”到栈上的代码逻辑?

五、src/cmd/compile/internal/ssa 就是整个编译器的“大脑”和“工厂”

 在 Go 1.17.1 的架构中,如果说 ssagen 是“翻译官”(负责把 Go 代码翻译成 SSA 形式),那么 src/cmd/compile/internal/ssa 就是整个编译器的“大脑”和“工厂”。
 它负责执行编译器中最核心、最复杂的优化(Optimization)和代码转换(Code Transformation)工作。
 
 具体职责包括以下四个维度:
 
1. 核心数据结构定义
    该包定义了 SSA 形式的基础构件:
  • Value:代表一个计算值(比如两个数相加的结果)。
  • Block:代表一个基本块(由 iffor 等控制流分割的代码块)。
  • Op:代表具体的操作码(例如 AMD64ADDQOpArgIntReg 等)。
  • 你之前在 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 编译器的开发者为了防止在常规编译(如 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
 注意,如果在 CMD 下执行失败,请检查是否漏掉了某个 *Ops.go 文件, 执行完毕后,你会发现上一级目录(src/cmd/compile/internal/ssa/)中的 opGen.go 被更新了。
  同时也会更新 C:\Go\src\cmd\compile\internal\ssa\rewrite*.go 文件
 
   总结:关系链路图
  1. 用户修改: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内这些生成好的数据。
  2. 运行工具:C:\Go\src\cmd\compile\internal\gen\main.go(读取修改)。
  3. 产出结果:C:\Go\src\cmd\compile\internal\ssa\opGen.go(生成包含 regNamesAMD64 数组的机器代码)。
  4. 编译器使用: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  //在浏览器中打开即可
八、AMD64Ops.go 里面的init作用
在 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 ... 时:
  1. Go 运行时会先执行所有被加载文件的 init 函数。
  2. AMD64Ops.go 的 init 把 AMD64 的所有寄存器和指令数据填入 gen 包的全局 Map 中。
  3. main.go 随后读取这些 Map,利用 模板(Template) 批量吐出 opGen.go
5.在 AMD64Ops.go 的 init 函数中,regInfo 结构体是 寄存器约束(Register Constraints) 的核心定义
   对于 9个通用寄存器,regInfo 决定了指令在运行时如何挑选这些寄存器。
var (
...
        gp21sp         = regInfo{
inputs: []regMask{gpsp, gp}, //两个输入参数,一必须是通用寄存器(gp)/SP ,另一个是gp通过寄存器
outputs: gponly} // // 输出结果,必须是通用寄存器(gp)
...
)
   比如: ADDQ指令:  
{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个寄存器



  总结:为什么改了这里顺序就变了?
    因为 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 - 博客园

 

 
posted @ 2026-01-15 08:10  jinzi  阅读(3)  评论(0)    收藏  举报