1. 为什么 Go 需要汇编
Go 在以下场景允许直接嵌入汇编:
- 性能热点:手写 SIMD 指令、避免编译器优化不足
- 系统调用:部分 runtime 代码(如
runtime·sys_linux_amd64.s)直接发起 syscall - 引导/启动:
rt0_linux_amd64.s等启动代码必须在汇编层面完成栈和寄存器初始化 - 密码学:
crypto/aes,crypto/sha256等标准库大量使用汇编加速
Go 使用 Plan 9 汇编语法,与 AT&T / Intel 语法都不同。
2. Plan 9 汇编基础
2.1 操作数顺序
Plan 9 汇编是源操作数在前,目标操作数在后(类似 AT&T,但无 % / $ 前缀):
| |
2.2 寄存器命名
| 通用寄存器 | Plan 9 名 | Intel 名 |
|---|---|---|
| AX / EAX / AX:BX | AX | RAX |
| BX | BX | RBX |
| CX | CX | RCX |
| DX | DX | RDX |
| Stack Pointer | SP | RSP |
| Base Pointer | BP | RBP |
| R8–R15 | R8–R15 | R8–R15 |
| X0–X15 (SSE) | X0–X15 | XMM0–XMM15 |
2.3 伪寄存器(核心概念)
Go 汇编定义了四个伪寄存器,没有物理硬件对应:
| 伪寄存器 | 含义 | 示例 |
|---|---|---|
| SB | Static Base — 全局符号基址 | TEXT ·Sum(SB) 声明全局函数 |
| FP | Frame Pointer — 指向参数和返回值区域的起始 | x+0(FP) 访问第一个参数 |
| SP | 栈指针 — 指向当前 goroutine 栈顶 | MOVQ AX, local+0(SP) 局部变量 |
| PC | Program Counter — 指令地址 | 用于跳转表和 CALL |
重点:
FP是函数参数/返回值的入口地址,硬件寄存器SP是RSP,而汇编中的SP指的是局部变量区的基址。两者是不同的。
2.4 指令后缀
| 后缀 | 操作数大小 | 对应 C 类型 |
|---|---|---|
B | 1 字节 | byte / uint8 |
W | 2 字节 | uint16 |
L | 4 字节 | uint32 |
Q | 8 字节 | uint64 / int (64-bit) |
3. 函数声明与栈帧
3.1 TEXT 指令
| |
·函数名:·(U+00B7)是包分隔符,等价于 Go 的.SB:使该符号全局可见,Go 链接器可找到它$N-M:N:局部变量所需的栈空间(0 表示不需要)M:参数 + 返回值占用的总字节数
3.2 栈布局(amd64)
| |
关键:
FP指向参数区的起始,SP(伪寄存器)指向局部变量区的起始。返回值存放在参数区的最末尾,紧接在参数之后。
3.3 参数与返回值的偏移计算
对于 func Sum(x, y int) int(amd64,int = 8 字节):
| |
因此 TEXT 指令的帧大小应为 $0-24(0 字节局部变量 + 24 字节参数/返回值空间)。
4. 实例解析
4.1 Go 侧 (main.go)
| |
Sum只有签名没有实现体- Go 编译器看到这样的声明会自动查找同名汇编符号
- 注意:汇编函数名大小写敏感,
Sum大写表示导出(exported)
4.2 汇编侧 (add.s)
| |
逐行解释:
MOVQ x+0(FP), AX— 从栈上偏移 0 处取出参数x,放入AXMOVQ y+8(FP), BX— 从栈上偏移 8 处取出参数y,放入BXADDQ BX, AX—AX += BXMOVQ AX, ret+16(FP)— 将AX的值写入返回值槽位RET— 函数返回,调用者从ret+16(FP)处读取结果
4.3 编译与运行
| |
5. 调试与工具
5.1 验证栈帧大小
编译时设置 -asan 或者直接检查:
| |
5.2 查看调用约定兼容性
| |
Go 1.17 起引入了基于寄存器的调用约定(ABIInternal),参数和返回值优先通过寄存器传递而非栈。但手写的汇编
.s文件默认仍使用栈传参(ABI0)。如果需要兼容 ABIInternal,需要在TEXT指令中指定。
6. 常见陷阱
6.1 帧大小标注不匹配
如果 TEXT 中声明的 $N-M 与实际使用的偏移量不一致,链接时可能不会报错但运行时会产生数据竞争或栈损坏。
当前仓库中 add.s 的 $0-8 应修正为 $0-24(amd64 下 int 为 8 字节,参数 + 返回值共 24 字节)。
6.2 符号命名
- Go 中使用
·(middle dot)分隔包名和函数名,不可用普通点. - 导出函数名(首字母大写)在汇编中同样需要大写
- 使用
"".Sum或·Sum均可(后者是缩写形式)
6.3 栈对齐
x86-64 ABI 要求栈在 CALL 前 16 字节对齐。如果手动调整栈指针,需要确保对齐。
6.4 寄存器保存
Go 汇编中,调用者需要保存 callee-saved 寄存器(BP、BX、R12–R15)。AX、CX、DX 等是调用者保存的 scratch 寄存器,可以随意使用。
6.5 Go 版本差异
| 版本 | 变化 |
|---|---|
| Go 1.17 | 引入 ABIInternal(寄存器传参),汇编默认仍使用 ABI0 |
| Go 1.21+ | 部分标准库汇编适配寄存器 ABI |
| Go 1.22+ | GOAMD64=v3 支持 AVX2 等新指令 |
7. 参考资源
- A Quick Guide to Go’s Assembler — Go 官方汇编快速指南
go tool compile -S xxx.go— 查看编译器生成的汇编go doc cmd/asm— 汇编器文档- 标准库
crypto/目录下大量.s文件可供学习