Go汇编入门
约 2019 字大约 7 分钟
goassembly
2025-04-27
概述
Go汇编使用Plan 9汇编语法,与主流的Intel/AT&T语法有显著差异。虽然日常Go开发很少需要直接编写汇编代码,但了解Go汇编对于理解Go运行时内部机制、性能调优和阅读标准库中的底层实现非常有帮助。本文将介绍Go汇编的基本语法、伪寄存器、函数声明、调用约定以及SIMD优化示例。
Plan 9 汇编语法特点
// Go汇编 vs Intel语法 vs AT&T语法
// Intel: MOV RAX, RBX (dst, src)
// AT&T: movq %rbx, %rax (src, dst,带%前缀)
// Plan 9: MOVQ BX, AX (src, dst,无%前缀,大写)关键区别:
- 操作数顺序:源在前,目的在后(与AT&T相同,与Intel相反)
- 寄存器名无前缀(不像AT&T的%前缀)
- 指令全大写
- 有四个伪寄存器(FP、SP、SB、PC)
四个伪寄存器
// 使用示例
// FP: 访问函数参数
// arg+0(FP) → 第一个参数
// arg+8(FP) → 第二个参数
// SP: 访问局部变量
// localvar-8(SP) → 第一个局部变量
// SB: 引用全局符号
// runtime·morestack(SB) → runtime包的morestack函数
// main·x(SB) → main包的全局变量x硬件SP vs 伪SP
注意:带符号名引用的SP是伪SP(如 x-8(SP)),不带符号名的SP是硬件SP(如 -8(SP))。
函数声明
// Go汇编中的函数声明
// TEXT 包名·函数名(SB), 标志位, $栈帧大小-参数大小
// 示例:func Add(a, b int64) int64
TEXT ·Add(SB), NOSPLIT, $0-24
// $0: 栈帧大小为0(没有局部变量)
// -24: 参数+返回值总大小(8+8+8=24字节)
MOVQ a+0(FP), AX // 第一个参数a → AX
ADDQ b+8(FP), AX // AX += 第二个参数b
MOVQ AX, ret+16(FP) // 结果写入返回值
RET
// 对应的Go声明(在.go文件中)
//go:nosplit
func Add(a, b int64) int64常用标志位
// NOSPLIT: 不插入栈检查序言(函数栈使用量必须在安全范围内)
TEXT ·SmallFunc(SB), NOSPLIT, $0-16
// NOFRAME: 没有栈帧(Go 1.?+)
TEXT ·Leaf(SB), NOSPLIT|NOFRAME, $0-8
// WRAPPER: 包装函数(影响traceback)
TEXT ·wrapper(SB), WRAPPER, $0-0
// 标志位在 textflag.h 中定义
#include "textflag.h"完整示例:向量点积
// dot_amd64.s
#include "textflag.h"
// func Dot(a, b []float64) float64
TEXT ·Dot(SB), NOSPLIT, $0-56
// 参数布局:
// a.data +0(FP) 8 bytes
// a.len +8(FP) 8 bytes
// a.cap +16(FP) 8 bytes
// b.data +24(FP) 8 bytes
// b.len +32(FP) 8 bytes
// b.cap +40(FP) 8 bytes
// ret +48(FP) 8 bytes
MOVQ a+0(FP), SI // SI = &a[0]
MOVQ b+24(FP), DI // DI = &b[0]
MOVQ a+8(FP), CX // CX = len(a)
XORPS X0, X0 // X0 = 0.0 (累加器)
TESTQ CX, CX
JZ done // if len == 0, return 0
loop:
MOVSD (SI), X1 // X1 = a[i]
MULSD (DI), X1 // X1 *= b[i]
ADDSD X1, X0 // X0 += X1
ADDQ $8, SI // SI += 8 (next float64)
ADDQ $8, DI // DI += 8
DECQ CX // CX--
JNZ loop // if CX != 0, continue
done:
MOVSD X0, ret+48(FP) // 写入返回值
RET// dot.go — Go声明
package simd
func Dot(a, b []float64) float64调用约定
Go 1.17之前(栈传参)
Go 1.17+(寄存器传参)
// Go 1.17引入了基于寄存器的调用约定
// 整数参数使用: AX, BX, CX, DI, SI, R8, R9, R10, R11
// 浮点参数使用: X0-X14
// 超过寄存器数量的参数溢出到栈上
// 寄存器调用约定带来约5-10%的性能提升SIMD 优化示例
AVX2 向量加法
// add_avx2_amd64.s
#include "textflag.h"
// func addAVX2(dst, a, b []float64)
TEXT ·addAVX2(SB), NOSPLIT, $0-72
MOVQ dst+0(FP), DI // DI = &dst[0]
MOVQ a+24(FP), SI // SI = &a[0]
MOVQ b+48(FP), DX // DX = &b[0]
MOVQ a+32(FP), CX // CX = len(a)
// 每次处理4个float64(AVX2: 256位 = 4*64位)
SHRQ $2, CX // CX /= 4
JZ tail
avx2_loop:
VMOVUPD (SI), Y0 // Y0 = a[i:i+4]
VADDPD (DX), Y0, Y1 // Y1 = Y0 + b[i:i+4]
VMOVUPD Y1, (DI) // dst[i:i+4] = Y1
ADDQ $32, SI // 4 * 8 bytes
ADDQ $32, DX
ADDQ $32, DI
DECQ CX
JNZ avx2_loop
VZEROUPPER // 清除AVX状态
tail:
// 处理剩余元素...
RET运行时特性检测
// 根据CPU特性选择不同实现
package simd
import "golang.org/x/sys/cpu"
func Add(dst, a, b []float64) {
if cpu.X86.HasAVX2 {
addAVX2(dst, a, b)
} else if cpu.X86.HasSSE2 {
addSSE2(dst, a, b)
} else {
addGeneric(dst, a, b)
}
}
func addGeneric(dst, a, b []float64) {
for i := range dst {
dst[i] = a[i] + b[i]
}
}
// 汇编声明
func addAVX2(dst, a, b []float64)
func addSSE2(dst, a, b []float64)查看Go编译器生成的汇编
# 查看Go代码对应的汇编
go tool compile -S main.go
# 使用objdump
go build -o app main.go
go tool objdump -s 'main.Add' app
# 使用gcflags查看优化信息
go build -gcflags='-S' main.go
# 查看特定函数的汇编(更友好)
go build -gcflags='-S -l' main.go 2>&1 | grep -A 50 '"".Add'
# 在线工具:godbolt.org 支持Go常用汇编指令
// 数据移动
MOVQ src, dst // 64位移动
MOVL src, dst // 32位移动
MOVW src, dst // 16位移动
MOVB src, dst // 8位移动
LEAQ src, dst // 加载有效地址
// 算术运算
ADDQ src, dst // dst += src
SUBQ src, dst // dst -= src
IMULQ src, dst // dst *= src
SHLQ $n, dst // dst <<= n
SHRQ $n, dst // dst >>= n
// 比较和跳转
CMPQ a, b // 比较a和b
TESTQ a, b // 按位AND测试
JZ label // 零则跳转
JNZ label // 非零则跳转
JL label // 小于则跳转
JG label // 大于则跳转
// 函数调用
CALL target // 调用函数
RET // 返回
// SIMD (SSE/AVX)
MOVSD src, dst // 移动标量double
ADDSD src, dst // 标量double加法
MULSD src, dst // 标量double乘法
MOVUPD src, dst // 移动打包double(未对齐)数据声明
// 全局变量
DATA symbol+offset(SB)/width, value
GLOBL symbol(SB), flags, $size
// 示例:全局字节数组
DATA myConst+0(SB)/8, $0x0102030405060708
GLOBL myConst(SB), RODATA, $8
// 字符串常量
DATA hello+0(SB)/5, $"hello"
GLOBL hello(SB), RODATA, $5实际应用场景
// Go汇编在标准库中的应用:
// 1. crypto/aes — AES-NI硬件加速
// 2. crypto/sha256 — SHA扩展指令
// 3. math/big — 大数运算
// 4. runtime/asm — goroutine调度、栈管理
// 5. internal/bytealg — 字符串/字节搜索
// 什么时候需要写Go汇编:
// 1. 使用SIMD指令优化热点循环
// 2. 使用CPU特定指令(AES-NI, CRC32C等)
// 3. 实现比编译器更好的优化
// 4. 理解和调试运行时行为总结
| 特性 | 说明 |
|---|---|
| 语法 | Plan 9风格,src在前dst在后 |
| 伪寄存器 | FP(参数)、SP(局部变量)、SB(全局)、PC(指令) |
| 函数声明 | TEXT ·Func(SB), 标志, $栈大小-参数大小 |
| 调用约定 | Go 1.17+寄存器传参 |
| SIMD | 直接使用SSE/AVX指令 |
| 调试 | go tool compile -S / go tool objdump |
Go汇编是一个专业的底层工具。大多数Go开发者不需要直接编写汇编,但理解其基本概念对于性能分析和理解Go运行时机制非常有价值。当确认某个热点函数需要汇编优化时,先用benchmark确认性能瓶颈,再考虑是否值得引入汇编的维护成本。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于