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

在《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 

同时我们也查看当前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汇编分析闭包-调用原理)