24

golang中的panic,recover执行过程?

 5 years ago
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.
neoserver,ios ssh client

上篇文章 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函数行见下图:

Jn6vIfR.png!web

按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(之前的链表头) 继续往下走:

U7zmYr7.png!web

运行到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:

NJfuiqv.png!web 用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跟踪一下到: 运行到下图:

uIVBZ3j.png!web

disass一下看一下CALLFN(. call32, 32)所指向的指令:

VFBZfmr.png!web

绿框处所对应的的就是源文件中的代码:

TEXT callRet<>(SB), NOSPLIT, $32-0
复制代码

那红框ret处就是reflectcall的返回。打到断点到ret处。 执行到这里见下图:

QNVfErA.png!web

ret的作用是pop 栈顶到rip,我们看一下rsp中的内容是啥?

mIN7n2r.png!web0x423025

所指向的内容:

m2QZjeM.png!web 图y和上面的图x的地址一样的,就是reflectcall指令的下条指令。再看一下源文件下行代码是啥? p.argp = nil 翻译成汇编代码就是图y中的 mov QWROD PTR [rsp+0x58],0x0

,就是变量赋值会把值存入栈中而不是寄存器中。

yi6VJje.png!web

执行完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()打上断点直接运行到这里见下图:

zEBzIf3.png!web 确实拦截到了gopanic,看一下它的调用链: main.main => runtime.sigpanic() => runtime.panicmem() => gopanic()。 那为什么汇编中没有sigpanic()入口还能调用这个函数呢? 看一下 *pi = 100

生成的汇编代码:

32uiU3i.png!web

划红线处: 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示例:

iIraqe3.png!web

输出: 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中到底是啥。看一下绿框中的指令:

RfUneib.png!web defer关键字会翻译成call runtime.deferproc那它下方绿框中的是runtime.deferproc后面的指令是编译器生成的(也可以这么理解,defer关键字会让编译器生成deferproc函数指令及后面一堆指令)第一行: test eax, eax 的地址是 0x4872d5

稍后会再次说到这个指令及地址。

继续断点执行到 d.pc = callerpc 之后,我们看一下 d.pc 到底是什么值,见下图:

7nEnUr2.png!web0x4872d5 这不是刚刚说的上图绿框处 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不就是返回的意思吗,见下图。

YFRnAbU.png!web

再看最后一条指令:

JMP BX
复制代码

看一下BX到底是啥:

zeieMjI.png!web 绿框处就是BX的值,也就是要jmp到这个地址处执行,这个地址眼熟吗,不就是刚提到的 0x4872d5 吗,对应的指令是 test eax,eax

。再重看一下这个图:

V7vquiY.png!web

其中绿框第一行就是要跳转的地址。刚才说了AX已经变成了1。那下方的两行指令

test eax, eax 
jne 0x4872f9
复制代码

的意思是如果eax不等于0就跳转到这个地址否则就去执行绿框处第三行的正常流程。因为eax已经不等0了,所以就会跳转到 0x4872f9 这个地址处,跟踪一下这个地址指向的是哪里,见下图:

mqQnIvF.png!web 原来它调用了 runtime.deferreturn()

函数,见下图。

RRB3Ynr.png!web

执行到这里。

sp := getcallersp() sp是调用者的sp。就是即将调用 defer func() { 时的sp。 d.sp 是调用链上第二个defer,因为第一个deferd已经脱链。 显然这两个不相等,所以return了,具体return底层到底是如何将re()的返回地址返回的就不在跟踪了。然后执行到了下放的入口地址处:

fmt.Println("After recovery!")
复制代码

整个流程,参看下图代码然后解释:

2Mjq6fm.png!web

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")
}
复制代码

参考: Go语言panic/recover的实现


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK