如何在 Go 中编写汇编代码(Plan9 汇编基础)?
在Go中编写Plan9汇编需遵循架构后缀等硬性规范,函数用TEXT定义并配合Go签名。DATA与GLOBL顺序不可颠倒,错用伪寄存器SP、FP、SB会导致崩溃,涉及指针时必须添加FUNCDATA和PCDATA。
在Go里写汇编,目的非常单纯——优化。不是什么炫技行为,而是当你看go tool compile -S的输出,发现关键路径上出现了寄存器溢出、栈帧过大、或者函数调用开销高到无法接受的时候,才值得动手介入。Plan9 汇编不是一门独立的语言,它必须和 Go 源码共存、共享符号、服从 GC 规则。跳过这一点,代码能编译通过,但 runtime 会在某个不经意的时刻崩溃给你看。
如何让 Go 编译器识别并链接你的汇编文件
先讲一个很容易踩的坑:Go 对汇编文件的命名有硬性要求,文件名必须带架构后缀,比如 add_amd64.s。函数名不能用包路径前缀,要用 ·Add 而不是 math·Add。更关键的是,同包的 Go 文件里必须声明对应的函数签名——哪怕是空实现也行,否则 go build 会直接报 undefined: Add。
具体来说,有几点需要记牢:
- 汇编文件要和 Go 源码放在同一个目录、同一个包下面,文件名格式是
xxx_$(GOARCH).s,比如utils_amd64.s。 - 函数定义用
TEXT ·Add(SB), NOSPLIT, $0。这里的·表示包级符号,SB是静态基址伪寄存器,NOSPLIT则告诉编译器不要插入栈分裂检查——避免递归调用时出现 panic。 - 如果函数有参数或返回值,必须在 Go 文件中给出完整签名,比如
func Add(a, b int) int,否则链接阶段就会失败。
DATA 和 GLOBL 的顺序与语义不能颠倒
这里有个常见的误解:GLOBL 不是“声明变量”,它的作用其实是向链接器宣告——这个符号是全局可见的,需要分配存储空间。真正写入初始值的是 DATA。如果两者顺序搞错,或者类型标记用错,后果很严重:GC 扫描会崩溃,或者干脆读出一堆垃圾值。
GLOBL必须紧跟在所有对应的DATA指令之后,而且size参数必须等于所有DATA写入的字节数之和。- 标记
NOPTR表示该变量不含指针(比如int、uint64),GC 会跳过扫描。如果误标,可能造成对象被提前回收。 - 只读数据应该优先用
RODATA标志(比如字符串常量)。它隐式带了NOPTR,而且会被加载到内存的只读段,运行时如果有人试图修改,直接 segfault。
SP、FP、SB 这三个伪寄存器用错就直接 crash
Plan9 汇里面没有 push/pop 指令,栈操作全靠 ADDQ/SUBQ 修改 SP。而 FP 和 SB 是只读的伪寄存器,如果你试图写入它们,链接器会报错,或者地址直接错乱。
symbol+8(FP)用来访问第二个参数(FP指向第一个参数的起始位置)。但注意,FP本身不可写,也不能参与算术运算。0(SP)指的是硬件栈顶(也就是当前RSP寄存器的值),buf+8(SP)是局部变量的偏移。两者混用极易越界。symbol(SB)是全局符号基址,用于访问DATA/GLOBL定义的变量。如果写成symbol(SP),那就变成了栈上的随机地址,结果可想而知。
这里有个坑,很多人容易忽略:所有涉及指针的汇编函数——比如返回 *int 或者接收 []byte——必须显式添加 FUNCDATA 和 PCDATA 指令,告诉 GC 哪些栈偏移处存活着指针。漏掉这个,程序可能跑几分钟才 panic,而且堆栈信息一片空白,完全无从排查。


































