swift為了實(shí)現(xiàn)快這么一個(gè)終極目標(biāo)嘱兼。在許多地方做了大量的優(yōu)化成艘。簡(jiǎn)直可以說(shuō)是集現(xiàn)代編程語(yǔ)言之長(zhǎng)爪飘。而這一點(diǎn)在swift中的方法調(diào)用尤為突出梧喷。我們來(lái)探究一下swift中的方法調(diào)用砌左。
swift中函數(shù)調(diào)用方試
swift語(yǔ)言中總共有三種方法調(diào)用方式:
1.通過(guò)內(nèi)存地址直接調(diào)用
2.通過(guò)v-table這么一個(gè)結(jié)構(gòu)類似數(shù)組的函數(shù)表調(diào)用
3.就是我們非常熟悉的send_msg()消息派發(fā)。
他們的調(diào)用效率是1>2>3铺敌。動(dòng)態(tài)性則是3>2>1汇歹。然后swift在任何時(shí)候都會(huì)優(yōu)先的盡量使用內(nèi)存直接調(diào)用,不能夠使用內(nèi)存地址直接調(diào)用的時(shí)候使用函數(shù)表調(diào)用偿凭。那么什么時(shí)候不能夠使用內(nèi)存直接調(diào)用了产弹,很簡(jiǎn)單函數(shù)可能會(huì)被重寫時(shí)就不能夠使用內(nèi)存直接調(diào)用了,而第三種消息表派發(fā)就是需要使用到runtime的時(shí)候會(huì)使用
stuct的方法調(diào)用
stuct一把都是采用的函數(shù)地址調(diào)用弯囊。在匯編代碼中bl表示跳轉(zhuǎn)到地址痰哨。這就效率最高的函數(shù)調(diào)用方式不用查找,擼起地址直接干匾嘱。但是上面我們看的func2方法添加了一個(gè)mutating的關(guān)鍵字斤斧,要知道它的不同我們進(jìn)入到sil中,能更詳細(xì)的看的swift中做了什么
struct Person {
func func1()
mutating func func2()
init()
}
.....
// Person.func1()
sil hidden [ossa] @$s14ViewController6PersonV5func1yyF : $@convention(method) (Person) -> () {
// %0 "self" // user: %1
bb0(%0 : $Person):
debug_value %0 : $Person, let, name "self", argno 1 // id: %1
.....
// Person.func2()
sil hidden [ossa] @$s14ViewController6PersonV5func2yyF : $@convention(method) (@inout Person) -> () {
// %0 "self" // user: %1
bb0(%0 : $*Person):
debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1
我截取了部分關(guān)鍵的sil代碼霎烙。在這段代碼里面 func1()和func2()第一個(gè)不同點(diǎn)在Person, var, name "self", argno 1。里面的Person取得是地址悬垃。
inout 的作用很簡(jiǎn)單可以讓let 定義的變量可以修改
func func3(age:inout Int){
age = 20
print(age)
}
我們都知道在swift是不可以直接修改參數(shù)值的如果你要強(qiáng)行修改就會(huì)報(bào)這個(gè)錯(cuò)
但是如果我添加了inout關(guān)鍵字就可以修改這個(gè)let定義的變量了游昼。調(diào)用inout修飾的參數(shù)的方法。參數(shù)需要傳地址盗忱。像這樣調(diào)用func3(age: &leoAge)酱床。
那么在sil中func2()方法這里swift隱式的添加@inout的目的也就很明顯了。意思就是可以修改傳遞進(jìn)來(lái)的Person的參數(shù)趟佃。也就是struct本身扇谣。這就是mutaiting修飾的方法可以修改struct變量的原因。
struct中只有內(nèi)存直接調(diào)用與函數(shù)表調(diào)用兩種調(diào)用方式闲昭。因?yàn)橐鞘褂孟⑴砂l(fā)必需繼承自NSObject對(duì)象罐寨。
函數(shù)表調(diào)用
函數(shù)表的方式調(diào)用函數(shù),在下面的截圖中我們可以看到func2()的blr后面跟的不是函數(shù)地址而是一個(gè)變量序矩。那這個(gè)變量swift是怎樣存儲(chǔ)的了鸯绿?我們知道它是怎樣存儲(chǔ)的就知道了它是怎樣調(diào)用的了。我們嘗試著來(lái)找到函數(shù)表中函數(shù)地址的存儲(chǔ)方式。
下面我們需要查看macho文件格式瓶蝴,我們先簡(jiǎn)單介紹一下macho文件的格式
Macho文件格式是這樣的
它主要分為三個(gè)部分:1.Header 2.Load commands 3Data區(qū)
header中表明該文件是 Mach-O 格式毒返,指定目標(biāo)架構(gòu),還有一些其他的文件屬性信 息舷手,文件頭信息影響后續(xù)的文件結(jié)構(gòu)
Load commands是一張包含很多內(nèi)容的表拧簸。內(nèi)容包括區(qū)域的位置、符號(hào)表男窟、動(dòng)態(tài)符號(hào)表 等盆赤。
Data 區(qū)主要就是負(fù)責(zé)代碼和數(shù)據(jù)記錄的。Mach-O 是以 Segment 這種結(jié)構(gòu)來(lái)組織數(shù)據(jù) 的歉眷,一個(gè) Segment 可以包含 0 個(gè)或多個(gè) Section牺六。根據(jù) Segment 是映射的哪一個(gè) Load Command,Segment 中 section 就可以被解讀為是是代碼汗捡,常量或者一些其他的數(shù)據(jù)類 型淑际。在裝載在內(nèi)存中時(shí),也是根據(jù) Segment 做內(nèi)存映射的凉唐。
我們?cè)诳匆幌聅wift的類的構(gòu)成庸追,有助于我們?cè)趍achoc中查找我們需要的信息。swift中類都有一個(gè)元數(shù)據(jù)結(jié)構(gòu)台囱,這個(gè)數(shù)據(jù)結(jié)構(gòu)是一個(gè)Metadata 的struct淡溯。通過(guò)swift的源碼我們可以推導(dǎo)出Metadata的內(nèi)部結(jié)構(gòu)是這樣的
struct Metadata{
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
其中我們需要關(guān)注typeDescriptor,不管是Class、Struct簿训、Enumd都有Descriptor.用來(lái)描述它自身咱娶。類的描述對(duì)象如下
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
...
}
我們都知道struct是值類型。如果我們現(xiàn)在能找到typeDescriptor這個(gè)指針地址强品,通過(guò)指針偏移就能訪問(wèn)到struct中值類型的變量的值或者引用類型的指針膘侮。那么v-table這個(gè)函數(shù)表我們推測(cè)它與位于size的后面。因?yàn)樗绻辉谶@個(gè)地方我們考慮不到它能存放在什么地方了的榛。下面我們來(lái)驗(yàn)證一下琼了。如果能找到就說(shuō)明確實(shí)是這樣的。
1.我們用machoview打開(kāi)我們編譯后的可執(zhí)行文件夫晌。
我們直接定位到數(shù)據(jù)段中的__TEXT,__swift5_types中雕薪。__TEXT,__swift5_types就是存放TargetClassDescriptor的地方。pFile是虛擬內(nèi)存地址晓淀,我們來(lái)計(jì)算一下前面8位的值所袁。0x0000BBDC+0x9CFBFFFF,注意這個(gè)地方的0x9CFBFFFF是小段地址所以應(yīng)該是0xFFFFFB9C。我們得到的值是0x10000B778凶掰。然后這個(gè)值我們需要減去程序本身虛擬內(nèi)存得地址燥爷,這個(gè)值在Load Commands中的LC_SEGMENT_64(__TEXT)中蜈亩。
可以看到這個(gè)值是0x100000000。我們現(xiàn)在得到的值是0x0000B778前翎,我們到data段中,在Section64(__TEXT,__const)中我們能看到0x0000B778位于的內(nèi)存區(qū)間稚配。
那么B778應(yīng)該就是在0xB770偏移8位,那么0x80000050就應(yīng)該是我們typeDescriptor的地址港华。
swift 方法的結(jié)構(gòu)
struct TargetMethodDescriptor就是swift中函數(shù)的數(shù)據(jù)結(jié)構(gòu)药有。其中第一個(gè)flags描述了函數(shù)的類型。
然后我們根據(jù)TargetClassDescriptor的結(jié)構(gòu)在來(lái)偏移13個(gè)字節(jié)我們?cè)诘刂?xB7B0找到了我們的TargetRelativeDirectPointer的偏移值苹丸。0x00000010ffffc5cc,根據(jù)TargetMethodDescriptor的數(shù)據(jù)結(jié)構(gòu)苇经,前面4個(gè)字節(jié)存放的是方法類型所以我們的函數(shù)本身的impl這個(gè)地方的偏移值應(yīng)該是0xFFFFC5CC赘理。我們當(dāng)前程序的ASLR的地址為0x1025d0000
0xFFFFC5CC+0x0000B7b0 = 0x100007d7c
0x100007d7c+0x1025d0000 = 0x2025D7D7C
0x2025D7D7C - Vm地址(0x10000000) = 0x1025D7D7C
下面看代碼
我們我們斷點(diǎn)到leo.fun2()這里查看匯編代碼,blr x8 其x8寄存器存放的就是func2()的地址扇单。我們使用register read x8讀取x8的地址 0x00000001025d7d7c商模。與我們計(jì)算得到的地址是一樣的說(shuō)明我們上面的推論是正確的。
消息派發(fā)
class Person:NSObject{
func func2(){
}
@objc dynamic func func3(){
}
@objc func func4(){
}
dynamic func func5(){
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let leo = Person()
leo.func2()
leo.func3()
leo.func4()
leo.func5()
}
}
我在Person中又添加了func3()蜘澜、func4()施流、func5()方法。同時(shí)對(duì)他們分別使用@objc 鄙信、dynamic修飾
然后我們看sil代碼
在Person的sil_vtable中func3()方法沒(méi)有添加到vtable這個(gè)函數(shù)表中瞪醋。因?yàn)閒unc3()使用了@objc + danamic 修飾。
在sil中 這個(gè)地方就很明顯的標(biāo)識(shí)出來(lái)了他們調(diào)用方法的不同装诡。func3()使用的是objc_method,而虛函數(shù)表使用的是class_method.
swift中的消息派發(fā)主要是為了兼容runtime的機(jī)制银受。所以使用消息派發(fā)的swift方法也就可以使用我們r(jià)untime的黑魔法了。