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.htextflag.h,但没有找到解决方案。 我也在网上搜索了这个问题,但没有找到任何解决方案…

如果您有任何想法,请告诉我您的解决方案!


更多关于Golang汇编函数引发的panic问题分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于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()
}

关键改进点:

  1. 使用 NO_LOCAL_POINTERS 宏声明函数没有本地指针
  2. 添加栈检查逻辑,确保有足够的栈空间
  3. 正确设置 panic 参数(两个字的 interface{} 结构)
  4. 使用 DATA 指令定义 panic 消息,确保正确的内存布局
  5. 在 Go 代码中使用 unsafe.Pointer 数组来匹配汇编中的内存布局

这样修改后,panic 和 recover 机制就能正常工作,栈跟踪信息也会正确显示。汇编函数现在具有完整的函数帧信息,运行时能够正确处理 panic 的传播和恢复。

输出示例:

recovered: Panic panic
First panic recovered
Second panic recovered: Panic panic
回到顶部