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
- 调用
panic函数(例如runtime.Panic、runtime.Panicf)时需传入一个参数,该参数可以是任意值。此参数即成为与错误关联的“恐慌值”。
更多关于Golang中runtime.panic*函数的调用机制解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Godbolt 使用颜色来显示汇编代码的哪些部分属于哪些 Go 代码。
我在这里粘贴了你的代码:
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)
这条指令实际上是对指针%rax(getSome()的返回值)进行空指针检查。当%rax为nil时,访问(%rax)会触发硬件异常(段错误),Go运行时通过信号处理机制捕获这个异常,并转换为panic。
以下是更详细的机制解析:
-
编译器插入检查:Go编译器在生成汇编时,会在可能发生panic的位置插入检查指令。对于空指针访问,就是
test指令。 -
硬件异常转换:当执行
test %al,(%rax)且%rax为0时,CPU会触发页面错误异常。Linux/Unix系统会将此作为SIGSEGV信号发送给进程。 -
信号处理:Go运行时在初始化时设置了信号处理器(
runtime.sigtramp)。当收到SIGSEGV信号时,处理器会检查故障地址和当前goroutine的状态。 -
转换为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})
}
- 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触发机制。

