golang中的panic,recover執行過程?

上篇文章golang中defer的執行過程是怎樣的?介紹了一下defer的執行過程,本篇是上一篇的引伸,主要介紹panic、recover的底層分析,若是沒有讀過上一篇文章,能夠先去讀一下在看這篇。 總共分3部分講解:linux

1 panic

2 defer panic

3 defer panic recover

環境:go version go1.12.5 linux/amd64golang

1 panic

golang中的異常總共分爲4中:sass

  • 編譯器捕獲的
  • 直接手動panic
  • golang捕獲的
  • 系統捕獲的
編譯器捕獲的

1/0 咱們知道被除數是不能等於0的,因此這種錯誤是編譯不過去的,會提示: ./main.go:7:8: division by zerobash

直接手動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)實現,先粗略看一下代碼的含義一些解釋在代碼中已經註解:post

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,去看一下定義:ui

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跟蹤一下上面的源碼示例。this

go build -o main gdb mainatom

進入gdb界面並斷點到panic函數行見下圖:

圖1
按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(以前的鏈表頭) 繼續往下走:

圖2
運行到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:

圖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跟蹤一下到: 運行到下圖:

圖片.png
disass一下看一下CALLFN(. call32, 32)所指向的指令:
圖片.png
綠框處所對應的的就是源文件中的代碼:

TEXT callRet<>(SB), NOSPLIT, $32-0
複製代碼

那紅框ret處就是reflectcall的返回。打到斷點到ret處。 執行到這裏見下圖:

圖片.png
ret的做用是pop 棧頂到rip,咱們看一下rsp中的內容是啥?
圖片.png
0x423025 所指向的內容:
圖y
圖y和上面的圖x的地址同樣的,就是reflectcall指令的下條指令。再看一下源文件下行代碼是啥? p.argp = nil 翻譯成彙編代碼就是圖y中的 mov QWROD PTR [rsp+0x58],0x0,就是變量賦值會把值存入棧中而不是寄存器中。
圖片.png
執行完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()打上斷點直接運行到這裏見下圖:

圖片.png
確實攔截到了gopanic,看一下它的調用鏈: main.main => runtime.sigpanic() => runtime.panicmem() => gopanic()。 那爲何彙編中沒有sigpanic()入口還能調用這個函數呢? 看一下 *pi = 100生成的彙編代碼:
圖片.png

劃紅線處: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示例:

圖片.png
輸出: 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中究竟是啥。看一下綠框中的指令:

圖片.png
defer關鍵字會翻譯成call runtime.deferproc那它下方綠框中的是runtime.deferproc後面的指令是編譯器生成的(也能夠這麼理解,defer關鍵字會讓編譯器生成deferproc函數指令及後面一堆指令)第一行: test eax, eax的地址是 0x4872d5稍後會再次說到這個指令及地址。

繼續斷點執行到d.pc = callerpc以後,咱們看一下d.pc究竟是什麼值,見下圖:

圖片.png
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不就是返回的意思嗎,見下圖。

圖片.png

再看最後一條指令:

JMP BX
複製代碼

看一下BX究竟是啥:

圖片.png
綠框處就是BX的值,也就是要jmp到這個地址處執行,這個地址眼熟嗎,不就是剛提到的 0x4872d5嗎,對應的指令是 test eax,eax。再重看一下這個圖:
圖片.png
其中綠框第一行就是要跳轉的地址。剛纔說了AX已經變成了1。那下方的兩行指令

test eax, eax 
jne 0x4872f9
複製代碼

的意思是若是eax不等於0就跳轉到這個地址不然就去執行綠框處第三行的正常流程。由於eax已經不等0了,因此就會跳轉到0x4872f9這個地址處,跟蹤一下這個地址指向的是哪裏,見下圖:

圖片.png
原來它調用了 runtime.deferreturn()函數,見下圖。
圖片.png
執行到這裏。

sp := getcallersp() sp是調用者的sp。就是即將調用defer func() {時的sp。 d.sp 是調用鏈上第二個defer,由於第一個deferd已經脫鏈。 顯然這兩個不相等,因此return了,具體return底層究竟是如何將re()的返回地址返回的就不在跟蹤了。而後執行到了下放的入口地址處:

fmt.Println("After recovery!")
複製代碼

整個流程,參看下圖代碼而後解釋:

圖片.png

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的實現

相關文章
相關標籤/搜索