Go 专家(13):Go 汇编快速入门 —— Plan 9 汇编风格、阅读运行时代码
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 13 篇,接 Go vs C 内存模型对比。Go 的汇编不是 Intel 或 AT&T 风格,而是 Plan 9 风格。虽然日常开发不用写汇编,但阅读运行时代码(如runtime、crypto包)需要理解它。理解 Go 汇编,能真正理解 Go 的调用约定和寄存器。
本文要回答的问题
- Go 的 Plan 9 汇编和 Intel/AT&T 汇编有什么区别?
- 寄存器怎么命名?参数怎么传?返回值怎么返回?
- 怎么阅读 Go 运行时的汇编代码?
- 什么时候需要写 Go 汇编?
一、Plan 9 汇编风格概览
Go 的汇编器使用 Plan 9 风格,和 Intel 格式主要区别:
| 对比 | Intel(x86-64) | Plan 9(Go) |
|---|---|---|
| 操作数顺序 | dst, src | src, dst |
| 寄存器命名 | rax | AX |
| 立即数 | 0x1234 | $0x1234 |
| 内存寻址 | [rax+0x8] | 8(AX) |
| 间接跳转 | jmp [rax] | JMP (AX) |
| 函数调用 | call func | CALL func(SB) |
格式核心:opcode src, dst
二、寄存器命名
| Plan 9 寄存器 | Intel 寄存器 | 作用 |
|---|---|---|
AX | rax | 通用/返回值 |
BX | rbx | 通用 |
CX | rcx | 通用/计数器 |
DX | rdx | 通用 |
DI | rdi | 通用 |
SI | rsi | 通用 |
R8 - R15 | r8 - r15 | 通用 |
Go 特有伪寄存器:
| 伪寄存器 | 作用 |
|---|---|
SB | 静态基址(Static Base),全局符号 |
FP | 帧指针(Frame Pointer),参数和返回值 |
SP | 栈指针(Stack Pointer),局部变量 |
PC | 程序计数器(Program Counter) |
重要: Go 汇编里的 SP 是伪寄存器,指向栈顶(局部变量)。真正的硬件 SP 是 RSP。
三、Go 调用约定
Go 1.17+ 使用 基于寄存器的调用约定(ABI Internal):
- 参数:通过寄存器传递,最多 9 个参数
- 返回值:通过寄存器返回,最多 9 个返回值
- 超出寄存器的参数:通过栈传递
go
// Go 函数
func add(a, b int) int
// 汇编调用约定:
// 参数 a → AX
// 参数 b → BX
// 返回值 → AX
// 栈上只需要局部变量四、汇编代码结构
asm
// example.s
TEXT ·add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX // 参数 a 在 FP+0
MOVQ b+8(FP), BX // 参数 b 在 FP+8
ADDQ BX, AX // AX = a + b
MOVQ AX, ret+16(FP) // 返回值在 FP+16
RETTEXT ·add(SB):定义的函数NOSPLIT:不需要栈分裂$0-24:栈帧大小 0,参数+返回值共 24 字节(3 个 int64)
五、阅读运行时汇编
Go 运行时大量使用汇编,比如 runtime.memmove:
asm
// runtime/memmove_amd64.s
TEXT runtime·memmove(SB), NOSPLIT, $0-24
MOVQ to+0(FP), DI
MOVQ from+8(FP), SI
MOVQ n+16(FP), BX
// 根据大小选择不同的拷贝策略
CMPQ BX, $16
JBE copy_16
CMPQ BX, $128
JBE copy_128
// 大拷贝用 MOVQ 循环
// ...怎么读: 从 go tool compile -S 输出看,或者用 go tool objdump 反汇编。
六、查看 Go 汇编
bash
# 查看 Go 函数的汇编
go tool compile -S main.go
# 反汇编已有二进制
go tool objdump -s "main\.add" main.exe
# 查看运行时汇编
go tool objdump -s "runtime\.memmove" /usr/local/go/pkg/linux_amd64/runtime.a七、什么时候需要写 Go 汇编
- 性能极致优化:某些算法汇编比 Go 快 2-3 倍
- 运行时和标准库:runtime、crypto、math 等包
- 原子操作:某些平台需要汇编实现
- 调试:反汇编看编译器优化效果
日常开发不需要写汇编,但需要能读汇编,特别是调试性能问题。
八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 把 Plan 9 的 SP 当成硬件 SP | 栈操作错误 | 伪 SP 是局部变量,硬件 SP 是 RSP |
忘记 NOSPLIT | 大栈帧导致栈复制 | 不确定栈大小就加 NOSPLIT |
| 参数偏移计算错误 | 访问到错误参数 | 从 FP 偏移正确计算 |
| 用 Intel 格式写 | 汇编器报错 | 用 Plan 9 格式 |
相关与延伸
下一篇:Go 编译器优化 —— SSA 中间表示、内联、边界检查消除;Go 汇编的寄存器调用约定,是 Go 1.17+ 的 ABI Internal,之前是栈传递。
一句话总结
Go 汇编快速入门:Plan 9 风格,opcode src, dst;寄存器:AX/BX/CX/DX/DI/SI + R8-R15,伪寄存器 SB/FP/SP/PC;Go 1.17+ 用寄存器传参(最多 9 个),超出走栈;TEXT ·name(SB), NOSPLIT, $frame-args 定义函数,FP 访问参数和返回值;日常不用写,但读运行时汇编能帮你理解底层。