1.例反汇编的两种命令
go tool compile -S a.go 或 go tool compile -N -l a.go go tool objdump a.o
Tips:两者的区别是汇编格式不一样,
-N -l
加上去除优化和内联选项
2.go语言函数调用栈
2.1 CALL 和 RET 作用
当 A 函数调用 B 函数时,编译器就会对应生成一条 CALL 指令,程序执行到 CALL 指令时,就会跳转到被调用函数的入口处开始执行,每个被调用的函数执行结束会有一条 RET 指令,负责函数结束后跳回调用处
3.go语言函数调用规约
caller +------------------+ | | +----------------------> -------------------- | | | | | caller parent BP | | BP(pseudo SP) -------------------- | | | | | Local Var0 | | -------------------- | | | | | ....... | | -------------------- | | | | | Local VarN | -------------------- caller stack frame | | | callee arg2 | | |------------------| | | | | | callee arg1 | | |------------------| | | | | | callee arg0 | | ----------------------------------------------+ FP(virtual register) | | | | | | return addr | parent return address | +----------------------> +------------------+--------------------------- <-------------------------------+ | caller BP | | | (caller frame pointer) | | BP(pseudo SP) ---------------------------- | | | | | Local Var0 | | ---------------------------- | | | | Local Var1 | ---------------------------- callee stack frame | | | ..... | ---------------------------- | | | | | Local VarN | | SP(Real Register) ---------------------------- | | | | | | | | | | | | | | | | +--------------------------+ <-------------------------------+ callee
----------------- current func arg0 ----------------- <----------- FP(pseudo FP) caller ret addr +---------------+ | caller BP(*) | ----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置) | Local Var0 | ----------------- | Local Var1 | ----------------- | Local Var2 | ----------------- - | ........ | ----------------- | Local VarN | ----------------- | | | | | temporarily | | unused space | | | | | ----------------- | call retn | ----------------- | call ret(n-1)| ----------------- | .......... | ----------------- | call ret1 | ----------------- | call argn | ----------------- | ..... | ----------------- | call arg3 | ----------------- | call arg2 | |---------------| | call arg1 | ----------------- <------------ hardware SP 位置 | return addr | +---------------+
函数执行时需要有足够的内存空间,供它存放局部变量、参数、返回值,这段空间对应到虚拟地址空间的栈
4.defer实现原理分析
4.1 例子1
package main import "fmt" func main(){ var a,b int b = incr(a) fmt.Println(a,b) } func incr(a int) (b int) { defer func() { a++ b++ }() a++ return a }
函数调用栈示意图:
4.2 例子2
package main import "fmt" func main(){ a,b := 1,2 c := incr(a,b) fmt.Println(c) } func incr(m,n int) int { var b int a := m + n defer func(x int) { a = a + x b++ }(a + 1) a = 99 b = a return b }
函数调用栈示意图:
使用如下命令查看函数调用栈的汇编:
go tool compile -N -l a.go go tool objdump a.o
5.go panic 和 recover 源码分析
5.1 Goroutine结构体示意图
- panic 能立即终止程序,并在当前 Goroutine 中递归执行调用方的 defer
- recover 可以终止 panic 造成的程序崩溃,并且它只能在defer中起到作用
5.2 panic结构体
5.3 defer结构体
5.4 例子1
例子 t1.go
package main import "fmt" func main() { defer A() defer B() defer C() panic("panic main") fmt.Println("func main") } func A() { fmt.Println("func A") } func B() { fmt.Println("func B") } func C(){ fmt.Println("func C") }
使用如下命令可以查看对应的底层汇编和函数调用栈信息:
go tool compile -N -l t1.go go tool objdump t1.o
可以看出来遇到 panic
时,底层调用的是 runtime.gopanic
,其对应的函数做如下事情:
- (1)往Goroutine的panic链表表头插入panic
- (2)循环执行Goroutine的_defer链表
- (3)_panic 执行 defer时会先把 started 字段置为 true
- (4)把 _defer 结构体 _panic 字段指针指向当前的执行 _panic 的结构体
情况一:panic 正常执行defer 情况二:panic 执行 defer 中有 panic
5.5 panic执行defer中有panic
例子 t2.go
package main import "fmt" func main() { defer A() defer B() defer C() panic("panic main") fmt.Println("func main") } func A() { fmt.Println("func A") } func B() { panic("panic B") fmt.Println("func B") } func C(){ fmt.Println("func C") }
- 该例子前面步骤和例子 t1.go 中一样,defer C节点被正常执行会被移除
- 执行到 defer B 的时候,该defer 节点B.startred=true,B.panic = &panicB, 并且 func B() 函数中有 panic,会调用底层 runtime.gopanic,执行和t1.go中相同的动作
- func B() 中的 panicB 会向 goroutine 中的 _panic 链表头部插入 panicB 节点
- panicB 会遍历 defer 链表,并且遍历到第一个 defer B 的时候,发现 defer B.startred 字段已经被设置为 true(是之前由panicA 设置的),此时就会把defer B.panic 指向的 panicA 结构体中的 aborted 字段设置为true,表示 panicA 被终止了
- 移除 defer B节点,继续正常执行 defer A
- 接下来打印 panic 信息,_panic 链表是从链表尾部向头部打印的,所以是先打印 panicA,在打印 panicB的信息
5.6 panic执行defer中有recover
例子t3.go
package main import "fmt" func main() { defer A() defer B() defer C() panic("panic main") fmt.Println("func main") } func A() { x := recover() fmt.Println("func A,recover:",x) } func B() { panic("panic B") fmt.Println("func B") } func C(){ fmt.Println("func C") }
- 该例子中前面步骤参考 t1.go 和 t2.go,最后panicB 在遍历defer A 节点时,触发 func A() 函数,该函数中,有recover函数
- recover() 函数只做一件事,把当前执行的panic节点的 recovered字段设置为true,并且此时recover()函数捕获到的信息为panicB的信息,即x=”panic B”,该例子中 func A() 函数中的 recover 设置的是 panicB节点中的 recovered字段,此时panicB节点已经被恢复,就会被移除,移除后会跳出panicB的处理流程,接着panicA的处理流程
5.7 panic执行defer中有recover&panic
例子t4.go
package main import "fmt" func main() { defer A() defer B() defer C() panic("panic main") fmt.Println("func main") } func A() { x := recover() fmt.Println("func A,recover:",x) panic("panic A") } func B() { panic("panic B") fmt.Println("func B") } func C(){ fmt.Println("func C") }
- 该例子中前面步骤参考 t1.go、t2.go、t3.go,当执行到 recover 函数时,panic B被recover()函数设置为已回复,但recover()函数后面还有 panicR
- panicR 会在goroutine 头部插入 panic 节点,并且 panicR 也会执行底层 runtime.gopanic函数,遍历defer 链表,发现此时 defer A startred 为 true,且会把 defer A链表指向的 panic B设置为已终止,并且移除 defer A,此时 panic B 的 aborted 和 recovered 都是true
- 接下来就是打印 panic 信息,和之前一样,_panic 链表也是从链表尾部开始的,首先打印 panic A的信息,由于 panic B被恢复了,所以先打印 panic B的信息时候,会提示[recovered],然后打印 panic R