直接call go函数会崩溃,因为go使用分段栈、栈分裂检查和特殊调用约定,汇编代码若不遵循会导致栈溢出或panic;必须通过runtime·cgocall安全调用,并注意符号名、参数类型及//go:nosplit等约束。

为什么直接 call Go 函数会崩溃
Plan 9 汇编(asm)里用 CALL 直接跳转到 Go 函数地址,几乎必然触发栈溢出或 panic。Go 的函数调用约定和栈管理与传统 C 不同:它使用分段栈(goroutine stack)、有栈分裂检查、参数通过栈传递且需对齐、还有隐式 runtime·morestack 调用链。汇编代码若不遵循这套约定,runtime 在检查栈边界时会发现“非法帧”,直接抛 fatal error: stack split at bad time 或 segfault。
必须通过 runtime·cgocall 包装 Go 函数
Go 运行时提供 runtime·cgocall 作为安全入口,它负责:保存当前 goroutine 栈状态、切换到足够大的系统栈、调用目标函数、恢复上下文。你的汇编函数不能直接 CALL Go 符号,而要把它当作一个 C 风格函数指针传给 runtime·cgocall。
实操要点:
- Go 函数签名必须是
func(uintptr, uintptr) uintptr或类似纯整数参数/返回类型(避免结构体、接口、指针逃逸) - 在 Go 文件中导出该函数,并用
//go:nosplit注释(防止 runtime 插入栈分裂检查) - 汇编中先将 Go 函数地址加载到寄存器(如
AX),再把参数压栈(Plan 9 栈向下增长,按从右到左顺序) - 最后调用
CALL runtime·cgocall(SB),而非直接 CALL 你的函数
示例(Go 端):
//go:nosplit
func mygo(x, y uintptr) uintptr {
return x + y
}
对应汇编(add.s):
TEXT ·add(SB), NOSPLIT, $0
MOVQ $mygo(SB), AX
MOVQ $123, BX
MOVQ $456, CX
PUSHQ CX
PUSHQ BX
CALL runtime·cgocall(SB)
POPQ AX
POPQ AX
RET
符号名必须加 runtime· 前缀且大小写敏感
Plan 9 汇编中引用 Go 符号,不能写 mygo(SB),而必须写 mygo(SB) —— 注意不是 runtime.mygo,而是直接用函数名;但调用 runtime·cgocall 时,点号是必需的(· 是 Unicode U+00B7,不是 ASCII 点)。大小写也严格匹配:Go 中定义的是 mygo,汇编里就不能写成 MyGo。
常见错误现象:
-
undefined: "mygo":链接时报错,说明符号名拼错或未导出 -
relocation target mygo not defined:Go 函数没加export注释,或未被任何 Go 代码引用导致 dead code elimination - 运行时 panic “invalid pointer found on stack”:参数未按
uintptr对齐,或传了 Go 指针(如*int)而非整数
性能开销大,仅用于极少数关键路径
每次调用 runtime·cgocall 至少涉及一次栈切换、两次函数跳转、以及 runtime 的上下文保存/恢复。它比普通 Go 函数调用慢 5–10 倍,比内联汇编慢两个数量级。不要为了“看起来快”而用它包装简单逻辑。
适用场景很窄:
- 需要从手写汇编中触发 GC 安全点(比如长循环里定期调用
runtime·osyield) - 实现 syscall 入口桥接(如
syscall·Syscall底层封装) - 调试或诊断工具中临时注入行为(如 hook malloc)
真正要优化的代码,优先考虑 Go 编译器内联、//go:register(1.22+)、或用 unsafe + reflect 绕过部分检查——而不是引入 Plan 9 汇编调用层。











