一種Swift Hook新思路——從Swift的虛函數(shù)表說起

摘要:業(yè)界對(duì)Swift的Hook大多都需要依靠OC的消息轉(zhuǎn)發(fā)特性來實(shí)現(xiàn),本文從修改Swift的虛函數(shù)表的角度,介紹了一種新的Hook思路。并以此為主線,重點(diǎn)介紹Swift的詳細(xì)結(jié)構(gòu)以及應(yīng)用。

引言

由于歷史包袱的原因,目前主流的大型APP基本都是以O(shè)bjective-C為主要開發(fā)語言。但是敏銳的同學(xué)應(yīng)該能發(fā)現(xiàn),從Swift的ABI穩(wěn)定以后,各個(gè)大廠開始陸續(xù)加大對(duì)Swift的投入。雖然在短期內(nèi)Swift還難以取代Objective-C,但是其與Objective-C并駕齊驅(qū)的趨勢(shì)是越來越明顯,從招聘的角度就即可管中窺豹。在過去一年的招聘過程中我們總結(jié)發(fā)現(xiàn),有相當(dāng)數(shù)量的候選人只掌握Swift開發(fā),對(duì)Objective-C開發(fā)并不熟悉,而且這部分候選人大多數(shù)比較年輕。另外,以RealityKit等新框架為例,其只支持Swift不支持Objective-C。上述種種現(xiàn)象意味著隨著時(shí)間的推移,如果項(xiàng)目不能很好的支持Swift開發(fā),那么招聘成本以及應(yīng)用創(chuàng)新等一系列問題將會(huì)凸顯出來。因此,58同城在2020年Q4的時(shí)候在集團(tuán)內(nèi)發(fā)起了跨部門協(xié)同項(xiàng)目,從各個(gè)層面打造Objective-C與Swift的混編生態(tài)環(huán)境——項(xiàng)目代號(hào) ”混天“。一旦混編生態(tài)構(gòu)建完善,那么很多問題將迎刃而解。

原理簡(jiǎn)述

文章篇幅較長,且內(nèi)容較為枯燥,為了方便讀者閱讀,先拋出結(jié)論及原理。如果您對(duì)相關(guān)代碼感興趣,可以在Github上搜索SwiftVTHook下載Demo

本文的技術(shù)方案僅針對(duì)通過虛函數(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é)而非方案推廣。

方案簡(jiǎn)圖

如果Swift通過虛函數(shù)表跳表的方式來實(shí)現(xiàn)方法調(diào)用,那么可以借助修改虛函數(shù)表來實(shí)現(xiàn)方法替換。即將特定虛函數(shù)表的函數(shù)地址修改為要替換的函數(shù)地址。但是由于虛函數(shù)表不包含地址與符號(hào)的映射,我們不能像Objective-C那樣根據(jù)函數(shù)的名字獲取到對(duì)應(yīng)的函數(shù)地址,因此修改Swift的虛函數(shù)是依靠函數(shù)索引來實(shí)現(xiàn)的。簡(jiǎn)單理解就是將虛函數(shù)表理解為數(shù)組,假設(shè)有一個(gè)FuncTable[],我們修改函數(shù)地址只能通過索引值來實(shí)現(xiàn),就像FuncTable[index] = replaceIMP 。但是這也涉及到一個(gè)問題,在版本迭代過程中我們不能保證代碼是一層不變的,因此這個(gè)版本的第index個(gè)函數(shù)可能是函數(shù)A,下個(gè)版本可能第index個(gè)函數(shù)就變成了函數(shù)B。顯然這對(duì)函數(shù)的替換會(huì)產(chǎn)生重大影響。

為此,我們通過Swift的OverrideTable來解決索引變更的問題。在Swift的OverrideTable中,每個(gè)節(jié)點(diǎn)都記錄了當(dāng)前這個(gè)函數(shù)重寫了哪個(gè)類的哪個(gè)函數(shù),以及重寫后函數(shù)的函數(shù)指針。因此只要我們能獲取到OverrideTable也就意味著能獲取被重寫的函數(shù)指針IMP0以及重寫后的函數(shù)指針IMP1。只要在FuncTable[]中找到IMP0并替換成IMP1即可完成方法替換。

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

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

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

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

  • 虛函數(shù)表的訪問
    虛函數(shù)表的訪問也是動(dòng)態(tài)調(diào)用的一種形式,只不過是通過訪問虛函數(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)過編譯后最終是通過blr 指令調(diào)用了x8寄存器中存儲(chǔ)的函數(shù)。至于x8寄存器中的數(shù)據(jù)從哪里來的,留到后面的章節(jié)闡述。

  • 直接地址調(diào)用
    假設(shè)還是上述代碼,我們?cè)賹?code>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ù)地址。

思考

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

基于TypeContext的方法交換

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

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

首先我們來回顧下Swift的類的結(jié)構(gòu)描述,結(jié)構(gòu)體ClassContextDescriptor是Swift類在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ù)類型及函數(shù)地址。
    OverrideTableList[]//先用4字節(jié)描述數(shù)量,隨后N個(gè)4+4+4字節(jié)描述當(dāng)前被重寫的類、被重寫的函數(shù)描述、當(dāng)前重寫函數(shù)地址。
}

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

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

那么泛型簽名到底能占多少字節(jié)呢?Swift的GenMeta.cpp文件中對(duì)泛型的存儲(chǔ)做了解釋,整理總結(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ǔ)長度規(guī)律,那么就能計(jì)算出虛函數(shù)表VTable的位置以及各個(gè)函數(shù)的字節(jié)位置。
了解了泛型的布局以及VTable的位置,是不是就意味著能實(shí)現(xiàn)函數(shù)指針的修改了呢?答案當(dāng)然是否定的,因?yàn)閂Table存儲(chǔ)在__TEXT段,__TEXT是只讀段,我們沒辦法直接進(jìn)行修改。不過最終我們通過remap的方式修改代碼段,將VTable中的函數(shù)地址進(jìn)行了修改,然而發(fā)現(xiàn)在運(yùn)行時(shí)函數(shù)并沒有被替換為我們修改的函數(shù)。那到底是怎么一回事呢?

基于Metadata的方法交換

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

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

接下來我們將回答下 Swift的函數(shù)調(diào)用 章節(jié)中提的問題,x8寄存器的函數(shù)地址是從哪里來的。還是前文中的Demo,我們?cè)趆elloWorld()函數(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 = 0x0000000280d08ef0ldr x8, [x0]則是將0x280d08ef0處存儲(chǔ)的數(shù)據(jù)放入x8(注意,這里是只將*myTest存入x8,而不是將0x280d08ef0存入x8)。單步執(zhí)行后,通過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)過上步單步執(zhí)行后,當(dāng)前程序要做的是ldr x8, [x8, #0x50],即將type metadata + 0x50處的數(shù)據(jù)存儲(chǔ)到x8。這一步就是跳表,也就是說經(jīng)過這一步后,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è)俅尾榭磝8,看看寄存器中是否為函數(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)過跳轉(zhuǎn)0x50位置后,程序找到了helloWorld()函數(shù)地址。類的Metadata位于__DATA段,是可讀寫的。其結(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)過0x50字節(jié)的偏移后正好位于func[0]的位置。因此要想動(dòng)態(tài)修改函數(shù)需要修改Metadata中的數(shù)據(jù)。經(jīng)過試驗(yàn)后發(fā)現(xiàn)修改后函數(shù)確實(shí)是在運(yùn)行后發(fā)生了改變。但是這并沒有結(jié)束,因?yàn)樘摵瘮?shù)表與消息發(fā)送有所不同,虛函數(shù)表中并沒有任何函數(shù)名和函數(shù)地址的映射,我們只能通過偏移來修改函數(shù)地址。比如,我想修改第1個(gè)函數(shù),那么我要找到Meatadata,并修改0x50處的8字節(jié)數(shù)據(jù)。同理,想要修改第2個(gè)函數(shù),那么我要修改0x58處的8字節(jié)數(shù)據(jù)。這就帶來一個(gè)問題,一旦函數(shù)數(shù)量或者順序發(fā)生了變更,那么都需要重新進(jìn)行修正偏移索引。舉例說明下,假設(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ù)變更的問題,我們需要了解下TypeContext與Metadata的關(guān)系。

TypeContext與Metadata的關(guān)系

Metadata結(jié)構(gòu)中的nominalTypeDescriptor指向了TypeContext,也就是說當(dāng)我們獲取到Metadata地址后,偏移0x40字節(jié)就能獲取到當(dāng)前這個(gè)類對(duì)應(yīng)的TypeContext地址。那么如何通過TypeContext找到Metadata呢?我們還是看剛才的那個(gè)Demo,此時(shí)我們將斷點(diǎn)打到init()函數(shù)上,我們想了解下MyTestClass的Metadata到底是哪里來的。

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

此時(shí)展開為匯編我們會(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.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22

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

    x0 = 0x0000000000000000

此時(shí)通過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 <compiler-generated>的函數(shù)到底是什么?在上文介紹的struct ClassContextDescriptor貌似有個(gè)成員是AccessFunction,那這個(gè)ClassContextDescriptor中的AccessFunction是不是Metadata的訪問函數(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相同,這就說明TypeContext是通過AccessFunction來獲取對(duì)應(yīng)的Metadata的地址的。當(dāng)然,實(shí)際上也會(huì)有例外,有時(shí)編譯器會(huì)直接使用緩存的cache Metadata的地址,而不再通過AccessFunction來獲取類的Metadata。

基于TypeContext和Metadata的方法交換

在了解了TypeContext和Metadata的關(guān)系后,我們就能做一些設(shè)想了。在Metadata中雖然存儲(chǔ)了函數(shù)的地址,但是我們并不知道函數(shù)的類型。這里的函數(shù)類型指的是函數(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;
};

通過Flag我們很容易知道是否是動(dòng)態(tài),是否是實(shí)例方法,以及函數(shù)類型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的源碼中可以很明顯的看到,類重寫的函數(shù)是單獨(dú)存儲(chǔ)的,也就是有單獨(dú)的OverrideTable。并且OverrideTable是存儲(chǔ)在VTable之后。與VTable中的method結(jié)構(gòu)不同,OverrideTable中的函數(shù)需要3個(gè)4字節(jié)描述:

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

也就是說SwiftOverrideMethod中能夠包含兩個(gè)函數(shù)的綁定關(guān)系,這種關(guān)系與函數(shù)的編譯順序和數(shù)量無關(guān)。如果Method記錄用于Hook的函數(shù)地址,OverrideMethod作為被Hook的函數(shù),那是不是就意味著無論如何改變虛函數(shù)表的順序及數(shù)量,只要Swift還是通過跳表的方式進(jìn)行函數(shù)調(diào)用,那么我們就無需關(guān)注函數(shù)變化了。為了驗(yàn)證可行性,我們寫Demo測(cè)試一下:

class MyTestClass {
    func helloWorld() {
        print("call helloWorld() in MyTestClass")
    }
}//作為被Hook類及函數(shù)

<--------------------------------------------------->

class HookTestClass: MyTestClass  {
    override func helloWorld() {
        print("\n********** call helloWorld() in HookTestClass **********")
        super.helloWorld()
        print("********** call helloWorld() in HookTestClass end **********\n")
    }
}//通過繼承和重寫的方式進(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 **********

總結(jié)

本文通過介紹Swift的虛函數(shù)表Hook思路,介紹了Swift Mach-O的存儲(chǔ)結(jié)構(gòu)以及運(yùn)行時(shí)的一些調(diào)試技巧。Swift的Hook方案一直是從Objective-C轉(zhuǎn)向Swift開發(fā)的同學(xué)比較感興趣的事情。我們想通過本文向大家介紹關(guān)于Swift更深層的一些內(nèi)容,至于方案本身也許并不是最重要的,重要的是我們希望是否能夠從中Swift的二進(jìn)制中找到更多的應(yīng)用場(chǎng)景。比如,Swift的調(diào)用并不會(huì)存儲(chǔ)到classref中,那如何通過靜態(tài)掃描知道哪些Swift 的類或Struct被調(diào)用了?其實(shí)解決方案也是隱含在本文中。

作者簡(jiǎn)介:

鄧竹立:用戶價(jià)值增長中心-平臺(tái)技術(shù)部-iOS技術(shù)部 資深開發(fā)工程師,WBBlades開源工具作者
蔣演:用戶價(jià)值增長中心-平臺(tái)技術(shù)部-iOS技術(shù)部 架構(gòu)師 58APP-iOS版本需求負(fù)責(zé)人

參考文獻(xiàn):

https://github.com/apple/swift/blob/d68d406dae39ea1677d586714b3991b8f2037dab/lib/IRGen/GenMeta.cpp
http://m.itdecent.cn/p/158574ab8809
http://m.itdecent.cn/p/ef0ff6ee6bc6
https://mp.weixin.qq.com/s/egrQxxJSympB-L6BdVDQVA
https://github.com/alibaba/HandyJSON

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容