Golang中runtime.panic*函数的调用机制解析

Golang中runtime.panic*函数的调用机制解析 我正在研究未优化的 Go 构建的汇编输出。有一个函数无论如何都会返回 nil。在运行时,Go 如期因为 nil 而引发恐慌。Go 是如何调用汇编源文件中定义的 runtime.panic* 函数的?我没有看到任何对它们的调用,只看到了一些测试。

源代码:

package main

type Some struct {
	a int
} 

func getSome() *Some {
	some := 23
	if some == 40 {
		return &Some{}
	} else {
		return nil	
	}
}

func main() {
	some := getSome()
	some.a = 13
}

汇编片段:

0000000000457660 <main.getSome>:
  457660:	55                   	push   %rbp
  457661:	48 89 e5             	mov    %rsp,%rbp
  457664:	48 83 ec 10          	sub    $0x10,%rsp
  457668:	48 c7 44 24 08 00 00 	movq   $0x0,0x8(%rsp)
  45766f:	00 00 
  457671:	48 c7 04 24 17 00 00 	movq   $0x17,(%rsp)
  457678:	00 
  457679:	eb 00                	jmp    45767b <main.getSome+0x1b>
  45767b:	48 c7 44 24 08 00 00 	movq   $0x0,0x8(%rsp)
  457682:	00 00 
  457684:	31 c0                	xor    %eax,%eax
  457686:	48 83 c4 10          	add    $0x10,%rsp
  45768a:	5d                   	pop    %rbp
  45768b:	c3                   	ret
  45768c:	cc                   	int3
  45768d:	cc                   	int3
  45768e:	cc                   	int3
  45768f:	cc                   	int3
  457690:	cc                   	int3
  457691:	cc                   	int3
  457692:	cc                   	int3
  457693:	cc                   	int3
  457694:	cc                   	int3
  457695:	cc                   	int3
  457696:	cc                   	int3
  457697:	cc                   	int3
  457698:	cc                   	int3
  457699:	cc                   	int3
  45769a:	cc                   	int3
  45769b:	cc                   	int3
  45769c:	cc                   	int3
  45769d:	cc                   	int3
  45769e:	cc                   	int3
  45769f:	cc                   	int3

00000000004576a0 <main.main>:
  4576a0:	49 3b 66 10          	cmp    0x10(%r14),%rsp
  4576a4:	76 20                	jbe    4576c6 <main.main+0x26>
  4576a6:	55                   	push   %rbp
  4576a7:	48 89 e5             	mov    %rsp,%rbp
  4576aa:	48 83 ec 08          	sub    $0x8,%rsp
  4576ae:	e8 ad ff ff ff       	call   457660 <main.getSome>
  4576b3:	48 89 04 24          	mov    %rax,(%rsp)
  4576b7:	84 00                	test   %al,(%rax)
  4576b9:	48 c7 00 0d 00 00 00 	movq   $0xd,(%rax)
  4576c0:	48 83 c4 08          	add    $0x8,%rsp
  4576c4:	5d                   	pop    %rbp
  4576c5:	c3                   	ret
  4576c6:	e8 f5 ce ff ff       	call   4545c0 <runtime.morestack_noctxt.abi0>
  4576cb:	eb d3                	jmp    4576a0 <main.main>

更多关于Golang中runtime.panic*函数的调用机制解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复
  • 调用 panic 函数(例如 runtime.Panicruntime.Panicf)时需传入一个参数,该参数可以是任意值。此参数即成为与错误关联的“恐慌值”。

更多关于Golang中runtime.panic*函数的调用机制解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Godbolt 使用颜色来显示汇编代码的哪些部分属于哪些 Go 代码。

我在这里粘贴了你的代码:

godbolt.org

Compiler Explorer - Go (x86-64 gc 1.21)

package main

type Some struct {
	a int
} 

func getSome() *Some {
	some := 23
	if some == 40 {
		return &Some{}
	} else {
		return nil	
	}
}

func main() {
	some := getSome()
	some.a = 13
}

也许这能帮到你?

在Go语言中,runtime.panic*函数的调用是通过编译器插入的隐式检查实现的,而不是直接在汇编中显式调用。从你的汇编代码可以看出,关键指令在main.main函数的4576b7地址处:

test %al,(%rax)

这条指令实际上是对指针%raxgetSome()的返回值)进行空指针检查。当%rax为nil时,访问(%rax)会触发硬件异常(段错误),Go运行时通过信号处理机制捕获这个异常,并转换为panic。

以下是更详细的机制解析:

  1. 编译器插入检查:Go编译器在生成汇编时,会在可能发生panic的位置插入检查指令。对于空指针访问,就是test指令。

  2. 硬件异常转换:当执行test %al,(%rax)%rax为0时,CPU会触发页面错误异常。Linux/Unix系统会将此作为SIGSEGV信号发送给进程。

  3. 信号处理:Go运行时在初始化时设置了信号处理器(runtime.sigtramp)。当收到SIGSEGV信号时,处理器会检查故障地址和当前goroutine的状态。

  4. 转换为panic:如果信号处理器确定这是由空指针解引用引起的,它会调用runtime.panicmem()来创建panic:

// runtime/panic.go中的实际实现
func panicmem() {
    panicmemAddr(nil)
}

func panicmemAddr(addr unsafe.Pointer) {
    panic(errorAddressString{msg: "invalid memory address or nil pointer dereference", addr: addr})
}
  1. panic传播runtime.panicmem()会创建panic对象,并开始panic的传播过程,最终打印堆栈跟踪信息。

你可以通过查看Go运行时的信号处理代码来验证这个机制。在runtime/signal_unix.go中:

func sigtrampgo(sig *sigctxt, gp *g) {
    // ...
    if sig.sig() == _SIGSEGV && gp.sigcode0 == _SI_USER && gp.sigcode1 == 0 {
        // 这是空指针解引用
        panicmem()
    }
    // ...
}

在你的例子中,由于getSome()总是返回nil,当main.main尝试访问some.a时,test %al,(%rax)指令会触发硬件异常,Go运行时捕获这个异常并转换为panic。

这就是为什么你在汇编代码中没有看到显式的call runtime.panicmem,但实际上panic仍然会发生的原因。编译器通过硬件异常机制和运行时信号处理的配合,实现了高效的panic触发机制。

回到顶部