golang中的panic,recover执行过程?
source link: https://www.tuicool.com/articles/QvYZfuJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
上篇文章 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:
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 <+48>: call 0x426c00 <runtime.deferproc> 0x00000000004872d5 <+53>: test eax,eax 0x00000000004872d7 <+55>: jne 0x4872f9 <main.re+89> 0x00000000004872d9 <+57>: jmp 0x4872db <main.re+59> 0x00000000004872db <+59>: lea rax,[rip+0x111be] # 0x4984a0 0x00000000004872e2 <+66>: mov QWORD PTR [rsp],rax 0x00000000004872e6 <+70>: lea rax,[rip+0x48643] 0x00000000004872ed <+77>: mov QWORD PTR [rsp+0x8],rax //panic() => gopanic 0x00000000004872f2 <+82>: call 0x427880 <runtime.gopanic> ... 复制代码
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") } 复制代码
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK