上篇文章golang中defer的執行過程是怎樣的?介紹了一下defer的執行過程,本篇是上一篇的引伸,主要介紹panic、recover的底層分析,若是沒有讀過上一篇文章,能夠先去讀一下在看這篇。 總共分3部分講解:linux
環境:go version go1.12.5 linux/amd64golang
golang中的異常總共分爲4中:sass
1/0
咱們知道被除數是不能等於0的,因此這種錯誤是編譯不過去的,會提示: ./main.go:7:8: division by zerobash
示例代碼:數據結構
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函數行見下圖:
按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(以前的鏈表頭)
繼續往下走:
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信息。
例如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。
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最後輸出。
第14行panic -> gopanic() -> reflectcall -> 第12行defer -> reflectcall -> 第8行defer -> 第9行panic -> gopanic -> reflectcall -> 繼續執行deferd鏈上的也就是第6行defer -> fatalpanic(裏面子函數printpanics()遞歸調用g._panic鏈)。
下面介紹的是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")
}
複製代碼