Go汇编函数名必须带·前缀(如·Add),因·是包作用域分隔符,漏掉则go build忽略该符号;需与Go文件中func Add(a,b uint64) uint64声明完全匹配,且TEXT ·Add(SB), NOSPLIT, $0-16中$0-16表示栈帧0字节、参数+返回值共16字节,FP为参数区起始逻辑偏移,a+0(FP)、b+8(FP)、c+16(FP)严格对应布局。
go 汇编里写一个 add 函数,不是“把 c 风格汇编抄过来就行”,关键在于参数传递约定和伪寄存器的定位必须严格匹配 go 的调用规范,否则函数根本不会被链接调用。
为什么 Go 汇编函数名必须带点前缀(如 ·Add)
Go 的汇编符号命名不是自由的。·Add 中的 · 是包作用域分隔符,表示该符号属于当前包(而非全局 C 符号)。如果你写成 Add(SB) 而漏掉 ·,go build 会完全忽略这个汇编实现——它只认 ·Add 这种格式的符号,且必须与 Go 源文件中声明的函数名、签名完全一致。
- Go 文件中必须存在
func Add(a, b uint64) uint64声明(哪怕函数体为空),否则编译器不会去查找同名的.s文件 - 汇编文件中的
TEXT ·Add(SB), NOSPLIT, $0-16中,$0-16表示栈帧大小为 0 字节,参数+返回值共占用 16 字节(两个uint64输入 + 一个uint64输出) -
NOSPLIT表示禁止栈分裂,因为该函数不调用其他函数、不分配堆内存,也不访问可能触发 GC 的数据
a+0(FP) 和 c+16(FP) 中的 FP 到底指哪?
FP 是 Go 汇编里的伪寄存器,不代表真实硬件寄存器,而是编译器约定的“函数参数起始位置”。它固定指向调用者在栈上为被调函数准备的参数区首地址。所以:
-
a+0(FP):第一个参数a的起始偏移是 +0 字节 -
b+8(FP):第二个参数b是uint64,占 8 字节,所以从 +8 开始 -
c+16(FP):返回值也是uint64,紧接在参数之后,从 +16 开始(0+8+8) - 注意:这不是内存绝对地址,也不是相对于栈顶(
SP)的偏移;它是相对于调用者栈帧中参数区起点的逻辑偏移
为什么 MOVQ 后直接 ADDQ 就能工作,不用 PUSH/POP?
因为 Go 汇编默认使用寄存器传参的简化模型(x86-64 ABI 下也如此),只要你不显式修改 callee-saved 寄存器(如 RBX, RBP, R12–R15),就不需要保存恢复。上面的 Add 示例只用了 AX、BX——它们是 caller-saved 寄存器,调用方本就预期其内容会被覆盖。
- 你不能在函数里擅自改
RBX却不PUSH/POP,否则上层 Go 代码的局部变量或循环变量可能出错 -
RET指令由 Go 运行时接管,它知道如何从当前栈帧正确返回,不需要你手动调整SP(除非你显式分配了栈空间) - 如果函数需要局部变量(比如临时数组),就得用
$N给TEXT指令指定栈帧大小,并用SP或FP偏移来访问,这时才涉及真正的栈操作
调试时看不到你的汇编代码?检查这三点
常见现象:写了 add_amd64.s,但 go build -gcflags="-S" main.go 输出的仍是 Go 编译器生成的汇编,而不是你手写的。
- 确保
.s文件名后缀是_amd64.s(或_arm64.s),且与目标平台匹配;混用会导致静默跳过 - 确保 Go 文件中函数签名完全一致:类型、顺序、是否导出(
Add首字母大写才导出)、返回值个数都不能差 - 确保没有重复定义:如果 Go 文件里写了
func Add(...) { return a + b },编译器会直接内联或生成自己的汇编,完全绕过你的.s文件
最易被忽略的一点:Go 汇编不是“独立运行”的机器码,它全程依赖 Go 的调用协议和栈布局约定。哪怕只错一个字节偏移(比如把 c+16(FP) 写成 c+8(FP)),结果就会写到参数 b 的内存上,导致返回值污染或崩溃——这种错误不会报编译错,只会在运行时悄悄出问题。


















