golang中的panic,recover执行过程?
上篇文章 golang中defer的执行过程是怎样的? 介绍了一下defer的执行过程,本篇是上一篇的引申,主要介绍panic、recover的底层分析,如果没有读过上一篇文章,可以先去读一下在看这篇。 总共分3部分讲解:
1 panic
2 defer panic
3 defer panic recover
环境:go version go1.12.5 linux/amd64
1 panic
golang中的异常总共分为4中:
- 编译器捕获的
- 直接手动panic
- golang捕获的
- 系统捕获的
编译器捕获的
1/0
我们知道被除数是不能等于0的,所以这种错误是编译不过去的,会提示: ./main.go:7:8: division by zero
直接手动panic
示例代码:
package main func main() { panic("panic error!!") } 复制代码
编译成汇编代码看panic函数会指向底层哪个函数: go tool compile -S main.go > main.s
0x0034 00052 (main.go:4) CALL runtime.gopanic(SB) 复制代码
查看 gopanic(SB)
实现,先粗略看一下代码的含义一些解释在代码中已经注解:
func gopanic(e interface{}) { gp := getg() //获取当前的g ....省略不重要的 var p _panic //_panic原型 p.arg = e //将panic参数存入arg参数 p.link = gp._panic //将p.link绑定到当前的g的_panic上。 gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) //将p绑定到g的链表头。 atomic.Xadd(&runningPanicDefers, 1) for { d := gp._defer if d == nil { break } if d.started { if d._panic != nil { d._panic.aborted = true } d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) continue } d.started = true d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))//将p绑定到g的链表头。 p.argp = unsafe.Pointer(getargp(0)) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) //调用g上的defer(源程序中如果没有defer函数,编译器会生成一个并绑定到g._defer上) p.argp = nil 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) if p.recovered { //先忽略讲到recover时候在说 ..... } } preprintpanics(gp._panic) //循环打印panic fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached } 复制代码
我们发现panic的原型是_panic,去看一下定义:
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 } 复制代码
发现是个结构体类型,里面的类型我们在调试代码的时候在去探究具体的含义。 接下来我们就用gdb跟踪一下上面的源码示例。
go build -o main gdb main
进入gdb界面并断点到panic函数行见下图:
按s进入到gopanic(interface)中。 发现这条语句:
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) 复制代码
原来当前的gp定义(由于不是讲goroutine 这里就不贴gp的原型了)中有_panc字段作为链表头,而_panic结构体中有link字段。不难看出和defer同理:从goroutine._panic作为头,然后用_painc.link作为链接组成了一个链表的数据结构。之所以是链表是因为recover到panic时候,recover中也有可能有panic,例如见下方代码:
if err := recover(); err != nil { panic("go on panic xitehip") } 复制代码
deferd函数也会继续有panic。下方讲到recover时候详细讲解。 执行上面的语句此时的链表示意结构见下方: gp._panic => p.link => gp._panic(之前的链表头)
继续往下走:
运行到reflectcall()函数,发现这个函数总共有5个参数:
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32) 复制代码
从第二个参数可知这个是函数指针,猜测这个reflectcall是调用我们实参 unsafe.Point(d.fn)
的。根据源码中的定义 d := gp._defer
可知变量d就是上文我们说的g._defer。那马上有疑问了,这个例子里根本没有用到defer关键字,就不会调用deferproc(SB)生成defer。那只有一种可能就是编译器帮我们做了生成了一个defer函数然后绑定到了g._defer的链表头上。 继续看reflectcall函数见下图x:
用disass命令查看一下汇编代码,绿线处的是即将调用的reflectcall函数。红线处是它的下一条指令,记住它的地址
0x0000000000423025
,我们去看一下reflectcall函数执行完的返回值是如何指向到红线处的指令的。 见下方汇编代码:
//runtime/asm_amd64.s TEXT ·reflectcall(SB), NOSPLIT, $0-32 MOVLQZX argsize+24(FP), CX DISPATCH(runtime·call32, 32) DISPATCH(runtime·call64, 64) ..... MOVQ $runtime·badreflectcall(SB), AX JMP AX 复制代码
//runtime/asm_amd64.s #define DISPATCH(NAME,MAXSIZE) \ CMPQ CX, $MAXSIZE; \ JA 3(PC); \ MOVQ $NAME(SB), AX; \ JMP AX 复制代码
//runtime/asm_amd64.s #define CALLFN(NAME,MAXSIZE) \ TEXT NAME(SB), WRAPPER, $MAXSIZE-32; \ NO_LOCAL_POINTERS; \ /* copy arguments to stack */ \ MOVQ argptr+16(FP), SI; \ MOVLQZX argsize+24(FP), CX; \ MOVQ SP, DI; \ REP;MOVSB; \ /* call function */ \ MOVQ f+8(FP), DX; \ PCDATA $PCDATA_StackMapIndex, $0; \ CALL (DX); \ /* copy return values back */ \ MOVQ argtype+0(FP), DX; \ MOVQ argptr+16(FP), DI; \ MOVLQZX argsize+24(FP), CX; \ MOVLQZX retoffset+28(FP), BX; \ MOVQ SP, SI; \ ADDQ BX, DI; \ ADDQ BX, SI; \ SUBQ BX, CX; \ CALL callRet(SB); \ RET 复制代码
是不是很乱,这些是啥??看不懂。用gdb跟踪一下到: 运行到下图:
disass一下看一下CALLFN(. call32, 32)所指向的指令:
绿框处所对应的的就是源文件中的代码:
TEXT callRet(SB), NOSPLIT, $32-0 复制代码
那红框ret处就是reflectcall的返回。打到断点到ret处。 执行到这里见下图:
ret的作用是pop 栈顶到rip,我们看一下rsp中的内容是啥?
0x423025
所指向的内容:
图y和上面的图x的地址一样的,就是reflectcall指令的下条指令。再看一下源文件下行代码是啥?
p.argp = nil
翻译成汇编代码就是图y中的
mov QWROD PTR [rsp+0x58],0x0
,就是变量赋值会把值存入栈中而不是寄存器中。
执行完d.fn,将d脱链:
d._panic = nil d.fn = nil gp._defer = d.link 复制代码
运行到:
func fatalpanic(msgs *_panic) 复制代码
进行打印输出,看一下实现:
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() var docrash bool systemstack(func() { if startpanic_m() && msgs != nil { atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs) } docrash = dopanic_m(gp, pc, sp) }) if docrash { crash() } systemstack(func() { exit(2) }) *(*int)(nil) = 0 // not reached } 复制代码
重点看如下函数:
printpanics(msgs) 复制代码
实现:
func printpanics(p *_panic) { if p.link != nil { printpanics(p.link) print("\t") } print("panic: ") printany(p.arg) if p.recovered { print(" [recovered]") } print("\n") } 复制代码
发现这个是个递归调用,从g._panic链表头开始直到链表结束然后打印出panic信息。
golang捕获的
例如slice越界,见下方代码:
package main import "fmt" func main() { arr := []int{1, 2} arr[2] = 3 fmt.Println(arr) } 复制代码
会panic: panic: runtime error: index out of range 编译成汇编代码:go tool compile -S main.go > main.s
0x003c 00060 (main.go:7) CALL runtime.panicindex(SB) 复制代码
可知调用了panicindex(SB) 去看一下它的实现:
func panicindex() { if hasPrefix(funcname(findfunc(getcallerpc())), "runtime.") { throw(string(indexError.(errorString))) } panicCheckMalloc(indexError) panic(indexError) } 复制代码
发现最终还是会调用panic(interface{})这个函数,然后就是上面所说的手动panic的执行流程,在这里不在重复赘述。
系统捕获的
比如对只读内存区赋值操作会引起panic
package main import "fmt" func main() { var pi *int *pi = 100 fmt.Printf("%v", *pi) } 复制代码
会报如下错误: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x488a53] goroutine 1 [running]: main.main() /server/gotest/src/hello/defer/main.go:7 +0x3a
编译成汇编代码没有发现gopanic入口。因为最终输出panic栈的信息,所以肯定调用了gopanic,给gopanic()打上断点直接运行到这里见下图:
确实拦截到了gopanic,看一下它的调用链: main.main => runtime.sigpanic() => runtime.panicmem() => gopanic()。 那为什么汇编中没有sigpanic()入口还能调用这个函数呢? 看一下
*pi = 100
生成的汇编代码:
划红线处: test BYTE PTR [ax], al
由于ax=0x0所以 BYTE PTR [ax]
是获取不到0x0的内存的。这样cpu执行这条语句的时候会进入内核态保存 0x488b1a
到寄存器,内核态发送消息给go进程,go处理函数将 0x488b1a
所指向的内容换成go启动时事先注册号的函数作为指令入口,回到内核态执行 0x488b1a -> 注册函数
的指令。具体的调用链在这里就不深究了重点还是panic,recover。
2 defer panic
2.1示例:
package main import "fmt" func main() { defer fmt.Println("d1") defer fmt.Println("d2") panic("panic error") } 复制代码
输出: d2 d1 panic error 如下核心代码:
//runtime/panic.go func gopanic(e interface{}) { for { ...//获取goroutine表头deferd //执行表头的deferd reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) ...//将表头的deferd拖链,将下一个deferd绑定到表头 } ... fatalpanic(gp._panic) // 运行递归调用gp._panic链表上的panic ... } 复制代码
从上面代码可知,gopanic先遍历deferd链在遍历panic链,所以panic error最后输出。
2.2示例:
输出: d2 d1 panic: panic error panic: panic error2 根据示例2.1 函数gopanic()可知函数的调用链见下面调用关系:
第14行panic -> gopanic() -> reflectcall -> 第12行defer -> reflectcall -> 第8行defer -> 第9行panic -> gopanic -> reflectcall -> 继续执行deferd链上的也就是第6行defer -> fatalpanic(里面子函数printpanics()递归调用g._panic链)。
3 defer panic recover
下面介绍的是recover的执行过程,先看下方示例代码:
package main import "fmt" func main() { re() fmt.Println("After recovery!") } func re() { defer func() { if err := recover(); err != nil { fmt.Println("err:", err) } }() panic("panic error1") } 复制代码
输出: err: panic error1 After recovery!
recover()的作用是捕获异常之后让程序正常往下执行而不会退出。这个例子里re()函数里有了异常,并且被捕获然后执行了re()下面的代码输出’After recovery’。
那为什么执行完recover()之后会跳转到输出行执行呢?
从汇编角度考虑:执行完re()之后要想保证继续往下执行,首先要把下一行的入口地址存起来,然后recover()之后再去取回来,放到rip指令寄存器中这样才可以向下执行。
在re()里除了deferd函数还有有panic()这行,那很明显它的内部实现里会有相关实现,继续分析recover的实现和panic内部的相关实现。
汇编查看recover(): go tool compile -S main.go
发现gorecover(SB),猜测是recover()的实现:
0x002a 00042 (main.go:13) CALL runtime.gorecover(SB) 复制代码
在recover()行打断点,发现确实执行了gorecover(SB)函数,实现如下:
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 } 复制代码
从以上代码可知gorecover(uintptr)只是把当前goroutine的_panic.recovered 设置为true,然后返回之前panic函数设置的参数(err)给调用方。其实就是将当前的g._panic设置个标致,告诉以后的程序说我已经被捕获到了。
这个有recover()的deferd函数执行完之后会返回到上面提到的gopanic(interface{})函数中的 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
下一行继续往下执行。 见下方代码:
func gopanic(e interfac{}) { ....... reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) //往下看: p.argp = nil if gp._defer != d { throw("bad defer entry in panic") } //执行完defered函数之后脱链 d._panic = nil d.fn = nil gp._defer = d.link pc := d.pc //deferproc()函数中存入的放回值地址 sp := unsafe.Pointer(d.sp) // freedefer(d) if p.recovered {//执行了gorecover()函数之后p.recovered == true atomic.Xadd(&runningPanicDefers, -1) gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { // must be done with signal gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc //pc恢复栈作用。 mcall(recovery) throw("recovery failed") // mcall should not return } ...... } 复制代码
看一下这行代码:
pc := d.pc 复制代码
pc是什么呢?它是上篇 文章 中提到的deferproc()函数中存入的,见下方代码:
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn ... callerpc := getcallerpc() d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.fn = fn d.pc = callerpc .... 复制代码
我们在下方截图的第12行打一断点来看一下pc中到底是啥。看一下绿框中的指令:
defer关键字会翻译成call runtime.deferproc那它下方绿框中的是runtime.deferproc后面的指令是编译器生成的(也可以这么理解,defer关键字会让编译器生成deferproc函数指令及后面一堆指令)第一行:
test eax, eax
的地址是
0x4872d5
稍后会再次说到这个指令及地址。
继续断点执行到 d.pc = callerpc
之后,我们看一下 d.pc
到底是什么值,见下图:
0x4872d5
这不是刚刚说的上图绿框处
test eax, eax
的指令地址吗。带着疑问继续往下看。
从上面gorecover(uintptr)函数代码可知 p.recoverd == true 所以gopanic()中会执行到 if p.recovered {
里,我们着重看两行代码:
gp.sigcode1 = pc 复制代码
将pc就是deferproc()函数的返回值赋值给gp.sigcode1,为返回到正常流程做准备。
mcall(recovery) 复制代码
其中的mcall先不看,先看recovery函数作用,见下方实现:
func recovery(gp *g) { // Info about defer passed in G struct. sp := gp.sigcode0 pc := gp.sigcode1 // 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 gogo(&gp.sched) } 复制代码
recovery(*g) 主要是gp.sched赋值。其中pc是当前deferproc函数的返回地址。我们再看一下gogo(&gp.sched)函数实现,因为gogo函数是用汇编实现的所以用gdb跟踪是最方便的见下方代码:
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 MOVQ gobuf_bp(BX), BP MOVQ $0, gobuf_sp(BX) // clear to help garbage collector MOVQ $0, gobuf_ret(BX) MOVQ $0, gobuf_ctxt(BX) MOVQ $0, gobuf_bp(BX) MOVQ gobuf_pc(BX), BX JMP BX 复制代码
着重看2行代码:
MOVQ gobuf_ret(BX), AX 复制代码
AX从某个值变成了1,这个指令的偏移数量是gobuf_ret,其中的ret不就是返回的意思吗,见下图。
再看最后一条指令:
JMP BX 复制代码
看一下BX到底是啥:
绿框处就是BX的值,也就是要jmp到这个地址处执行,这个地址眼熟吗,不就是刚提到的
0x4872d5
吗,对应的指令是
test eax,eax
。再重看一下这个图:
其中绿框第一行就是要跳转的地址。刚才说了AX已经变成了1。那下方的两行指令
test eax, eax jne 0x4872f9 复制代码
的意思是如果eax不等于0就跳转到这个地址否则就去执行绿框处第三行的正常流程。因为eax已经不等0了,所以就会跳转到 0x4872f9
这个地址处,跟踪一下这个地址指向的是哪里,见下图:
原来它调用了
runtime.deferreturn()
函数,见下图。
执行到这里。
sp := getcallersp() sp是调用者的sp。就是即将调用 defer func() {
时的sp。 d.sp 是调用链上第二个defer,因为第一个deferd已经脱链。 显然这两个不相等,所以return了,具体return底层到底是如何将re()的返回地址返回的就不在跟踪了。然后执行到了下放的入口地址处:
fmt.Println("After recovery!") 复制代码
整个流程,参看下图代码然后解释:
call re()
=> 将re()返回值压栈到栈顶
=> 执行12行defer函数
=> 执行deferproc():将deferproc返回值存入pc,调用者(re())栈顶存入到sp,将defered函数加入到链表头,返回0(return0函数作用是将ax设为0)
=> 返回到下方代码test eax eax处
=> 由于ax=0继续运行到17行的panic()
=> gopanic()
=> 调用reflectcall():执行deferd函数
=> 执行recovery():将recoverd标志位设为1
=> mcall(recovery)
=> gogo():ax设为1,跳转到pc处
=> 再一次跳转到test eax, eax :由于ax=1
=> 跳转到deferreturn()函数:callersp !=d.sp,这里的d.sp中的d其实已经是是g上面默认带的_defer了,所以不等
=> return 获取re()的返回地址pop到rip处
=> cpu执行其返回值
=> 输出'After recovery'
... //defer函数 =>deferproc 0x00000000004872d0 : call 0x426c00 0x00000000004872d5 : test eax,eax 0x00000000004872d7 : jne 0x4872f9 0x00000000004872d9 : jmp 0x4872db 0x00000000004872db : lea rax,[rip+0x111be] # 0x4984a0 0x00000000004872e2 : mov QWORD PTR [rsp],rax 0x00000000004872e6 : lea rax,[rip+0x48643] 0x00000000004872ed : mov QWORD PTR [rsp+0x8],rax //panic() => gopanic 0x00000000004872f2 : call 0x427880 ... 复制代码
recover()的核心其实就是defer函数生成的汇编指令:判断跳转区分正常流程还是获取返回值流程。见上方汇编代码。 机器指令是从上往下执行,正常流程是执行完deferproc之后再执行panic()生成的gopanic()。获取返回值流程必然需要跳转到某处获取,而golang的设计者放到了deferreturn()函数中所以最终要跳到这里来。
留个疑问下方代码如何输出,为什么?
package main import "fmt" func main() { re() fmt.Println("After recovery!") } func re() { defer func() { if err := recover(); err != nil { fmt.Println("Recover again:", err) } }() defer func() { if err := recover(); err != nil { switch v := err.(type) { case string: panic(string(v)) } } }() panic("start panic") } 复制代码