在《Swift匯編分析閉包-內(nèi)存布局》中介紹了閉包表達(dá)式和閉包之間的區(qū)別,同時(shí)也知道了閉包在內(nèi)存中的布局方式,那么這篇文章是對(duì)其的補(bǔ)充,主要是通過匯編來窺探閉包的調(diào)用。
廢話也不多說了,我們就直接看代碼吧。
/**** 閉包 *********/
typealias Fn = (Int) -> Int
func exec() -> Fn {
var a:Int = 10
func plus(_ i: Int) -> Int {
a += i
return a
}
return plus
}
我們知道,閉包是一個(gè)函數(shù)以及其捕獲的外部變量合稱為閉包,那么我們?cè)谑褂脮r(shí)如下,即獲取函數(shù)對(duì)象并調(diào)用。
var fn = exec()
var a = fn(1)
但我們平時(shí)只是用了,并沒有想過當(dāng)前的fn到底是什么。
首先我們來看一下當(dāng)前fn所占字節(jié)大小,可以得到結(jié)果是16個(gè)字節(jié)。
從代碼上看我們調(diào)用exec()此時(shí)返回的是plus函數(shù),那我們是不是可以猜想這16個(gè)字節(jié)中是否存放著函數(shù)的地址?
print(MemoryLayout.stride(ofValue: fn)) //16
既然懷疑存放著函數(shù)的地址,那么我們干脆直接看看函數(shù)的大小。
func sum1(v1: Int, v2: Int) -> Int {
return v1 + v2
}
var fn = sum1
print(MemoryLayout.stride(ofValue: fn)) //16
如上代碼所示,我們可以通過該方式獲取到函數(shù)的大小也是16個(gè)字節(jié)。那么我們通過匯編分析一下當(dāng)前的fn中的數(shù)據(jù)。
在print(MemoryLayout.stride(ofValue: fn))處打上斷點(diǎn),同時(shí)開啟匯編調(diào)試,command + r 運(yùn)行,此時(shí)斷點(diǎn)觸發(fā)

可以看到其實(shí)匯編這里已經(jīng)有了明顯的提示了,右側(cè)顯示了
fn,這里movq %rcx, 0x581d(%rip)是將%rcx中的數(shù)據(jù)放入到%rip + 0x581d(0x1000081C0)中, movq $0x0, 0x581a(%rip)則是將0x0放入到%rip + 0x581a(0x1000081C8)中,這兩個(gè)操作分別都是操作了8個(gè)字節(jié),一起也就是16個(gè)字節(jié)。再結(jié)合前面我們直接打印的fn是占了16個(gè)字節(jié),那么說明這里就是往fn所在的內(nèi)存寫入了數(shù)據(jù)。我們?cè)倏吹?code>7行的指令
leaq 0x134(%rip), %rcx,將一個(gè)地址放入到了%rcx中,而在第8行有將%rcx中的數(shù)據(jù)寫入到到fn的前8個(gè)字節(jié)。也就是前8字節(jié)中存放的是0x100002AD0,我們也可以直接打印fn可以看到一樣,這與匯編中的邏輯相互印證。
(lldb) p fn
() -> () $R0 = 0x0000000100002ad0 SwiftStudy`SwiftStudy.sum1(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:12
但是這里有一個(gè)疑問,為什么會(huì)movq兩次分別寫入數(shù)據(jù),且第二次寫入的還是0,這是因?yàn)?code>movq指令一次只能移動(dòng)8個(gè)字節(jié),但是fn是占用了16個(gè)字節(jié)的,那么這里就只能通過兩次方式寫入數(shù)據(jù)。第二次寫入的0你可以理解為格式化當(dāng)前的內(nèi)存。
上面是單獨(dú)講函數(shù)拿出來分析,那么回到原點(diǎn)此時(shí)分析閉包,但我們現(xiàn)在先不去捕獲外部變量,此時(shí)看看當(dāng)前的函數(shù)返回了什么內(nèi)容。

同樣斷點(diǎn)然后進(jìn)入到匯編部分。

可以看到斷點(diǎn)處
callq了一個(gè)方法,前文也提到過函數(shù)的返回值是放在rax中,那么此時(shí)可以看到第9行將rax內(nèi)的數(shù)據(jù)放入到了一個(gè)全局變量的內(nèi)存中,而第10行則是將rdx中的內(nèi)容放入到了另一塊內(nèi)存中。而且從后面的提示也可以知道是與fn相關(guān)。我們接著看函數(shù)調(diào)用,此時(shí)
si進(jìn)入。
可以看到第
4行是與內(nèi)部的plus函數(shù)相關(guān),此時(shí)也將一個(gè)地址寫入到了rax中,那那么其實(shí)可以推測(cè)這里寫入的應(yīng)該是plus的地址(0x100002D60),再看第6行將ecx中的數(shù)據(jù)寫入到了edx,而edx是rdx的一部分那么這里可以理解將ecx數(shù)據(jù)寫入到了rdx中而ecx是前面第5行異或(異為0,同為1)得到的數(shù)據(jù)(0),那么與前面呼應(yīng),我們退回(執(zhí)行finish指令)到函數(shù)調(diào)用之時(shí)(上文函數(shù)調(diào)用圖片)。此時(shí)我們也看看
rax內(nèi)部存放的數(shù)據(jù)是否與之前內(nèi)部調(diào)用返回的是否一致。
(lldb) register read rax
rax = 0x0000000100002d60 SwiftStudy`plus #1 (Swift.Int) -> Swift.Int in SwiftStudy.exec() -> (Swift.Int) -> Swift.Int at main.swift:100
也可得知存放函數(shù)的地址與數(shù)據(jù)的地址分別是0x100008158 0x100008160也是連續(xù)的。
這種情況是為捕獲外部變量的情況,現(xiàn)在我們來探究看下真正的閉包是怎么處理的。

斷點(diǎn)在
return plus處,進(jìn)入?yún)R編代碼。
首先我們可以看到第9行處在堆空間分配了地址,此時(shí)的返回?cái)?shù)據(jù)應(yīng)該是在rax處,那么我們?cè)谶@里大哥斷點(diǎn)看一下當(dāng)前返回的rax中的內(nèi)容。
(lldb) register read rax
rax = 0x0000000100443a70
也就是此時(shí)堆空間的地址是0x0000000100646980,也可以看做是fn的前8個(gè)字節(jié)中的數(shù)據(jù)。
前面我們也知道返回值是放在rax與rdx中的,那么我們看函數(shù)返回之前,也就是第21行往rdx中寫入了數(shù)據(jù),再看第15行可以得知是將rax中的數(shù)據(jù)寫入到-0x10(%rbp)中然后再將-0x10(%rbp)數(shù)據(jù)寫到rdx中,那么可以推斷rdx中放的就是堆空間的地址,那么我們?cè)?code>22行處斷點(diǎn)看一下rdx中的數(shù)據(jù)。
(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>
同時(shí)我們也查看當(dāng)前rax中存放的數(shù)據(jù),可以看到也是一個(gè)地址值,后面的描述是與plus函數(shù)相關(guān)。
然后我們?cè)?code>plus函數(shù)內(nèi)打上斷點(diǎn),進(jìn)入到函數(shù)內(nèi)部。可以看到當(dāng)前函數(shù)的地址與打印出的函數(shù)地址并不一致,所以前面說的是與plus函數(shù)相關(guān),并不直接是plus函數(shù)

上面我們已經(jīng)弄清楚了fn中總共16個(gè)字節(jié)中的前8個(gè)字節(jié)存放的是函數(shù)地址,后8個(gè)字節(jié)存放的是堆空間的地址。那么在fn(1)處斷點(diǎn),查看其是怎么調(diào)用函數(shù)的。

前文也提到過函數(shù)調(diào)用是通過callq指令來調(diào)用的,那么我們這里調(diào)用fn實(shí)際上是調(diào)用了內(nèi)部函數(shù)的地址,也就是會(huì)從fn中取其前8個(gè)字節(jié)調(diào)用,那么說明callq調(diào)用的不會(huì)是一個(gè)固定的地址而應(yīng)該是一個(gè)動(dòng)態(tài)的地址,比如從rax中取出來之類的地址,那么我們這里直接找callq指令且其后面并非固定地址的地方,顯而易見可以看到第33行處callq *%rax,而rax中的數(shù)據(jù)則是從-0x40(%rbp)來的,再看第24行-0x40(%rbp)中的內(nèi)容是從rax中而來,而在看第21行可以得知rax中的數(shù)據(jù)是取自于fn的前8個(gè)字節(jié)也就是函數(shù)地址。同樣我們?cè)诳?code>fn的后8個(gè)字節(jié)中的數(shù)據(jù)的存放是經(jīng)故宮rcx后最終落到了r13中,而r13一般是作為函數(shù)參數(shù)的寄存器使用(參照前文)

我們上面說了函數(shù)的調(diào)用以及堆空間的數(shù)據(jù)傳遞,但是我們這里調(diào)用fn(1)還會(huì)再傳另外一個(gè)參數(shù),那么這個(gè)參數(shù)時(shí)如何傳遞的呢,其實(shí)我們?cè)倏吹?code>30行,這里會(huì)將1放入到edi(rdi)寄存器內(nèi),也就是傳參1到函數(shù)內(nèi)部。
進(jìn)入到函數(shù)調(diào)用內(nèi)部,可以看到說明是apply for plus 與之前看到的描述一致,且其會(huì)通過jmp指令跳轉(zhuǎn)到真正的plus函數(shù),同時(shí)也可以看到這里會(huì)將r13內(nèi)的數(shù)據(jù)放入到rsi中,其他的寄存器中并沒有去做修改。
我們現(xiàn)在plus函數(shù)內(nèi)部是做了加法計(jì)算,那么在做加法計(jì)算的時(shí)候是如何訪問到堆空間的數(shù)據(jù)的呢。
通過前文分析知道通過獲取fn的前8個(gè)字節(jié)調(diào)用了plus函數(shù),將后8個(gè)字節(jié)通過rsi寄存器傳參,外部參數(shù)1則通過rdi寄存器傳遞。
進(jìn)入到plus函數(shù)內(nèi)部調(diào)用。
rsi -> 堆空間地址值
rdi -> 外部參數(shù)(1)

11行,將rsi中的數(shù)據(jù)放入到了-0x50(%rbp)36行,將-0x50(%rbp)放入到了rdx中37行,進(jìn)行了加操作指令,取出0x10(%rdx)與%rcx相加
再看第9行,將%rdi中的數(shù)據(jù)放入到了-0x48(%rbp)中
23行,將-0x48(%rbp)放入到rcx中
至此到36行rcx中的數(shù)據(jù)未曾改變,也就是這個(gè)時(shí)候其值就是1
那么到37行也就是rcx=rcx + 0x10(%rdx) 也就是plus`函數(shù)內(nèi)部的邏輯。

計(jì)算完成后計(jì)算得到的新值仍然放在了
rcx中,此時(shí)我們看第44行會(huì)將rcx中的數(shù)據(jù)放入到rax的地址中,那么rax值時(shí)在第42行由-0x70(%rbp)而來,而-0x70(%rbp)則是在第31行從rdx獲取到的,結(jié)合第11行和第25行可以得知rdx中存放的就是堆空間的地址,但是因?yàn)槠淝?code>16個(gè)字節(jié)分別存放了"類"和引用計(jì)數(shù)相關(guān)的信息,因此在第26對(duì)其地址做了一個(gè)偏移操作,直接指向了數(shù)據(jù)位,所以最終就是將計(jì)算結(jié)果存放到了數(shù)據(jù)位中。至此整個(gè)閉包的調(diào)用流程基本清晰了。
首先我們通過查看其內(nèi)存大小知道閉包函數(shù)總共是占用了
16個(gè)字節(jié),其中前8個(gè)字節(jié)是“函數(shù)地址”,后8個(gè)字節(jié)是堆空間地址,然后在調(diào)用fn時(shí)實(shí)際上是調(diào)用了其內(nèi)部的函數(shù),而這個(gè)函數(shù)并非真正的plus函數(shù)而是在其內(nèi)部間接調(diào)用了plus函數(shù),然后在plus函數(shù)內(nèi)部則調(diào)用addq完成了加法操作,并將最終的結(jié)果直接寫入到了堆空間存放數(shù)據(jù)的地址。