溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Swift Hook的虛函數(shù)表的使用原理是什么

發(fā)布時(shí)間:2021-10-15 10:02:41 來(lái)源:億速云 閱讀:120 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹“Swift Hook的虛函數(shù)表的使用原理是什么”,在日常操作中,相信很多人在Swift Hook的虛函數(shù)表的使用原理是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Swift Hook的虛函數(shù)表的使用原理是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

1. 前言

由于歷史包袱的原因,目前主流的大型APP基本都是以 Objective-C 為主要開(kāi)發(fā)語(yǔ)言。

但是敏銳的同學(xué)應(yīng)該能發(fā)現(xiàn),從 Swift 的 ABI 穩(wěn)定以后,各個(gè)大廠開(kāi)始陸續(xù)加大對(duì) Swift 的投入。

雖然在短期內(nèi) Swift 還難以取代 Objective-C,但是其與 Objective-C  并駕齊驅(qū)的趨勢(shì)是越來(lái)越明顯,從招聘的角度就即可管中窺豹。

在過(guò)去一年的招聘過(guò)程中我們總結(jié)發(fā)現(xiàn),有相當(dāng)數(shù)量的候選人只掌握 Swift 開(kāi)發(fā),對(duì)Objective-C  開(kāi)發(fā)并不熟悉,而且這部分候選人大多數(shù)比較年輕。

另外,以 RealityKit 等新框架為例,其只支持 Swift 不支持  Objective-C。上述種種現(xiàn)象意味著隨著時(shí)間的推移,如果項(xiàng)目不能很好的支持 Swift 開(kāi)發(fā),那么招聘成本以及應(yīng)用創(chuàng)新等一系列問(wèn)題將會(huì)凸顯出來(lái)。

因此,58 同城在 2020 年 Q4 的時(shí)候在集團(tuán)內(nèi)發(fā)起了跨部門(mén)協(xié)同項(xiàng)目,從各個(gè)層面打造 Objective-C 與 Swift  的混編生態(tài)環(huán)境——項(xiàng)目代號(hào) ”混天“。

一旦混編生態(tài)構(gòu)建完善,那么很多問(wèn)題將迎刃而解。

2. 原理簡(jiǎn)述

本文的技術(shù)方案僅針對(duì)通過(guò)虛函數(shù)表調(diào)用的函數(shù)進(jìn)行 Hook,不涉及直接地址調(diào)用和objc_msgSend 的調(diào)用的情況。

另外需要注意的是,Swift Compiler 設(shè)置為 Optimize for speed(Release默認(rèn))則TypeContext 的  VTable 的函數(shù)地址會(huì)清空。

設(shè)置為 Optimize for size 則 Swfit 可能會(huì)轉(zhuǎn)變?yōu)橹苯拥刂氛{(diào)用。

以上兩種配置都會(huì)造成方案失效。因此本文重點(diǎn)在介紹技術(shù)細(xì)節(jié)而非方案推廣。

Swift Hook的虛函數(shù)表的使用原理是什么

如果 Swift  通過(guò)虛函數(shù)表跳表的方式來(lái)實(shí)現(xiàn)方法調(diào)用,那么可以借助修改虛函數(shù)表來(lái)實(shí)現(xiàn)方法替換。即將特定虛函數(shù)表的函數(shù)地址修改為要替換的函數(shù)地址。但是由于虛函數(shù)表不包含地址與符號(hào)的映射,我們不能像  Objective-C 那樣根據(jù)函數(shù)的名字獲取到對(duì)應(yīng)的函數(shù)地址,因此修改 Swift 的虛函數(shù)是依靠函數(shù)索引來(lái)實(shí)現(xiàn)的。

簡(jiǎn)單理解就是將虛函數(shù)表理解為數(shù)組,假設(shè)有一個(gè) FuncTable[],我們修改函數(shù)地址只能通過(guò)索引值來(lái)實(shí)現(xiàn),就像 FuncTable[index] =  replaceIMP 。但是這也涉及到一個(gè)問(wèn)題,在版本迭代過(guò)程中我們不能保證代碼是一層不變的,因此這個(gè)版本的第 index 個(gè)函數(shù)可能是函數(shù) A,下個(gè)版本可能第  index 個(gè)函數(shù)就變成了函數(shù) B。顯然這對(duì)函數(shù)的替換會(huì)產(chǎn)生重大影響。

為此,我們通過(guò) Swift 的 OverrideTable 來(lái)解決索引變更的問(wèn)題。在 Swift 的OverrideTable  中,每個(gè)節(jié)點(diǎn)都記錄了當(dāng)前這個(gè)函數(shù)重寫(xiě)了哪個(gè)類(lèi)的哪個(gè)函數(shù),以及重寫(xiě)后函數(shù)的函數(shù)指針。

因此只要我們能獲取到 OverrideTable 也就意味著能獲取被重寫(xiě)的函數(shù)指針 IMP0 以及重寫(xiě)后的函數(shù)指針 IMP1。只要在  FuncTable[] 中找到 IMP0 并替換成 IMP1 即可完成方法替換。

接下來(lái)將詳細(xì)介紹Swift的函數(shù)調(diào)用、TypeContext、Metadata、VTable、OverrideTable  等細(xì)節(jié),以及他們彼此之間有何種關(guān)聯(lián)。為了方便閱讀和理解,本文所有代碼及運(yùn)行結(jié)果,都是基于 arm64 架構(gòu)

3. Swift 的函數(shù)調(diào)用

首先我們需要了解 Swift 的函數(shù)如何調(diào)用的。與 Objective-C 不同,Swift 的函數(shù)調(diào)用存在三種方式,分別是:基于 Objective-C  的消息機(jī)制、基于虛函數(shù)表的訪問(wèn)、以及直接地址調(diào)用。

? 3.1 Objective-C 的消息機(jī)制

首先我們需要了解在什么情況下 Swift 的函數(shù)調(diào)用是借助 Objective-C 的消息機(jī)制。如果方法通過(guò) @objc dynamic  修飾,那么在編譯后將通過(guò) objc_msgSend 的來(lái)調(diào)用函數(shù)。

假設(shè)有如下代碼

class MyTestClass :NSObject {     @objc dynamic func helloWorld() {         print("call helloWorld() in MyTestClass")     } }  let myTest = MyTestClass.init() myTest.helloWorld()

編譯后其對(duì)應(yīng)的匯編為

0x1042b8824 <+120>: bl     0x1042b9578               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 0x1042b8828 <+124>: mov    x20, x0 0x1042b882c <+128>: bl     0x1042b8998               ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22 0x1042b8830 <+132>: stur   x0, [x29, #-0x30] 0x1042b8834 <+136>: adrp   x8, 13 0x1042b8838 <+140>: ldr    x9, [x8, #0x320] 0x1042b883c <+144>: stur   x0, [x29, #-0x58] 0x1042b8840 <+148>: mov    x1, x9 0x1042b8844 <+152>: str    x8, [sp, #0x60] 0x1042b8848 <+156>: bl     0x1042bce88               ; symbol stub for: objc_msgSend 0x1042b884c <+160>: mov    w11, #0x1 0x1042b8850 <+164>: mov    x0, x11 0x1042b8854 <+168>: ldur   x1, [x29, #-0x48] 0x1042b8858 <+172>: bl     0x1042bcd5c               ; symbol stub for:

從上面的匯編代碼中我們很容易看出調(diào)用了地址為0x1042bce88的objc_msgSend 函數(shù)。

? 3.2 虛函數(shù)表的訪問(wèn)

虛函數(shù)表的訪問(wèn)也是動(dòng)態(tài)調(diào)用的一種形式,只不過(guò)是通過(guò)訪問(wèn)虛函數(shù)表的方式進(jìn)行調(diào)用。

假設(shè)還是上述代碼,我們將 @objc dynamic 去掉之后,并且不再繼承自 NSObject。

class MyTestClass {     func helloWorld() {         print("call helloWorld() in MyTestClass")     } }  let myTest = MyTestClass.init() myTest.helloWorld()

匯編代碼變成了下面這樣?

0x1026207ec <+120>: bl     0x102621548               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 0x1026207f0 <+124>: mov    x20, x0 0x1026207f4 <+128>: bl     0x102620984               ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22 0x1026207f8 <+132>: stur   x0, [x29, #-0x30] 0x1026207fc <+136>: ldr    x8, [x0] 0x102620800 <+140>: adrp   x9, 8 0x102620804 <+144>: ldr    x9, [x9, #0x40] 0x102620808 <+148>: ldr    x10, [x9] 0x10262080c <+152>: and    x8, x8, x10 0x102620810 <+156>: ldr    x8, [x8, #0x50] 0x102620814 <+160>: mov    x20, x0 0x102620818 <+164>: stur   x0, [x29, #-0x58] 0x10262081c <+168>: str    x9, [sp, #0x60] 0x102620820 <+172>: blr    x8 0x102620824 <+176>: mov    w11, #0x1 0x102620828 <+180>: mov    x0, x11

從上面匯編代碼可以看出,經(jīng)過(guò)編譯后最終是通過(guò) blr 指令調(diào)用了 x8 寄存器中存儲(chǔ)的函數(shù)。至于 x8  寄存器中的數(shù)據(jù)從哪里來(lái)的,留到后面的章節(jié)闡述。

? 3.3 直接地址調(diào)用

假設(shè)還是上述代碼,我們?cè)賹?Build Setting 中Swift Compiler - Code Generaation ->  Optimization Level 修改為 Optimize for Size[-Osize],匯編代碼變成了下面這樣?

0x1048c2114 <+40>:  bl     0x1048c24b8               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 0x1048c2118 <+44>:  add    x1, sp, #0x10             ; =0x10  0x1048c211c <+48>:  bl     0x1048c5174               ; symbol stub for: swift_initStackObject 0x1048c2120 <+52>:  bl     0x1048c2388               ; SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 0x1048c2124 <+56>:  adr    x0, #0xc70c               ; demangling cache variable for type metadata for Swift._ContiguousArrayStorage<Any>

這是大家就會(huì)發(fā)現(xiàn)bl 指令后跟著的是一個(gè)常量地址,并且是 SwiftDemo.MyTestClass.helloWorld() 的函數(shù)地址。

4. 思考

既然基于虛函數(shù)表的派發(fā)形式也是一種動(dòng)態(tài)調(diào)用,那么是不是以為著只要我們修改了虛函數(shù)表中的函數(shù)地址,就實(shí)現(xiàn)了函數(shù)的替換?

5. 基于 TypeContext 的方法交換

在往期文章《從 Mach-O 角度談?wù)?Swift 和 OC 的存儲(chǔ)差異》我們可以了解到在Mach-O 文件中,可以通過(guò) __swift5_types  查找到每個(gè) Class 的ClassContextDescriptor,并且可以通過(guò) ClassContextDescriptor  找到當(dāng)前類(lèi)對(duì)應(yīng)的虛函數(shù)表,并動(dòng)態(tài)調(diào)用表中的函數(shù)。

注意:(在 Swift 中,Class/Struct/Enum 統(tǒng)稱為 Type,為了方便起見(jiàn),我們?cè)谖闹刑岬降腡ypeContext 和  ClassContextDescriptor 都指的是 ClassContextDescriptor)。

首先我們來(lái)回顧下 Swift 的類(lèi)的結(jié)構(gòu)描述,結(jié)構(gòu)體 ClassContextDescriptor 是 Swift  類(lèi)在Section64(__TEXT,__const) 中的存儲(chǔ)結(jié)構(gòu)。

struct ClassContextDescriptor{     uint32_t Flag;     uint32_t Parent;     int32_t  Name;     int32_t  AccessFunction;     int32_t  FieldDescriptor;     int32_t  SuperclassType;     uint32_t MetadataNegativeSizeInWords;     uint32_t MetadataPositiveSizeInWords;     uint32_t NumImmediateMembers;     uint32_t NumFields;     uint32_t FieldOffsetVectorOffset;     <泛型簽名> //字節(jié)數(shù)與泛型的參數(shù)和約束數(shù)量有關(guān)     <MaybeAddResilientSuperclass>//有則添加4字節(jié)     <MaybeAddMetadataInitialization>//有則添加4*3字節(jié)     VTableList[]//先用4字節(jié)存儲(chǔ)offset/pointerSize,再用4字節(jié)描述數(shù)量,隨后N個(gè)4+4字節(jié)描述函數(shù)類(lèi)型及函數(shù)地址。     OverrideTableList[]//先用4字節(jié)描述數(shù)量,隨后N個(gè)4+4+4字節(jié)描述當(dāng)前被重寫(xiě)的類(lèi)、被重寫(xiě)的函數(shù)描述、當(dāng)前重寫(xiě)函數(shù)地址。 }

從上述結(jié)構(gòu)可以看出,ClassContextDescriptor 的長(zhǎng)度是不固定的,不同的類(lèi) ClassContextDescriptor  的長(zhǎng)度可能不同。那么如何才能知道當(dāng)前這個(gè)類(lèi)是不是泛型?以及是否有 ResilientSuperclass、MetadataInitialization  特征?其實(shí)在前一篇文章《從Mach-O 角度談?wù)?Swift 和 OC 的存儲(chǔ)差異》中已經(jīng)做了說(shuō)明,我們可以通過(guò) Flag 的標(biāo)記位來(lái)獲取相關(guān)信息。

例如,如果 Flag 的 generic 標(biāo)記位為 1,則說(shuō)明是泛型。

|  TypeFlag(16bit)  |  version(8bit) | generic(1bit) | unique(1bit) | unknow (1bi) | Kind(5bit) | //判斷泛型 (Flag & 0x80) == 0x80

那么泛型簽名到底能占多少字節(jié)呢?Swift 的 GenMeta.cpp 文件中對(duì)泛型的存儲(chǔ)做了解釋?zhuān)砜偨Y(jié)如下:

假設(shè)有泛型有paramsCount個(gè)參數(shù),有requeireCount個(gè)約束  /**      16B  =  4B + 4B + 2B + 2B + 2B + 2B      addMetadataInstantiationCache -> 4B      addMetadataInstantiationPattern -> 4B      GenericParamCount -> 2B      GenericRequirementCount -> 2B      GenericKeyArgumentCount -> 2B      GenericExtraArgumentCount -> 2B  */  short pandding = (unsigned)-paramsCount & 3;  泛型簽名字節(jié)數(shù) = (16 + paramsCount + pandding + 3 * 4 * (requeireCount) + 4);

因此只要明確了 Flag 各個(gè)標(biāo)記位的含義以及泛型的存儲(chǔ)長(zhǎng)度規(guī)律,那么就能計(jì)算出虛函數(shù)表 VTable 的位置以及各個(gè)函數(shù)的字節(jié)位置。

了解了泛型的布局以及 VTable 的位置,是不是就意味著能實(shí)現(xiàn)函數(shù)指針的修改了呢?答案當(dāng)然是否定的,因?yàn)?VTable 存儲(chǔ)在 __TEXT  段,__TEXT 是只讀段,我們沒(méi)辦法直接進(jìn)行修改。不過(guò)最終我們通過(guò) remap 的方式修改代碼段,將 VTable  中的函數(shù)地址進(jìn)行了修改,然而發(fā)現(xiàn)在運(yùn)行時(shí)函數(shù)并沒(méi)有被替換為我們修改的函數(shù)。那到底是怎么一回事呢?

6. 基于 Metadata 的方法交換

上述實(shí)驗(yàn)的失敗當(dāng)然是我們的不嚴(yán)謹(jǐn)導(dǎo)致的。在項(xiàng)目一開(kāi)始我們先研究的是類(lèi)型存儲(chǔ)描述 TypeContext,主要是類(lèi)的存儲(chǔ)描述  ClassContextDescriptor。在找到 VTable 后我們想當(dāng)然的認(rèn)為運(yùn)行時(shí) Swift 是通過(guò)訪問(wèn)  ClassContextDescriptor 中的 VTable 進(jìn)行函數(shù)調(diào)用的。但是事實(shí)并不是這樣。

7. VTable 函數(shù)調(diào)用

接下來(lái)我們將回答下 Swift的函數(shù)調(diào)用 章節(jié)中提的問(wèn)題,x8 寄存器的函數(shù)地址是從哪里來(lái)的。還是前文中的 Demo,我們?cè)?helloWorld()  函數(shù)調(diào)用前打斷點(diǎn)

let myTest = MyTestClass.init() ->  myTest.helloWorld()

斷點(diǎn)停留在 0x100230ab0 處?

0x100230aac <+132>: stur   x0, [x29, #-0x30] 0x100230ab0 <+136>: ldr    x8, [x0] 0x100230ab4 <+140>: ldr    x8, [x8, #0x50] 0x100230ab8 <+144>: mov    x20, x0 0x100230abc <+148>: str    x0, [sp, #0x58] 0x100230ac0 <+152>: blr    x8

此時(shí) x0 寄存器中存儲(chǔ)的是 myTest 的地址 x0 = 0x0000000280d08ef0,ldr x8, [x0] 則是將  0x280d08ef0 處存儲(chǔ)的數(shù)據(jù)放入 x8(注意,這里是只將 *myTest 存入 x8,而不是將 0x280d08ef0 存入 x8)。單步執(zhí)行后,通過(guò)  re read 查看各個(gè)寄存器的數(shù)據(jù)后會(huì)發(fā)現(xiàn) x8 存儲(chǔ)的是 type metadata 的地址,而不是 TypeContext 的地址。

x0 = 0x0000000280d08ef0 x1 = 0x0000000280d00234 x2 = 0x0000000000000000 x3 = 0x00000000000008fd x4 = 0x0000000000000010 x5 = 0x000000016fbd188f x6 = 0x00000002801645d0 x7 = 0x0000000000000000 x8 = 0x000000010023e708  type metadata for SwiftDemo.MyTestClass x9 = 0x0000000000000003 x10= 0x0000000280d08ef0 x11= 0x0000000079c00000

經(jīng)過(guò)上步單步執(zhí)行后,當(dāng)前程序要做的是 ldr x8, [x8, #0x50],即將 type metadata + 0x50 處的數(shù)據(jù)存儲(chǔ)到  x8。這一步就是跳表,也就是說(shuō)經(jīng)過(guò)這一步后,x8 寄存器中存儲(chǔ)的就是 helloWorld() 的地址。

    0x100230aac <+132>: stur   x0, [x29, #-0x30]     0x100230ab0 <+136>: ldr    x8, [x0] ->  0x100230ab4 <+140>: ldr    x8, [x8, #0x50]     0x100230ab8 <+144>: mov    x20, x0     0x100230abc <+148>: str    x0, [sp, #0x58]     0x100230ac0 <+152>: blr    x8

那是否真的是這樣呢?ldr x8, [x8, #0x50] 執(zhí)行后,我們?cè)俅尾榭?x8,看看寄存器中是否為函數(shù)地址?

x0 = 0x0000000280d08ef0 x1 = 0x0000000280d00234 x2 = 0x0000000000000000 x3 = 0x00000000000008fd x4 = 0x0000000000000010 x5 = 0x000000016fbd188f x6 = 0x00000002801645d0 x7 = 0x0000000000000000 x8 = 0x0000000100231090  SwiftDemo`SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 x9 = 0x0000000000000003

結(jié)果表明 x8 存儲(chǔ)的確實(shí)是 helloWorld() 的函數(shù)地址。上述實(shí)驗(yàn)表明經(jīng)過(guò)跳轉(zhuǎn)0x50 位置后,程序找到了 helloWorld()  函數(shù)地址。類(lèi)的 Metadata 位于__DATA 段,是可讀寫(xiě)的。其結(jié)構(gòu)如下:

struct SwiftClass {     NSInteger kind;     id superclass;     NSInteger reserveword1;     NSInteger reserveword2;     NSUInteger rodataPointer;     UInt32 classFlags;     UInt32 instanceAddressPoint;     UInt32 instanceSize;     UInt16 instanceAlignmentMask;     UInt16 runtimeReservedField;     UInt32 classObjectSize;     UInt32 classObjectAddressPoint;     NSInteger nominalTypeDescriptor;     NSInteger ivarDestroyer;     //func[0]     //func[1]     //func[2]     //func[3]     //func[4]     //func[5]     //func[6]     .... };

上面的代碼在經(jīng)過(guò)0x50 字節(jié)的偏移后正好位于 func[0] 的位置。因此要想動(dòng)態(tài)修改函數(shù)需要修改Metadata中的數(shù)據(jù)。

經(jīng)過(guò)試驗(yàn)后發(fā)現(xiàn)修改后函數(shù)確實(shí)是在運(yùn)行后發(fā)生了改變。但是這并沒(méi)有結(jié)束,因  為虛函數(shù)表與消息發(fā)送有所不同,虛函數(shù)表中并沒(méi)有任何函數(shù)名和函數(shù)地址的映射,我們只能通過(guò)偏移來(lái)修改函數(shù)地址。

比如,我想修改第1個(gè)函數(shù),那么我要找到 Meatadata,并修改 0x50 處的 8 字節(jié)數(shù)據(jù)。同理,想要修改第 2 個(gè)函數(shù),那么我要修改 0x58  處的 8 字節(jié)數(shù)據(jù)。這就帶來(lái)一個(gè)問(wèn)題,一旦函數(shù)數(shù)量或者順序發(fā)生了變更,那么都需要重新進(jìn)行修正偏移索引。

舉例說(shuō)明下,假設(shè)當(dāng)前 1.0 版本的代碼為

class MyTestClass {     func helloWorld() {         print("call helloWorld() in MyTestClass")     } }

此時(shí)我們對(duì) 0x50 處的函數(shù)指針進(jìn)行了修改。當(dāng) 2.0 版本變更為如下代碼時(shí),此時(shí)我們的偏移應(yīng)該修改為  0x58,否則我們的函數(shù)替換就發(fā)生了錯(cuò)誤。

class MyTestClass {     func sayhi() {         print("call sayhi() in MyTestClass")     }      func helloWorld() {         print("call helloWorld() in MyTestClass")     } }

為了解決虛函數(shù)變更的問(wèn)題,我們需要了解下 TypeContext 與 Metadata 的關(guān)系。

8. TypeContext 與 Metadata 的關(guān)系

Metadata 結(jié)構(gòu)中的 nominalTypeDescriptor 指向了 TypeContext,也就是說(shuō)當(dāng)我們獲取到 Metadata  地址后,偏移 0x40 字節(jié)就能獲取到當(dāng)前這個(gè)類(lèi)對(duì)應(yīng)的 TypeContext地址。那么如何通過(guò) TypeContext 找到 Metadata 呢?

我們還是看剛才的那個(gè) Demo,此時(shí)我們將斷點(diǎn)打到 init() 函數(shù)上,我們想了解下 MyTestClass 的 Metadata  到底是哪里來(lái)的。

->  let myTest = MyTestClass.init() myTest.helloWorld()

此時(shí)展開(kāi)為匯編我們會(huì)發(fā)現(xiàn),程序準(zhǔn)備調(diào)用一個(gè)函數(shù)。

->  0x1040f0aa0 <+120>: bl     0x1040f16a8               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated>     0x1040f0aa4 <+124>: mov    x20, x0     0x1040f0aa8 <+128>: bl     0x1040f0c18               ; SwiftDemo.MyTestClass.__al

在執(zhí)行 bl 0x1040f16a8 指令之前,x0 寄存器為 0。

x0 = 0x0000000000000000

此時(shí)通過(guò) si 單步調(diào)試就會(huì)發(fā)現(xiàn)跳轉(zhuǎn)到了函數(shù) 0x1040f16a8 處,其函數(shù)指令較少,如下所示?

SwiftDemo`type metadata accessor for MyTestClass: ->  0x1040f16a8 <+0>:  stp    x29, x30, [sp, #-0x10]!     0x1040f16ac <+4>:  adrp   x8, 13     0x1040f16b0 <+8>:  add    x8, x8, #0x6f8            ; =0x6f8      0x1040f16b4 <+12>: add    x8, x8, #0x10             ; =0x10      0x1040f16b8 <+16>: mov    x0, x8     0x1040f16bc <+20>: bl     0x1040f4e68               ; symbol stub for: objc_opt_self     0x1040f16c0 <+24>: mov    x8, #0x0     0x1040f16c4 <+28>: mov    x1, x8     0x1040f16c8 <+32>: ldp    x29, x30, [sp], #0x10     0x1040f16cc <+36>: ret

在執(zhí)行 0x1040f16a8 函數(shù)執(zhí)行完后,x0 寄存器就存儲(chǔ)了 MyTestClass 的 Metadata 地址。

x0 = 0x00000001047e6708  type metadata for SwiftDemo.MyTestClass

那么這個(gè)被標(biāo)記為 type metadata accessor for SwiftDemo.MyTestClass at的函數(shù)到底是什么?

在上文介紹的 struct ClassContextDescriptor 貌似有個(gè)成員是 AccessFunction,那這個(gè)  ClassContextDescriptor 中的 AccessFunction 是不是 Metadata 的訪問(wèn)函數(shù)呢?這個(gè)其實(shí)很容易驗(yàn)證。

我們?cè)俅芜\(yùn)行 Demo,此時(shí)metadata accessor 為 0x1047d96a8,繼續(xù)執(zhí)行后Metadata地址為  0x1047e6708。

x0 = 0x00000001047e6708  type metadata for SwiftDemo.MyTestClass

查看 0x1047e6708,繼續(xù)偏移 0x40 字節(jié)后可以得到 Metadata 結(jié)構(gòu)中的 nominalTypeDescriptor 地址  0x1047e6708 + 0x40 = 0x1047e6748。

查看 0x1047e6748 存儲(chǔ)的數(shù)據(jù)為 0x1047df4a0。

(lldb) x 0x1047e6748 0x1047e6748: a0 f4 7d 04 01 00 00 00 00 00 00 00 00 00 00 00  ..}............. 0x1047e6758: 90 90 7d 04 01 00 00 00 18 8c 7d 04 01 00 00 00  ..}.......}.....

ClassContextDescriptor 中的 AccessFunction 在第 12 字節(jié)處,因此對(duì) 0x1047df4a0 + 12 可知  AccessFunction 的位置為 0x1047df4ac。繼續(xù)查看 0x1047df4ac 存儲(chǔ)的數(shù)據(jù)為

(lldb) x 0x1047df4ac 0x1047df4ac: fc a1 ff ff 70 04 00 00 00 00 00 00 02 00 00 00  ....p........... 0x1047df4bc: 0c 00 00 00 02 00 00 00 00 00 00 00 0a 00 00 00  ................

由于在 ClassContextDescriptor 中,AccessFunction 為相對(duì)地址,因此我們做一次地址計(jì)算 0x1047df4ac +  0xffffa1fc - 0x10000000 = 0x1047d96a8,與 metadata accessor 0x1047d96a8 相同,這就說(shuō)明  TypeContext 是通過(guò) AccessFunction 來(lái)獲取對(duì)應(yīng)的Metadata的地址的。

當(dāng)然,實(shí)際上也會(huì)有例外,有時(shí)編譯器會(huì)直接使用緩存的 cache Metadata 的地址,而不再通過(guò) AccessFunction 來(lái)獲取類(lèi)的  Metadata。

9. 基于 TypeContext 和 Metadata 的方法交換

在了解了 TypeContext 和 Metadata 的關(guān)系后,我們就能做一些設(shè)想了。在  Metadata中雖然存儲(chǔ)了函數(shù)的地址,但是我們并不知道函數(shù)的類(lèi)型。這里的函數(shù)類(lèi)型指的是函數(shù)是普通函數(shù)、初始化函數(shù)、getter、setter 等。

在 TypeContext 的 VTable 中,method 存儲(chǔ)一共是 8 字節(jié),第一個(gè)4字節(jié)存儲(chǔ)的函數(shù)的  Flag,第二個(gè)4字節(jié)存儲(chǔ)的函數(shù)的相對(duì)地址。

struct SwiftMethod {     uint32_t Flag;     uint32_t Offset; };

通過(guò) Flag 我們很容易知道是否是動(dòng)態(tài),是否是實(shí)例方法,以及函數(shù)類(lèi)型 Kind。

|  ExtraDiscriminator(16bit)  |... | Dynamic(1bit) | instanceMethod(1bit) | Kind(4bit) |

Kind 枚舉如下?

typedef NS_ENUM(NSInteger, SwiftMethodKind) {     SwiftMethodKindMethod             = 0,     // method     SwiftMethodKindInit               = 1,     //init     SwiftMethodKindGetter             = 2,     // get     SwiftMethodKindSetter             = 3,     // set     SwiftMethodKindModify             = 4,     // modify     SwiftMethodKindRead               = 5,     // read };

從 Swift 的源碼中可以很明顯的看到,類(lèi)重寫(xiě)的函數(shù)是單獨(dú)存儲(chǔ)的,也就是有單獨(dú)的OverrideTable。

并且 OverrideTable 是存儲(chǔ)在 VTable 之后。與 VTable 中的 method 結(jié)構(gòu)不同,OverrideTable 中的函數(shù)需要  3 個(gè) 4 字節(jié)描述:

struct SwiftOverrideMethod {     uint32_t OverrideClass;//記錄是重寫(xiě)哪個(gè)類(lèi)的函數(shù),指向TypeContext     uint32_t OverrideMethod;//記錄重寫(xiě)哪個(gè)函數(shù),指向SwiftMethod     uint32_t Method;//函數(shù)相對(duì)地址 };

也就是說(shuō) SwiftOverrideMethod 中能夠包含兩個(gè)函數(shù)的綁定關(guān)系,這種關(guān)系與函數(shù)的編譯順序和數(shù)量無(wú)關(guān)。

如果 Method 記錄用于 Hook 的函數(shù)地址,OverrideMethod  作為被Hook的函數(shù),那是不是就意味著無(wú)論如何改變虛函數(shù)表的順序及數(shù)量,只要 Swift 還是通過(guò)跳表的方式進(jìn)行函數(shù)調(diào)用,那么我們就無(wú)需關(guān)注函數(shù)變化了。

為了驗(yàn)證可行性,我們寫(xiě) Demo 測(cè)試一下:

class MyTestClass {     func helloWorld() {         print("call helloWorld() in MyTestClass")     } }//作為被Hook類(lèi)及函數(shù)  <--------------------------------------------------->  class HookTestClass: MyTestClass  {     override func helloWorld() {         print("\n********** call helloWorld() in HookTestClass **********")         super.helloWorld()         print("********** call helloWorld() in HookTestClass end **********\n")     } }//通過(guò)繼承和重寫(xiě)的方式進(jìn)行Hook  <--------------------------------------------------->    let myTest = MyTestClass.init()  myTest.helloWorld()   //do hook  print("\n------ replace MyTestClass.helloWorld() with   HookTestClass.helloWorld() -------\n")   WBOCTest.replace(HookTestClass.self);   //hook 生效  myTest.helloWorld()

運(yùn)行后,可以看出 helloWorld() 已經(jīng)被替換成功?

2021-03-09 17:25:36.321318+0800 SwiftDemo[59714:5168073] _mh_execute_header = 4368482304 call helloWorld() in MyTestClass  ------ replace MyTestClass.helloWorld() with HookTestClass.helloWorld() -------   ********** call helloWorld() in HookTestClass ********** call helloWorld() in MyTestClass ********** call helloWorld() in HookTestClass end **********

到此,關(guān)于“Swift Hook的虛函數(shù)表的使用原理是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI