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 <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完成了加法操作,并将最终的结果直接写入到了堆空间存放数据的地址。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容