golang 疑问1:defer 调用之后到底发生了什么

先来看一个个函数,并猜猜它们的返回值

func f1() (result int) {
    defer func() {
        result *= 7
    }()
    return 6
}

尝试打印这个函数的返回值看看

fmt.Println(f1())
//stdout:42 

Why ‘42’ ???


‘42’ 的前世今生

首先,我们先用最简单的方式:单步调试看一下返回值的变化情况。

3f0076f6334f2d77907291a3c4dfa4d9.png
figure-1

55d020405d2e5102f15222240404124a.png
figure-2

ebbfa320125006bab01e2c7cbf0fe2e4.png
figure-3

69b503f7f002a832d18ac4ca51d55fc6.png
figure-4

通过上面的简单截图,我们可以看到:

  1. 返回值result初始值为 0
  2. 程序先在 figure-2 的时候,通过语句 return 6 将返回值修改为 6
  3. 然后在 figure-3的时候开始执行 defer 闭包
  4. 在 defer 执行完之后,返回值被修改成了42

通过上面简单分析,我们可以看到是 defer 最终修改了返回值,但是要想知道修改的机制,就要看下汇编了。

汇编分析

首先通过命令 GOOS=linux GOARCH=amd64 go tool compile -l -S main.go 获得如下汇编代码(注释是我自己加的):

"".f1 STEXT size=124 args=0x8 locals=0x20
    0x0000 00000 (main.go:7)    TEXT    "".f1(SB), ABIInternal, $32-8 // 生命函数即将分配的 栈帧32字节 参数8字节
    0x0000 00000 (main.go:7)    MOVQ    (TLS), CX // 这三行是检测栈空间是否足够,不够的话会跳转到runtime.morestack_noctxt(SB) 申请更多空间
    0x0009 00009 (main.go:7)    CMPQ    SP, 16(CX)
    0x000d 00013 (main.go:7)    JLS    117
    0x000f 00015 (main.go:7)    SUBQ    $32, SP // 申请32字节栈空间
    0x0013 00019 (main.go:7)    MOVQ    BP, 24(SP) // 保存 caller BP ,供函数执行完成之后恢复 caller 使用
    0x0018 00024 (main.go:7)    LEAQ    24(SP), BP // 将f1() 的栈低地址传入BP寄存器
    0x001d 00029 (main.go:7)    MOVQ    $0, "".result+40(SP) //赋初始值0给 返回参数 result
    0x0026 00038 (main.go:8)    MOVL    $8, (SP) // 将 deferproc 第一个参数压栈,runtime.panic.go:92 deferproc(siz int32, fn *funcval)
    0x002d 00045 (main.go:8)    LEAQ    "".f1.func1·f(SB), AX //保存匿名函数 AX
    0x0034 00052 (main.go:8)    MOVQ    AX, 8(SP) // 压栈
    0x0039 00057 (main.go:8)    LEAQ    "".result+40(SP), AX // 保存返回值地址 AX
    0x003e 00062 (main.go:8)    MOVQ    AX, 16(SP) //压栈, 和下方的deferproc 没太多关系,deferproc 只接受2个参数
    0x0043 00067 (main.go:8)    CALL    runtime.deferproc(SB) //调用 deferproc
    0x0048 00072 (main.go:8)    TESTL    AX, AX
    0x004a 00074 (main.go:8)    JNE    101
    0x004c 00076 (main.go:11)    MOVQ    $6, "".result+40(SP) //将数字6传入 返回参数 result
    0x0055 00085 (main.go:11)    XCHGL    AX, AX // 据说是只是为了填充指令 nop
    0x0056 00086 (main.go:11)    CALL    runtime.deferreturn(SB) //执行匿名函数, 在 runtime.panic.go:351 中,getcallersp() 获取了当前函数栈,
    0x005b 00091 (main.go:11)    MOVQ    24(SP), BP //恢复BP寄存器
    0x0060 00096 (main.go:11)    ADDQ    $32, SP // 释放32字节栈空间
    0x0064 00100 (main.go:11)    RET
    0x0065 00101 (main.go:8)    XCHGL    AX, AX
    0x0066 00102 (main.go:8)    CALL    runtime.deferreturn(SB)
    0x006b 00107 (main.go:8)    MOVQ    24(SP), BP
    0x0070 00112 (main.go:8)    ADDQ    $32, SP
    0x0074 00116 (main.go:8)    RET
    0x0075 00117 (main.go:8)    NOP
    0x0075 00117 (main.go:7)    CALL    runtime.morestack_noctxt(SB)
    0x007a 00122 (main.go:7)    JMP    0

"".f1.func1 STEXT nosplit size=20 args=0x8 locals=0x0 // 这里的栈结构要看 runtime.panic.go:345 deferreturn(arg0 uintptr)
    0x0000 00000 (main.go:8)    TEXT    "".f1.func1(SB), NOSPLIT|ABIInternal, $0-8
    0x0000 00000 (main.go:9)    MOVQ    "".&result+8(SP), AX // result 指针放入AX
    0x0005 00005 (main.go:9)    MOVQ    (AX), CX // AX指向的值放入 CX
    0x0008 00008 (main.go:9)    LEAQ    (CX)(CX*2), DX //DX = 2*CX + CX  -> 2*result + result = 3*result
    0x000c 00012 (main.go:9)    LEAQ    (CX)(DX*2), CX //CX = 2*DX + CX  -> 2*(3*result) + result = 7*result
    0x0010 00016 (main.go:9)    MOVQ    CX, (AX) // 将AX指向的值更新为 7*result
    0x0013 00019 (main.go:10)    RET

--未完待续--

参考连接

https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md
http://xargin.com/plan9-assembly/
https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf
https://github.com/chai2010/advanced-go-programming-book/tree/master/ch3-asm
http://mcll.top/2018/04/15/golang%E4%B8%AD%E7%9A%84defer%E5%AE%9E%E7%8E%B0/

如果计算机体系不太熟,建议先看看这个

https://www.bilibili.com/video/av31289365/?p=6
http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html

标签: none
返回文章列表 文章二维码
本页链接的二维码
打赏二维码
添加新评论