本文主要分析Go语言的panic/recover在AMD64 Linux平台下的实现,包括:
阅读本文所必需的预备知识:
panic/recover例子
先来热一下身,大家可以先想想下面几个例子的输出会是什么,检测一下自己对panic/recover的理解。
例1:
func f() {
defer catch("f")
g()
}
func g() {
defer m()
panic("g panic")
}
func m() {
panic("m panic")
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
例2:
func f() {
defer catch("f")
g()
}
func g() {
defer m()
panic("g panic")
}
func m() {
defer catch("m")
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
例3:
func f() {
defer catch("f")
g()
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
func g() {
defer m()
panic("g panic")
}
func m() {
defer catch("m")
panic("m panic")
}
panic/recover要点简介
为了更好的理解panic/recover的实现代码,我们首先需要了解几个与之有关的要点:
下面对第2点和第3点做个简单的说明。假设有如下程序片段:
例4
package main
import "fmt"
func main() {
f()
fmt.Println("main")
}
func f() {
defer catch("f") // 1
panic("f panic")
fmt.Println("f continue")
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
f()函数运行时会发生panic,但该panic会被它通过defer注册的catch函数所捕获从而恢复程序的正常执行流程,上一篇文章我们提到过deferproc函数有个隐含的返回值与panic/recover有关,下面我们通过f()函数再来看一下相关的汇编指令片段:
......
# 对应defer catch("f")
0x0000000000487245 <+69>: callq 0x426c00 <runtime.deferproc>
0x000000000048724a <+74>: test %eax,%eax
0x000000000048724c <+76>: jne 0x48726c <main.f+108>
......
# 对应panic("f panic")
0x0000000000487265 <+101>: callq 0x427880 <runtime.gopanic>
......
0x000000000048726c <+108>: nop
0x000000000048726d <+109>: callq 0x427490 <runtime.deferreturn>
......
该代码片段前3条指令对应着f()函数中注释1处的一行go代码,最后两条指令通过调用deferreturn函数去执行defered函数,如果f函数不发生panic,其执行流程我们在上一篇文章中已经详细介绍过,但此处的f函数会发生panic,其流程稍有不同:
主动调用panic()函数
一般来说,Go程序在两种情况下会发生panic:
我们先来看主动调用panic函数时panic/recover的流程。
通过反汇编可以得知go代码中对panic()/recover()函数的调用会被编译器翻译成对runtime包中的gopanic()以及gorecover()函数的调用。
runtime/panic.go : 453
// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
gp := getg()
......
//panic可以嵌套,比如发生了panic之后运行defered函数又发生了panic,如上面的例3。
//最新的panic会被挂入goroutine对应的g结构体对象的_panic链表的表头
var p _panic //创建_panic结构体对象
p.arg = e //panic的参数
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
for {
d := gp._defer //取出_defer链表头的defered函数
if d == nil {
break //没有defer函数将会跳出循环,然后打印栈信息然后结束程序
}
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
if d.started {
//到这里一定发生了panic嵌套,即在defered函数中又发生了panic,请参考本文开头的例1
//d.started = true是panic嵌套的充分条件,但并不是必要条件,也就是说
//即使d.started为false也是可能发生嵌套的,请结合defer的处理流程并参考本文开头的例3
//最近发生的一次panic并没有被recover所以取消上一次发生的panic
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
// Mark defer as started, but keep on list, so that traceback
// can find and update the defer's argument frame if stack growth
// or a garbage collection happens before reflectcall starts executing d.fn.
d.started = true //用于判断是否发生了嵌套panic
// Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
//把panic和defer函数关联起来
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//在panic中记录当前panic的栈顶位置,用于recover判断
p.argp = unsafe.Pointer(getargp(0))
//通过reflectcall函数调用defered函数
//如果defered函数再次发生panic而且并未被该defered函数recover,则reflectcall永远不会返回,参考例2。
//如果defered函数并没有发生过panic或者发生了panic但该defered函数成功recover了新发生的panic,
//则此函数会返回继续执行后面的代码。
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil
// reflectcall did not panic. Remove d.
if gp._defer != d {
throw("bad defer entry in panic")
}
//defer函数已经被执行,脱链
d._panic = nil
d.fn = nil
gp._defer = d.link
// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC()
pc := d.pc //call deferproc的下一条指令的地址,下一条指令为 test rax, rax,在defer实现机制一文中有详细说明
//call deferproc指令执行前的栈顶指针
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
if p.recovered {
//defered函数调用recover成功捕获了panic会设置p.recovered = true
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig = 0
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
//mcall函数永远不会返回,mcall函数的实现可以参考公众号内的其它文章,有详细分析
//调用recovery函数跳转到pc位置继续执行
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// ran out of deferred calls - old-school panic now
// Because it is unsafe to call arbitrary user code after freezing
// the world, we call preprintpanics to invoke all necessary Error
// and String methods to prepare the panic strings before startpanic.
preprintpanics(gp._panic)
//打印函数调用链,然后挂死程序
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
// runtime/panic.go : 578
// The implementation of the predeclared function recover.
// Cannot split the stack because it needs to reliably
// find the stack segment of its caller.
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
//条件argp == uintptr(p.argp)在判断panic和recover是否匹配,内层recover不能捕获外层的panic
//比如本文开头的例2中m函数中的defer catch("m")不能捕获g函数中的panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true //通过设置p.recovered = true告诉gopanic函数panic已经被recover了
return p.arg
}
return nil
}
// runtime/panic.go : 634
// Unwind the stack after a deferred function calls recover
// after a panic. Then arrange to continue running as though
// the caller of the deferred function returned normally.
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0 //call deferproc时的栈顶指针
pc := gp.sigcode1 //call deferproc下一条指令的地址
// d's arguments need to be in the stack.
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
}
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1 //该值(1)会被gogo函数放入eax寄存器
gogo(&gp.sched) //跳转到pc所指的指令处继续执行,gogo函数的实现请参考公众号内的其它文章,有详细分析
}
从代码可以看出,如果不考虑嵌套,主动 panic/recover 的流程比较清晰:遍历当前 goroutine 所注册的 defered 函数并通过 reflectcall 调用遍历到的函数,如果某个 defered 函数调用了recover(对应到runtime的gorecover函数)则使用 mcall(recovery) 恢复程序的正常流程,否则执行完所有的 defered 函数之后打印出 panic 的栈信息然后退出程序。这里需要说明一下为什么需要通过 reflectcall 来调用 defered 函数而不是直接调用 defered 函数。原因在于直接调用 defered 函数就得在当前栈帧中为它准备参数,而不同的 defered 函数的参数大小可能会有很大差异,比如有的defered函数没有参数而有些defered函数可能又需要成千上万字节的参数,然而gopanic 函数的栈帧大小固定而且很小,所以很有可能没有足够的空间来存放 defered 函数的参数,而reflectcall函数可以处理这种情况,具体是怎么处理的这里就不介绍了,有兴趣的话大家可以去看一下reflectcall函数的代码。
对于panic的嵌套,也就是defered函数再次发生了panic,这会导致gopanic函数再次被调用,也就是说gopanic函数会存在递归调用,其调用链为 gopanic()->reflectcall()->defered函数->gopanic() ,这时有两种情况:
对于上述两种情况,大家可以结合前面代码中的注释以及例2和例3加以理解。
非法操作引起的panic
最常见的非法操作主要是非法访问内存,我们来看一个例子:
package main
import (
"fmt"
)
func f() {
var p *int
*p = 100 // crash
fmt.Println("not reached")
}
func main() {
f()
}
这个程序运行时会发生panic,原因是f()函数企图向p所指的内存写入100,但指针变量p却是nil。来看看f函数的汇编代码片段:
0x0000000000487200 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000487209 <+9>: cmp 0x10(%rcx),%rsp
0x000000000048720d <+13>: jbe 0x4872a1 <main.f+161>
0x0000000000487213 <+19>: sub $0x70,%rsp
0x0000000000487217 <+23>: mov %rbp,0x68(%rsp)
0x000000000048721c <+28>: lea 0x68(%rsp),%rbp
0x0000000000487221 <+33>: movq $0x0,0x30(%rsp)
0x000000000048722a <+42>: xor %eax,%eax
0x000000000048722c <+44>: test %al,(%rax)
0x000000000048722e <+46>: movq $0x64,(%rax) # *p = 100
0x0000000000487235 <+53>: xorps %xmm0,%xmm0
0x0000000000487238 <+56>: movups %xmm0,0x40(%rsp)
0x000000000048723d <+61>: lea 0x40(%rsp),%rax
......
通过汇编代码我们可以确定编译器并未插入对gopanic函数的调用,但这个程序运行起来发生panic时与在go代码中直接调用gopanic函数时的表现是一样的,都会输出panic时栈的信息,所以这种非法操作最终应该也会调用到gopanic函数,但具体是怎么调用到它的呢?我们可以使用调试工具dlv给gopanic下一个断点,等断下来之后使用bt可以看到其函数调用链为:
f()->runtime.sigpanic()->runtime.panicmem()->runtime.gopanic()
可以看到f()调用了runtime.sigpanic()函数,但从上面的汇编代码可以得知f()其实并没有直接调用runtime.sigpanic()函数,是不是有些奇怪?
事实上,当CPU在执行
0x000000000048722e <+46>: movq $0x64,(%rax) # *p = 100
这一条指令时,CPU会发生异常,异常发生后将依次执行如下流程:
上述整个流程与Linux系统的信号处理有关,了解即可,如果有兴趣可以参考相关的内核资料,这里我们只需要关注第5步和第7步。从该流程可以看出,当go程序发生异常之后之所以能够最终执行到gopanic函数,关键在于上述流程的第5步修改了异常之后的执行流程,而第5步中的信号处理程序是由go语言的runtime提供的,所以下面我们直接从信号处理程序开始大致看一下其流程。
SIGSEGV信号处理流程
对于SIGSEGV信号,信号处理程序的函数调用链为
内核返回-> runtime.sigtramp() ->runtime.sigtrampgo()->runtime.sighandler()->sigctxt.preparePanic()修改异常返回地址
这个调用链中的函数由大家自己去挖掘细节,这里只说两点:
// preparePanic sets up the stack to look like a call to sigpanic.
func (c *sigctxt) preparePanic(sig uint32, gp *g) {
if GOOS == "darwin" {
......
}
//指针c所指的内存即执行信号处理程序之前由内核保存在栈上的数据
//c.rip即为异常返回地址,也就是异常发生时CPU正在执行的指令的地址
pc := uintptr(c.rip())
sp := uintptr(c.rsp())
if shouldPushSigpanic(gp, pc, *(*uintptr)(unsafe.Pointer(sp))) {
// Make it look the like faulting PC called sigpanic.
if sys.RegSize > sys.PtrSize {
sp -= sys.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = 0
}
sp -= sys.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = pc
c.set_rsp(uint64(sp))
}
c.set_rip(uint64(funcPC(sigpanic))) //修改异常返回地址为sigpanic函数的地址
}
这个函数的最后一行把异常返回地址修改成了runtime.sigpanic函数的地址,等信号处理完成进入内核后再次返回用户态时CPU将会从runtime.sigpanic函数开始执行,最终执行到前面已经分析过的gopanic函数,这部分代码很清晰,大家有兴趣的话可以自己看看。