美文网首页
Swift汇编分析闭包-调用原理

Swift汇编分析闭包-调用原理

作者: oldmonster | 来源:发表于2021-05-20 13:33 被阅读0次

    《Swift汇编分析闭包-内存布局》中介绍了闭包表达式和闭包之间的区别,同时也知道了闭包在内存中的布局方式,那么这篇文章是对其的补充,主要是通过汇编来窥探闭包的调用。

    废话也不多说了,我们就直接看代码吧。

    /****   闭包  *********/
    typealias Fn = (Int) -> Int
    
    func exec() -> Fn {
        var a:Int = 10
        func plus(_ i: Int) -> Int {
            a += i
            return a
        }
        return plus
    }
    

    我们知道,闭包是一个函数以及其捕获的外部变量合称为闭包,那么我们在使用时如下,即获取函数对象并调用。

    var fn = exec()
    var a = fn(1)
    

    但我们平时只是用了,并没有想过当前的fn到底是什么。
    首先我们来看一下当前fn所占字节大小,可以得到结果是16个字节
    从代码上看我们调用exec()此时返回的是plus函数,那我们是不是可以猜想这16个字节中是否存放着函数的地址?

    print(MemoryLayout.stride(ofValue: fn))  //16
    

    既然怀疑存放着函数的地址,那么我们干脆直接看看函数的大小。

    func sum1(v1: Int, v2: Int) -> Int {
        return v1 + v2
    }
    var fn = sum1
    print(MemoryLayout.stride(ofValue: fn)) //16
    

    如上代码所示,我们可以通过该方式获取到函数的大小也是16个字节。那么我们通过汇编分析一下当前的fn中的数据。

    print(MemoryLayout.stride(ofValue: fn))处打上断点,同时开启汇编调试,command + r 运行,此时断点触发

    image.png
    可以看到其实汇编这里已经有了明显的提示了,右侧显示了fn,这里movq %rcx, 0x581d(%rip)是将%rcx中的数据放入到%rip + 0x581d(0x1000081C0)中, movq $0x0, 0x581a(%rip)则是将0x0放入到%rip + 0x581a(0x1000081C8)中,这两个操作分别都是操作了8个字节,一起也就是16个字节。再结合前面我们直接打印的fn是占了16个字节,那么说明这里就是往fn所在的内存写入了数据。
    我们再看第7行的指令leaq 0x134(%rip), %rcx,将一个地址放入到了%rcx中,而在第8行有将%rcx中的数据写入到到fn的前8个字节。也就是前8字节中存放的是0x100002AD0,我们也可以直接打印fn可以看到一样,这与汇编中的逻辑相互印证。
    (lldb) p fn
    () -> () $R0 = 0x0000000100002ad0 SwiftStudy`SwiftStudy.sum1(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:12
    

    但是这里有一个疑问,为什么会movq两次分别写入数据,且第二次写入的还是0,这是因为movq指令一次只能移动8个字节,但是fn是占用了16个字节的,那么这里就只能通过两次方式写入数据。第二次写入的0你可以理解为格式化当前的内存。

    上面是单独讲函数拿出来分析,那么回到原点此时分析闭包,但我们现在先不去捕获外部变量,此时看看当前的函数返回了什么内容。


    image.png

    同样断点然后进入到汇编部分。

    函数调用
    可以看到断点处callq了一个方法,前文也提到过函数的返回值是放在rax中,那么此时可以看到第9行将rax内的数据放入到了一个全局变量的内存中,而第10行则是将rdx中的内容放入到了另一块内存中。而且从后面的提示也可以知道是与fn相关。
    我们接着看函数调用,此时si进入。
    函数调用
    可以看到第4行是与内部的plus函数相关,此时也将一个地址写入到了rax中,那那么其实可以推测这里写入的应该是plus的地址(0x100002D60),再看第6行将ecx中的数据写入到了edx,而edxrdx的一部分那么这里可以理解将ecx数据写入到了rdx中而ecx是前面第5异或(异为0,同为1)得到的数据(0),那么与前面呼应,我们退回(执行finish指令)到函数调用之时(上文函数调用图片)。
    此时我们也看看rax内部存放的数据是否与之前内部调用返回的是否一致。
    (lldb) register read rax
         rax = 0x0000000100002d60  SwiftStudy`plus #1 (Swift.Int) -> Swift.Int in SwiftStudy.exec() -> (Swift.Int) -> Swift.Int at main.swift:100
    

    也可得知存放函数的地址与数据的地址分别是0x100008158 0x100008160也是连续的。
    这种情况是为捕获外部变量的情况,现在我们来探究看下真正的闭包是怎么处理的。

    闭包
    断点在return plus处,进入汇编代码。
    image.png

    首先我们可以看到第9行处在堆空间分配了地址,此时的返回数据应该是在rax处,那么我们在这里大哥断点看一下当前返回的rax中的内容。

    (lldb) register read rax
         rax = 0x0000000100443a70
    

    也就是此时堆空间的地址是0x0000000100646980,也可以看做是fn的前8个字节中的数据。
    前面我们也知道返回值是放在raxrdx中的,那么我们看函数返回之前,也就是第21行往rdx中写入了数据,再看第15行可以得知是将rax中的数据写入到-0x10(%rbp)中然后再将-0x10(%rbp)数据写到rdx中,那么可以推断rdx中放的就是堆空间的地址,那么我们在22行处断点看一下rdx中的数据。

    (lldb) register read rdx
         rdx = 0x0000000100443a70
    (lldb) register read rax
         rax = 0x0000000100002ce0  SwiftStudy`partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftStudy.exec() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    

    同时我们也查看当前rax中存放的数据,可以看到也是一个地址值,后面的描述是与plus函数相关。
    然后我们在plus函数内打上断点,进入到函数内部。可以看到当前函数的地址与打印出的函数地址并不一致,所以前面说的是与plus函数相关,并不直接是plus函数

    image.png

    上面我们已经弄清楚了fn中总共16个字节中的前8个字节存放的是函数地址,后8个字节存放的是堆空间的地址。那么在fn(1)处断点,查看其是怎么调用函数的。

    image.png

    前文也提到过函数调用是通过callq指令来调用的,那么我们这里调用fn实际上是调用了内部函数的地址,也就是会从fn中取其前8个字节调用,那么说明callq调用的不会是一个固定的地址而应该是一个动态的地址,比如从rax中取出来之类的地址,那么我们这里直接找callq指令且其后面并非固定地址的地方,显而易见可以看到第33行处callq *%rax,而rax中的数据则是从-0x40(%rbp)来的,再看第24-0x40(%rbp)中的内容是从rax中而来,而在看第21行可以得知rax中的数据是取自于fn的前8个字节也就是函数地址。同样我们在看fn的后8个字节中的数据的存放是经故宫rcx后最终落到了r13中,而r13一般是作为函数参数的寄存器使用(参照前文)

    image.png

    我们上面说了函数的调用以及堆空间的数据传递,但是我们这里调用fn(1)还会再传另外一个参数,那么这个参数时如何传递的呢,其实我们再看第30行,这里会将1放入到edi(rdi)寄存器内,也就是传参1到函数内部。

    进入到函数调用内部,可以看到说明是apply for plus 与之前看到的描述一致,且其会通过jmp指令跳转到真正的plus函数,同时也可以看到这里会将r13内的数据放入到rsi中,其他的寄存器中并没有去做修改。

    我们现在plus函数内部是做了加法计算,那么在做加法计算的时候是如何访问到堆空间的数据的呢。

    通过前文分析知道通过获取fn的前8个字节调用了plus函数,将后8个字节通过rsi寄存器传参,外部参数1则通过rdi寄存器传递。
    进入到plus函数内部调用。

    rsi -> 堆空间地址值
    rdi -> 外部参数(1)
    
    image.png
    11行,将rsi中的数据放入到了-0x50(%rbp)
    36行,将-0x50(%rbp)放入到了rdx
    37行,进行了加操作指令,取出0x10(%rdx)%rcx相加

    再看第9行,将%rdi中的数据放入到了-0x48(%rbp)
    23行,将-0x48(%rbp)放入到rcx
    至此到36rcx中的数据未曾改变,也就是这个时候其值就是1
    那么到37行也就是rcx=rcx + 0x10(%rdx) 也就是plus`函数内部的逻辑。

    image.png
    计算完成后计算得到的新值仍然放在了rcx中,此时我们看第44行会将rcx中的数据放入到rax的地址中,那么rax值时在第42行由-0x70(%rbp)而来,而-0x70(%rbp)则是在第31行从rdx获取到的,结合第11行和第25行可以得知rdx中存放的就是堆空间的地址,但是因为其前16个字节分别存放了"类"和引用计数相关的信息,因此在第26对其地址做了一个偏移操作,直接指向了数据位,所以最终就是将计算结果存放到了数据位中。
    至此整个闭包的调用流程基本清晰了。
    首先我们通过查看其内存大小知道闭包函数总共是占用了16个字节,其中前8个字节是“函数地址”,后8个字节是堆空间地址,然后在调用fn时实际上是调用了其内部的函数,而这个函数并非真正的plus函数而是在其内部间接调用了plus函数,然后在plus函数内部则调用addq完成了加法操作,并将最终的结果直接写入到了堆空间存放数据的地址。

    相关文章

      网友评论

          本文标题:Swift汇编分析闭包-调用原理

          本文链接:https://www.haomeiwen.com/subject/drosdltx.html