panic 的本质其实就是 gopanic,用户代码和 runtime 中都会有对 panic() 的调用,最终会转为这个 runtime.gopanic 函数。
func gopanic(e interface{}) {
// 获取当前 g
gp := getg()
// 生成一个新的 panic 结构体
var p _panic
p.arg = e
// 链上之前的 panic 结构
p.link = gp._panic
// 新节点做整个链表的表头
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 统计
atomic.Xadd(&runningPanicDefers, 1)
for {
d := gp._defer
if d == nil {
break
}
// 标记 defer 为已启动,暂时保持在链表上
// 这样 traceback 在栈增长或者 GC 的时候,能够找到并更新 defer 的参数栈帧
// 并用 reflectcall 执行 d.fn
d.started = true
// 记录在 defer 中发生的 panic
// 如果在 defer 的函数调用过程中又发生了新的 panic,那个 panic 会在链表中找到 d
// 然后标记 d._panic(指向当前的 panic) 为 aborted 状态。
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil
// reflectcall 没有 panic,移除 d
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
// p.recovered 字段在 gorecover 函数中已经修改为 true 了
if p.recovered {
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 { // 必须已处理完 signal
gp.sig = 0
}
// 把需要恢复的栈帧信息传给 recovery 函数
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall 永远不会返回
}
}
}
gopanic 会把当前函数的 defer 一直执行完,其中碰到 recover 的话就会调用 gorecover 函数:
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
很简单,就是改改 recovered 字段。
gopanic 里生成的 panic 对象是带指针的结构体,串成一个链表,用 link 指针相连接,并挂在 g 结构体上:
// panics
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
下面这种写法应该会形成一个 _panic 的链表:
package main
func fxfx() {
defer func() {
if err := recover(); err != nil {
println("recover here")
}
}()
defer func() {
panic(1)
}()
defer func() {
panic(2)
}()
}
func main() {
fxfx()
}
非 defer 中的 panic 的话,函数的后续代码就没办法运行了,写代码的时候要注意这个特性。
上面的 panic 恢复过程,最终走到了 recovery:
// 把需要恢复的栈帧信息传给 recovery 函数
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
// 在被 defer 的函数中调用的 recover 从 panic 中已恢复,展开栈继续运行。
// 现场恢复为发生 panic 的函数正常返回时的情况
func recovery(gp *g) {
// 之前之前传入的 sp 和 pc 恢复
sp := gp.sigcode0
pc := gp.sigcode1
// d 的参数需要放在栈上
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")
}
// 让这个 defer 结构体的 deferproc 位置的调用重新返回
// 这次将返回值修改为 1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1 // ------> 没有调用 deferproc,但直接修改了返回值,所以跳转到 deferproc 的下一条指令位置,且设置了 1,假装作为 deferproc 的返回值
gogo(&gp.sched)
}
这里比较 trick,实际上 recovery 中传入的 pc 寄存器的值是 call deferproc 的下一条汇编指令的地址:
0x0034 00052 (x.go:4) CALL runtime.deferproc(SB)
0x0039 00057 (x.go:4) TESTL AX, AX -----> 传入到 deferproc 中的 pc 寄存器指向的位置
0x003b 00059 (x.go:4) JNE 135
和刚注册 defer 结构体链表时的情况不同,panic 时,我们没有调用 deferproc,而是直接跳到了 deferproc 的下一条指令的地址上,并且检查 AX 的值,这里已经被改成 1 了。
所以会直接跳转到函数最后对应该 deferproc 的 deferreturn 位置去。
最后确认一次,runtime.gogo 会把 sched.ret 搬到 AX 寄存器:
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
MOVQ 0(DX), CX // make sure g != nil
get_tls(CX)
MOVQ DX, g(CX)
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ret(BX), AX // ----------> 重点在这里
MOVQ gobuf_ctxt(BX), DX
这样所有流程就都打通了。