汇编简单窥探一下闭包

319 阅读5分钟

1.闭包定义

闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。闭包是一个引用类型。

关键字:捕获,常量变量,引用。

2.分析闭包

闭包例子

typealias Fn = (Int) -> Int;
func getFn() -> Fn {
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i;
        return num;
    }
    
    return plus;
}

var fn = getFn()

print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))

先猜一下会打印什么,和你想象中的结果一样吗? getFn是一个函数,num是一个局部变量,为什么会累加呢?

汇编分析:

断点放在return plus处。

SwiftTest`getFn():
    0x100002a40 <+0>:  pushq  %rbp
    0x100002a41 <+1>:  movq   %rsp, %rbp
    0x100002a44 <+4>:  subq   $0x20, %rsp
    0x100002a48 <+8>:  movq   $0x0, -0x8(%rbp)
    0x100002a50 <+16>: leaq   0x15d1(%rip), %rdi
    0x100002a57 <+23>: movl   $0x18, %esi
    0x100002a5c <+28>: movl   $0x7, %edx
 -> 0x100002a61 <+33>: callq  0x100003c8e               ; symbol stub for: swift_allocObject
    0x100002a66 <+38>: movq   %rax, %rcx
 -> 0x100002a69 <+41>: addq   $0x10, %rcx
    0x100002a6d <+45>: movq   %rcx, -0x8(%rbp)
    0x100002a71 <+49>: movq   $0x0, 0x10(%rax)
->  0x100002a79 <+57>: movq   %rax, %rdi
    0x100002a7c <+60>: movq   %rax, -0x10(%rbp)
    0x100002a80 <+64>: callq  0x100003cac               ; symbol stub for: swift_retain
    0x100002a85 <+69>: movq   -0x10(%rbp), %rdi
    0x100002a89 <+73>: movq   %rax, -0x18(%rbp)
    0x100002a8d <+77>: callq  0x100003ca6               ; symbol stub for: swift_release
    0x100002a92 <+82>: leaq   0x137(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100002a99 <+89>: movq   -0x10(%rbp), %rdx
    0x100002a9d <+93>: addq   $0x20, %rsp
    0x100002aa1 <+97>: popq   %rbp
    0x100002aa2 <+98>: retq  

  • callq 0x100003c8e ; symbol stub for: swift_allocObject,可以看到是getFun函数里面开辟的堆空间。并且返回%rax

  • movq $0x0, 0x10(%rax),把0放到%rax+0x10,也就是rax的第16个字节位置

这时候,我们先记录下来rax的内存地址,和内存地址的值

(lldb) register read rax
     rax = 0x0000000100522450

(lldb) x/4xg 0x0000000100522450
0x100522450: 0x0000000100004028 0x0000000000000003
0x100522460: 0x0000000000000000 0x00007fff2384f465

可以看出来,rax的16个字节处为0。

然后断点放在return num;

SwiftTest`plus #1 (_:) in getFn():
    0x100002ad0 <+0>:   pushq  %rbp
    0x100002ad1 <+1>:   movq   %rsp, %rbp
    0x100002ad4 <+4>:   subq   $0x90, %rsp
    0x100002adb <+11>:  xorl   %eax, %eax
    0x100002add <+13>:  movl   %eax, %ecx
    0x100002adf <+15>:  xorl   %eax, %eax
    0x100002ae1 <+17>:  leaq   -0x8(%rbp), %rdx
    0x100002ae5 <+21>:  movq   %rdi, -0x48(%rbp)
    0x100002ae9 <+25>:  movq   %rdx, %rdi
    0x100002aec <+28>:  movq   %rsi, -0x50(%rbp)
    0x100002af0 <+32>:  movl   %eax, %esi
    0x100002af2 <+34>:  movl   $0x8, %edx
    0x100002af7 <+39>:  movq   %rdx, -0x58(%rbp)
    0x100002afb <+43>:  movq   %rcx, -0x60(%rbp)
    0x100002aff <+47>:  movl   %eax, -0x64(%rbp)
    0x100002b02 <+50>:  callq  0x100003c46               ; symbol stub for: memset
    0x100002b07 <+55>:  leaq   -0x10(%rbp), %rcx
    0x100002b0b <+59>:  movq   %rcx, %rdi
    0x100002b0e <+62>:  movl   -0x64(%rbp), %esi
    0x100002b11 <+65>:  movq   -0x58(%rbp), %rdx
    0x100002b15 <+69>:  callq  0x100003c46               ; symbol stub for: memset
    0x100002b1a <+74>:  movq   -0x48(%rbp), %rcx
    0x100002b1e <+78>:  movq   %rcx, -0x8(%rbp)
    0x100002b22 <+82>:  movq   -0x50(%rbp), %rdx
    0x100002b26 <+86>:  addq   $0x10, %rdx
    0x100002b2d <+93>:  movq   %rdx, -0x10(%rbp)
    0x100002b31 <+97>:  movq   %rdx, %rdi
    0x100002b34 <+100>: leaq   -0x28(%rbp), %rsi
    0x100002b38 <+104>: movl   $0x21, %r8d
    0x100002b3e <+110>: movq   %rdx, -0x70(%rbp)
    0x100002b42 <+114>: movq   %r8, %rdx
    0x100002b45 <+117>: movq   -0x60(%rbp), %rcx
    0x100002b49 <+121>: callq  0x100003c94               ; symbol stub for: swift_beginAccess
    0x100002b4e <+126>: movq   -0x48(%rbp), %rcx
    0x100002b52 <+130>: movq   -0x50(%rbp), %rdx
    0x100002b56 <+134>: addq   0x10(%rdx), %rcx
    0x100002b5a <+138>: seto   %r9b
    0x100002b5e <+142>: testb  $0x1, %r9b
    0x100002b62 <+146>: movq   %rcx, -0x78(%rbp)
    0x100002b66 <+150>: jne    0x100002bc0               ; <+240> [inlined] Swift runtime failure: arithmetic overflow at main.swift:393
    0x100002b68 <+152>: movq   -0x70(%rbp), %rax
    0x100002b6c <+156>: movq   -0x78(%rbp), %rcx
    0x100002b70 <+160>: movq   %rcx, (%rax)
->  0x100002b73 <+163>: leaq   -0x28(%rbp), %rdi
    0x100002b77 <+167>: callq  0x100003ca0               ; symbol stub for: swift_endAccess

这时候,相当于运行到fn(1)里面的plus方法,我们主要看下面这段就好了

    0x100002b49 <+121>: callq  0x100003c94               ; symbol stub for: swift_beginAccess
 -> 0x100002b4e <+126>: movq   -0x48(%rbp), %rcx
    0x100002b52 <+130>: movq   -0x50(%rbp), %rdx
 -> 0x100002b56 <+134>: addq   0x10(%rdx), %rcx
    0x100002b5a <+138>: seto   %r9b
    0x100002b5e <+142>: testb  $0x1, %r9b
    0x100002b62 <+146>: movq   %rcx, -0x78(%rbp)
    0x100002b66 <+150>: jne    0x100002bc0               ; <+240> [inlined] Swift runtime failure: arithmetic overflow at main.swift:393
    0x100002b68 <+152>: movq   -0x70(%rbp), %rax
    0x100002b6c <+156>: movq   -0x78(%rbp), %rcx
 -> 0x100002b70 <+160>: movq   %rcx, (%rax)
->  0x100002b73 <+163>: leaq   -0x28(%rbp), %rdi
    0x100002b77 <+167>: callq  0x100003ca0               ; symbol stub for: swift_endAccess

movq -0x48(%rbp), %rcx

movq -0x50(%rbp), %rdx

 addq 0x10(%rdx), %rcx

局部变量i=1给到% rcx,然后把%rdx+0x10的值也就是堆空间sum=0,相加,为rcx = 1

movq %rcx, (%rax)

rcx赋值给%rax的值

打印一下rax的内容

(lldb) register read rax
     rax = 0x0000000100522460

(lldb) x/xg 0x0000000100522460
0x100522460: 0x0000000000000001

(lldb) x/4xg 0x0000000100522450

0x100522450: 0x0000000100004028 0x0000000200000003
0x100522460: 0x0000000000000001 0x00007fff2384f465


可以看到这里的rax的地址值0x0000000100522460 和我们之前的sum的地址值0x0000000100522450,相差了16个字节,也可以看来sum的地址值的16个字节处由0变为1,sum地址前面16个字节存储的是基础信息和引用计数。

后面的fn(2)也是这样走的流程。

结论

在闭包中,创建一个getFn(),就会开辟一次堆空间来存储捕获变量的值,var fn = getFn()走完后,函数的局部变量sum已经回收了。plus方法的sum其实是在堆空间的sum,记录这为0。

fn(1)后sum更新为1,fn(2)后sum更新为3。fn(3)后sum更新为6。fn(4)后sum更新为10。