「最傷人的是這一切的野蠻侵略、掠奪破壞都是以神的名義做的,而狂熱的人類只是玩具。」
正如之前在汇编代码中看到的,当函数正常结束时,其递归调用了runtime.deferreturn函数遍历defer链,并调用存储在defer中的函数。
func deferreturn(arg0 uintptr) {
gp := getg()
//当前协程defer函数链表
d := gp._defer
if d == nil {
return
}
// 获取调用deferreturn函数时的栈顶位置
sp := getcallersp()
if d.sp != sp {
return
}
// 内联defer调用规则
if d.openDefer {
done := runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
//把保存在_defer对象中的fn函数需要用到的参数复制到栈上,准备调用fn
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
//使gp._defer指向下一个_defer结构体
gp._defer = d.link
//释放_defer结构体
freedefer(d)
// 调用fn函数
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
在遍历defer链表的过程中,有两个重要的终止条件。一个是当遍历到链表的末尾时,最终链表指针变为nil,这时需要终止链表。除此之外,当defer结构中存储的SP地址与当前deferreturn的调用者SP地址不同时,仍然需要终止执行。原因是协程的链表中放入了当前函数调用链所有函数的defer结构,但是在执行时只能执行当前函数的defer结构。例如当前函数的执行链为a()→b()→c(),在执行函数c正常返回后,当前三个函数的defer结构都存储在链表中,但是当前只能够执行函数c中的fc函数。如果发现defer是其他函数的内容,则立即返回。
func a(){
defer fa()
b()
}
func b(){
defer fb()
c()
}
func c(){
defer fc()
...
}
deferreturn获取需要执行的defer函数,需要将当前defer函数的参数重新转移到栈中,调用freedefer销毁当前的结构体,并将链表指向下一个_defer结构体。现在有了函数指针,也有了参数,可以调用jmpdefer完成函数的调用。jmpdefer是一段汇编代码,其在amd64下的汇编代码如下所示。
Jmpdefer函数了比较巧妙的方式实现了对deferreturn函数反复调用。其核心思想是调整了deferreturn函数的SP、BP地址,使deferreturn函数退出之后再次调用deferreturn函数,从而实现循环调用。
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
// jmpdefer函数的第一个参数fn的地址放入DX寄存器
MOVQ fv+0(FP), DX
// jmpdefer函数的第二个参数放入 BX 寄存器
MOVQ argp+8(FP), BX
// 调整SP、BP位置
LEAQ -8(BX), SP
MOVQ -8(SP), BP
// 调整返回地址
SUBQ $5, (SP)
// 继续执行deferreturn
MOVQ 0(DX), BX
JMP BX
上例执行SUBQ $5, (SP) 语句后,对应的栈帧结构如图所示。从图中可以看出,jmpdefer函数通过调整SP、BP寄存器值,已经抛弃了deferreturn的栈帧供后续使用。并且,由于调整了返回地址,jmpdefer函数在执行完毕返回时可以递归调用deferreturn函数,复用了栈空间,不会因为大量调用导致栈溢出。这种策略在协程调用循环中也会使用。
图 递归defer调用的栈帧结构
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8