deferrenturn如何实现循环处理

本文将条分缕析地说明deferreturn循环执行的原理


deferrenturn如何实现循环处理

我们知道当程序使用了defer关键字后,会在调用defer处注册defer函数,并在函数RET前执行deferreturn函数;那一个deferreturn函数如何把所有的defer函数全部执行完呢? 我们可以首先看一下deferreturn函数:

// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
// Cannot split the stack because we reuse the caller's frame to
// call the deferred function.
// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) { // arg0是defer函数的参数位置,这个位置应该是caller函数的堆栈内
    gp := getg()
    d := gp._defer
    if d == nil {
        return
    }
    sp := getcallersp()
    if d.sp != sp {
        return
    }
    // Moving arguments around.
    //
    // Everything called after this point must be recursively
    // nosplit because the garbage collector won't know the form
    // of the arguments until the jmpdefer can flip the PC over to
    // 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)) // 将_defer结构中保存的参数再拷贝回caller队长内
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link // 去掉该链表节点
    freedefer(d) // 清空_defer结构,并将该结构存储回P的deferpool中,注意deferpool是属于P的
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

如果从deferreturn这个函数的角度来看,这是里并没有在函数调用上有什么特殊处理,秘密都来自jmpdefer函数,下面列出jmpdefer函数代码:

// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
	MOVQ	fv+0(FP), DX	// fn,FP是位寄存器,指向calller传递给callee的第一个参数,因此这个参数目前就是funcval指针
	MOVQ	argp+8(FP), BX	// caller sp;我们知道这个argp其实是deferreturn函数参数
	LEAQ	-8(BX), SP	// caller sp after CALL; 而又因为我们是在模拟调用jmpdefer,所以还需要有return address,因此这里将BX-8作为调用后的SP; 取(BX-8)地址所指向的值再取地址作为SP,相当于是BX值减去8赋值给SP,那就是caller的SP
	MOVQ	-8(SP), BP	// restore BP as if deferreturn returned (harmless if framepointers not in use);这里是计算caller的BP值,相当于是将SP-8处的值赋值给BP,相当于是复原了caller的BP
	SUBQ	$5, (SP)	// return to CALL again; 
	MOVQ	0(DX), BX 取fucnval的值给BX寄存器,funcval中的第一参数就是函数地址值
	JMP	BX	// but first run the deferred function; 调用被defer的函数

为了后续更好描述,我们首先给几个函数定义一下专有名词:使用了defer关键字的函数是caller,被defer执行的函数是deferred func; 下面开始解读函数jmpdefer: 在deferreturn函数中的jmpdefer函数有两个参数fn和uintptr(unsafe.Pointer(&arg0)),第一个参数是封装了deferred func的funcval结构体指针,第二个参数是deferred func所要使用的第一个参数的指针; MOVQ fv+0(FP), DX将第一个参数的地址赋给了BX寄存器;如果要实现caller直接调用deferred func的效果,那么CALL指令还会在caller的堆栈中,所以LEAQ -8(BX), SP相当于是重置SP将其往下移8个字节从而把return address的位置留出来; MOVQ -8(SP), BP这个命令相当于恢复caller的BP;-8(SP)其实是deferred func被调用时的BP,这个BP存储的就是caller的BP,所以MOVQ -8(SP), BP相当于是读取【SP指针的值再减8后】的地址并赋值给BP,这相当于是POP BP的操作了,该指令执行过后,BP、SP寄存器都恢复成了caller自己的BP和SP; 下面这一句是deferreturn可以循环执行的关键,原谅我之前的才疏学浅这里之前怎么也理解不了,多亏拜读了该文章————defer 链如何被遍历后,醍醐灌顶; SUBQ $5, (SP)把返回地址减少了 5B,刚好是一个 CALL 指令的长度(我们前文说过SP当前指向的位置是caller调用deferreturn函数后的下一条指令,相当于是又将return address指向了指令CALL deferreturn这个指令,那么在调用完deferred func后还会再次执行deferreturn函数)。

引用一下上述文章中的段落:

什么意思?当执行完 deferreturn 函数之后,执行流程会返回到 CALL deferreturn 的下一条指令,将这个值减少 5B,也就又回到了 CALL deferreturn 指令,从而实现了“递归地”调用 deferreturn 函数的效果。当然,栈却不会在增长!

大家可能对5B比较疑惑,为什么减了5Bytes就是CALL deferreturn指令了,可以go tool compile来看一下相应的汇编代码:

        0x005f 00095 (main.go:11)       JMP     97
        0x0061 00097 (main.go:12)       XCHGL   AX, AX
        0x0062 00098 (main.go:12)       CALL    runtime.deferreturn(SB)
        0x0067 00103 (main.go:12)       MOVQ    80(SP), BP
        0x006c 00108 (main.go:12)       ADDQ    $88, SP

通过上面的汇编代码可以清晰地看到,CALL runtime.deferreturn(SB)占据了5Bytes的地址

下面两条语句比较简单:MOVQ 0(DX), BX解引用*funcval值的并赋值给BX,通过查看funcval结构可知现在BX就是存储了函数地址:

type funcval struct {
	fn uintptr
	// variable-size, fn-specific data here
}

JMP BX跳转到相应的函数地址进行执行,jmpdefer函数完成了caller调用deferred func的功能,个人觉得整个过程真实精彩!