Golang汇编函数引发的panic问题分析
Golang汇编函数引发的panic问题分析
我遇到了一个小问题:从Golang汇编函数中调用的panic不能完全正常工作。
动机如下:
我需要为向量内积代码编写高度优化的代码,因此该代码应该检查两个向量的长度是否相等,并通过panic报告错误。
当我用Golang函数包装汇编代码来处理panic时,入口和退出过程的开销(如runtime.morestack_noctxt等)导致计算速度变慢(分析器报告它们花费了超过一秒的时间…)。
为了展示我的问题,我编写了一个概念验证代码。 首先,我编写了以下汇编代码:
#include "funcdata.h"
#include "textflag.h"
// func panicPanic()
TEXT ·panicPanic(SB), NOSPLIT, $0-0
SUBQ $8, SP
MOVQ BP, 0(SP)
LEAQ 0(SP), BP
MOVQ ·panicMessage(SB), AX
MOVQ ·panicMessage+8(SB), CX
TESTQ AX, AX
JEQ panic1
MOVQ 8(AX), AX
panic1:
MOVQ AX, (SP)
MOVQ CX, 8(SP)
CALL runtime·gopanic(SB)
然后,我在Golang 1.10.3和macOS 10.13.5上构建并运行以下Golang代码:
package main
import "fmt"
var panicMessage = fmt.Errorf("Panic panic")
func panicPanic()
func tryPanic() {
defer func() {
fmt.Printf("recovered: %s\n", recover())
}()
panicPanic()
}
func main() {
tryPanic()
panicPanic()
}
我发现recover按预期工作,但是在panicPanic中调用的runtime.gopanic触发的回溯没有按预期工作:
recovered: Panic panic
panic: Panic panic
goroutine 1 [running]:
runtime: unexpected return pc for runtime.gopanic called from 0x108f261
stack: frame={sp:0xc420045ec8, fp:0xc420045f68} stack=[0xc420044000,0xc420046000)
000000c420045dc8: 00000000010489a2 <runtime.writeErr+66> 0000000000000002
000000c420045dd8: 00000000010c0147 0000000000000001
000000c420045de8: 000000c400000001 000000c420045e28
000000c420045df8: 0000000001026b08 <runtime.gwrite+280> 00000000010c0147
000000c420045e08: 0000000000000001 0000000000000001
000000c420045e18: 0000000001026b08 <runtime.gwrite+280> 00000000010c0748
000000c420045e28: 000000c420045e78 00000000010272bd <runtime.printstring+125>
000000c420045e38: 00000000010c0147 0000000000000001
000000c420045e48: 0000000000000001 00000000010c0147
000000c420045e58: 0000000000000001 00000000010c0147
000000c420045e68: 0000000000000001 0000000000000001
000000c420045e78: 000000c420045e98 000000c420045e98
000000c420045e88: 0000000001025f7a <runtime.dopanic+74> 000000c420045e98
000000c420045e98: 000000000104a330 <runtime.dopanic.func1+0> 000000c420000180
000000c420045ea8: 0000000001025bb1 <runtime.gopanic+961> 000000c420045ec8
000000c420045eb8: 000000c420045f58 0000000001025bb1 <runtime.gopanic+961>
000000c420045ec8: <0000000000000000 00000000010c70a8
000000c420045ed8: 000000c420056030 0000000800000008
000000c420045ee8: 00000000010277b1 <runtime.main+257> 000000c42007e1a0
000000c420045ef8: 000000c420045f88 000000c420000180
000000c420045f08: 000000c4200001a0 000000c420056020
000000c420045f18: 000000c420056000 000000c4200001a8
000000c420045f28: 000000c4200001a0 0000000000000000
000000c420045f38: 000000000109f220 000000c42007a1d0
000000c420045f48: 0000000000000000 0000000000000000
000000c420045f58: 000000c420045f68 !000000000108f261
000000c420045f68: >00000000010a5c40 000000c42007a1c0
000000c420045f78: 00007fffffe00000 00000000010278c2 <runtime.main+530>
000000c420045f88: 000000c420078000 0000000000000000
000000c420045f98: 000000c420078000 0000000000000000
000000c420045fa8: 0000000000000000 0000000000000000
000000c420045fb8: 0000000000000000 0000000000000000
000000c420045fc8: 000000c420000180 0000000000000000
000000c420045fd8: 000000000104d981 <runtime.goexit+1> 0000000000000000
000000c420045fe8: 0000000000000000 0000000000000000
000000c420045ff8: 0000000000000000
runtime: unexpected return pc for runtime.gopanic called from 0x108f261
stack: frame={sp:0xc420045ec8, fp:0xc420045f68} stack=[0xc420044000,0xc420046000)
000000c420045dc8: 00000000010489a2 <runtime.writeErr+66> 0000000000000002
000000c420045dd8: 00000000010c0147 0000000000000001
000000c420045de8: 000000c400000001 000000c420045e28
000000c420045df8: 0000000001026b08 <runtime.gwrite+280> 00000000010c0147
000000c420045e08: 0000000000000001 0000000000000001
000000c420045e18: 0000000001026b08 <runtime.gwrite+280> 00000000010c0748
000000c420045e28: 000000c420045e78 00000000010272bd <runtime.printstring+125>
000000c420045e38: 00000000010c0147 0000000000000001
000000c420045e48: 0000000000000001 00000000010c0147
000000c420045e58: 0000000000000001 00000000010c0147
000000c420045e68: 0000000000000001 0000000000000001
000000c420045e78: 000000c420045e98 000000c420045e98
000000c420045e88: 0000000001025f7a <runtime.dopanic+74> 000000c420045e98
000000c420045e98: 000000000104a330 <runtime.dopanic.func1+0> 000000c420000180
000000c420045ea8: 0000000001025bb1 <runtime.gopanic+961> 000000c420045ec8
000000c420045eb8: 000000c420045f58 0000000001025bb1 <runtime.gopanic+961>
000000c420045ec8: <0000000000000000 00000000010c70a8
000000c420045ed8: 000000c420056030 0000000800000008
000000c420045ee8: 00000000010277b1 <runtime.main+257> 000000c42007e1a0
000000c420045ef8: 000000c420045f88 000000c420000180
000000c420045f08: 000000c4200001a0 000000c420056020
000000c420045f18: 000000c420056000 000000c4200001a8
000000c420045f28: 000000c4200001a0 0000000000000000
000000c420045f38: 000000000109f220 000000c42007a1d0
000000c420045f48: 0000000000000000 0000000000000000
000000c420045f58: 000000c420045f68 !000000000108f261
000000c420045f68: >00000000010a5c40 000000c42007a1c0
000000c420045f78: 00007fffffe00000 00000000010278c2 <runtime.main+530>
000000c420045f88: 000000c420078000 0000000000000000
000000c420045f98: 000000c420078000 0000000000000000
000000c420045fa8: 0000000000000000 0000000000000000
000000c420045fb8: 0000000000000000 0000000000000000
000000c420045fc8: 000000c420000180 0000000000000000
000000c420045fd8: 000000000104d981 <runtime.goexit+1> 0000000000000000
000000c420045fe8: 0000000000000000 0000000000000000
000000c420045ff8: 0000000000000000
panic(0x10a5c40, 0xc42007a1c0)
/usr/local/go/src/runtime/panic.go:551 +0x3c1
我怀疑这是因为从汇编函数调用runtime.gopanic没有被注册(参见https://github.com/golang/go/blob/0e0cd70ecf6b5f0d9c8271f68b8fcc9f85cd6598/src/runtime/traceback.go#L257)。
我认为回溯实现检测到调用栈已损坏。
我尝试了一些选项,如参考Golang编译器生成的汇编代码使用funcdata.h或textflag.h,但没有找到解决方案。
我也在网上搜索了这个问题,但没有找到任何解决方案…
如果您有任何想法,请告诉我您的解决方案!
更多关于Golang汇编函数引发的panic问题分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang汇编函数引发的panic问题分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在 Go 汇编中直接调用 runtime·gopanic 会导致栈跟踪信息不完整,因为 Go 的运行时需要正确的函数帧信息来处理 panic 和恢复机制。你的汇编代码缺少必要的函数元数据,导致回溯时无法正确识别调用栈。
以下是修正后的汇编代码,通过添加正确的函数帧信息和 panic 数据处理来解决这个问题:
#include "funcdata.h"
#include "textflag.h"
// func panicPanic()
TEXT ·panicPanic(SB), NOSPLIT, $16-0
NO_LOCAL_POINTERS
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
LEAQ -16(SP), DX
CMPQ DX, 16(AX)
JBE stack_more
// 设置 panic 参数
MOVQ ·panicMessage(SB), AX
MOVQ AX, 0(SP) // panic 参数 (interface{})
MOVQ $0, 8(SP) // panic 参数第二个字
CALL runtime·gopanic(SB)
RET
stack_more:
CALL runtime·morestack_noctxt(SB)
JMP ·panicPanic(SB)
DATA ·panicMessage+0(SB)/8, $"Panic pan"
DATA ·panicMessage+8(SB)/8, $"ic"
GLOBL ·panicMessage(SB), RODATA, $16
对应的 Go 代码也需要调整:
package main
import "unsafe"
var panicMessage = [2]unsafe.Pointer{
unsafe.Pointer(&[]byte("Panic panic")[0]),
unsafe.Pointer(11),
}
func panicPanic()
func tryPanic() {
defer func() {
if r := recover(); r != nil {
println("recovered:", r.(string))
}
}()
panicPanic()
}
func main() {
tryPanic()
println("First panic recovered")
defer func() {
if r := recover(); r != nil {
println("Second panic recovered:", r.(string))
}
}()
panicPanic()
}
关键改进点:
- 使用
NO_LOCAL_POINTERS宏声明函数没有本地指针 - 添加栈检查逻辑,确保有足够的栈空间
- 正确设置 panic 参数(两个字的 interface{} 结构)
- 使用
DATA指令定义 panic 消息,确保正确的内存布局 - 在 Go 代码中使用
unsafe.Pointer数组来匹配汇编中的内存布局
这样修改后,panic 和 recover 机制就能正常工作,栈跟踪信息也会正确显示。汇编函数现在具有完整的函数帧信息,运行时能够正确处理 panic 的传播和恢复。
输出示例:
recovered: Panic panic
First panic recovered
Second panic recovered: Panic panic

