Swift Hook 新思路 -- 虛函數表
摘要:業界對Swift 的 Hook 大多都需要依靠 OC 的消息轉發特性來實現,本文從修改 Swift 的虛函數表的角度,介紹了一種新的 Hook 思路。并以此為主線,重點介紹 Swift 的詳細結構以及應用。
1. 前言
由于歷史包袱的原因,目前主流的大型APP基本都是以 Objective-C 為主要開發語言。
但是敏銳的同學應該能發現,從 Swift 的 ABI 穩定以后,各個大廠開始陸續加大對 Swift 的投入。
雖然在短期內 Swift 還難以取代 Objective-C,但是其與 Objective-C 并駕齊驅的趨勢是越來越明顯,從招聘的角度就即可管中窺豹。
在過去一年的招聘過程中我們總結發現,有相當數量的候選人只掌握 Swift 開發,對Objective-C 開發并不熟悉,而且這部分候選人大多數比較年輕。
另外,以 RealityKit 等新框架為例,其只支持 Swift 不支持 Objective-C。上述種種現象意味著隨著時間的推移,如果項目不能很好的支持 Swift 開發,那么招聘成本以及應用創新等一系列問題將會凸顯出來。
因此,58 同城在 2020 年 Q4 的時候在集團內發起了跨部門協同項目,從各個層面打造 Objective-C 與 Swift 的混編生態環境——項目代號 ”混天“。
一旦混編生態構建完善,那么很多問題將迎刃而解。
2. 原理簡述
本文的技術方案僅針對通過虛函數表調用的函數進行 Hook,不涉及直接地址調用和objc_msgSend 的調用的情況。
另外需要注意的是,Swift Compiler 設置為 Optimize for speed(Release默認)則TypeContext 的 VTable 的函數地址會清空。
設置為 Optimize for size 則 Swfit 可能會轉變為直接地址調用。
以上兩種配置都會造成方案失效。因此本文重點在介紹技術細節而非方案推廣。
如果 Swift 通過虛函數表跳表的方式來實現方法調用,那么可以借助修改虛函數表來實現方法替換。即將特定虛函數表的函數地址修改為要替換的函數地址。但是由于虛函數表不包含地址與符號的映射,我們不能像 Objective-C 那樣根據函數的名字獲取到對應的函數地址,因此修改 Swift 的虛函數是依靠函數索引來實現的。
簡單理解就是將虛函數表理解為數組,假設有一個 FuncTable[],我們修改函數地址只能通過索引值來實現,就像 FuncTable[index] = replaceIMP 。但是這也涉及到一個問題,在版本迭代過程中我們不能保證代碼是一層不變的,因此這個版本的第 index 個函數可能是函數 A,下個版本可能第 index 個函數就變成了函數 B。顯然這對函數的替換會產生重大影響。
為此,我們通過 Swift 的 OverrideTable 來解決索引變更的問題。在 Swift 的OverrideTable 中,每個節點都記錄了當前這個函數重寫了哪個類的哪個函數,以及重寫后函數的函數指針。
因此只要我們能獲取到 OverrideTable 也就意味著能獲取被重寫的函數指針 IMP0 以及重寫后的函數指針 IMP1。只要在 FuncTable[] 中找到 IMP0 并替換成 IMP1 即可完成方法替換。
接下來將詳細介紹Swift的函數調用、TypeContext、Metadata、VTable、OverrideTable 等細節,以及他們彼此之間有何種關聯。為了方便閱讀和理解,本文所有代碼及運行結果,都是基于 arm64 架構
3. Swift 的函數調用
首先我們需要了解 Swift 的函數如何調用的。與 Objective-C 不同,Swift 的函數調用存在三種方式,分別是:基于 Objective-C 的消息機制、基于虛函數表的訪問、以及直接地址調用。
▐ 3.1 Objective-C 的消息機制
首先我們需要了解在什么情況下 Swift 的函數調用是借助 Objective-C 的消息機制。如果方法通過 @objc dynamic 修飾,那么在編譯后將通過 objc_msgSend 的來調用函數。
假設有如下代碼
- class MyTestClass :NSObject {
- @objc dynamic func helloWorld() {
- print("call helloWorld() in MyTestClass")
- }
- }
- let myTest = MyTestClass.init()
- myTest.helloWorld()
編譯后其對應的匯編為
- 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:
從上面的匯編代碼中我們很容易看出調用了地址為0x1042bce88的objc_msgSend 函數。
▐ 3.2 虛函數表的訪問
虛函數表的訪問也是動態調用的一種形式,只不過是通過訪問虛函數表的方式進行調用。
假設還是上述代碼,我們將 @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
從上面匯編代碼可以看出,經過編譯后最終是通過 blr 指令調用了 x8 寄存器中存儲的函數。至于 x8 寄存器中的數據從哪里來的,留到后面的章節闡述。
▐ 3.3 直接地址調用
假設還是上述代碼,我們再將 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>
這是大家就會發現bl 指令后跟著的是一個常量地址,并且是 SwiftDemo.MyTestClass.helloWorld() 的函數地址。
4. 思考
既然基于虛函數表的派發形式也是一種動態調用,那么是不是以為著只要我們修改了虛函數表中的函數地址,就實現了函數的替換?
5. 基于 TypeContext 的方法交換
在往期文章《從 Mach-O 角度談談 Swift 和 OC 的存儲差異》我們可以了解到在Mach-O 文件中,可以通過 __swift5_types 查找到每個 Class 的ClassContextDescriptor,并且可以通過 ClassContextDescriptor 找到當前類對應的虛函數表,并動態調用表中的函數。
注意:(在 Swift 中,Class/Struct/Enum 統稱為 Type,為了方便起見,我們在文中提到的TypeContext 和 ClassContextDescriptor 都指的是 ClassContextDescriptor)。
首先我們來回顧下 Swift 的類的結構描述,結構體 ClassContextDescriptor 是 Swift 類在Section64(__TEXT,__const) 中的存儲結構。
- 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;
- <泛型簽名> //字節數與泛型的參數和約束數量有關
- <MaybeAddResilientSuperclass>//有則添加4字節
- <MaybeAddMetadataInitialization>//有則添加4*3字節
- VTableList[]//先用4字節存儲offset/pointerSize,再用4字節描述數量,隨后N個4+4字節描述函數類型及函數地址。
- OverrideTableList[]//先用4字節描述數量,隨后N個4+4+4字節描述當前被重寫的類、被重寫的函數描述、當前重寫函數地址。
- }
從上述結構可以看出,ClassContextDescriptor 的長度是不固定的,不同的類 ClassContextDescriptor 的長度可能不同。那么如何才能知道當前這個類是不是泛型?以及是否有 ResilientSuperclass、MetadataInitialization 特征?其實在前一篇文章《從Mach-O 角度談談 Swift 和 OC 的存儲差異》中已經做了說明,我們可以通過 Flag 的標記位來獲取相關信息。
例如,如果 Flag 的 generic 標記位為 1,則說明是泛型。
- | TypeFlag(16bit) | version(8bit) | generic(1bit) | unique(1bit) | unknow (1bi) | Kind(5bit) |
- //判斷泛型
- (Flag & 0x80) == 0x80
那么泛型簽名到底能占多少字節呢?Swift 的 GenMeta.cpp 文件中對泛型的存儲做了解釋,整理總結如下:
- 假設有泛型有paramsCount個參數,有requeireCount個約束
- /**
- 16B = 4B + 4B + 2B + 2B + 2B + 2B
- addMetadataInstantiationCache -> 4B
- addMetadataInstantiationPattern -> 4B
- GenericParamCount -> 2B
- GenericRequirementCount -> 2B
- GenericKeyArgumentCount -> 2B
- GenericExtraArgumentCount -> 2B
- */
- short pandding = (unsigned)-paramsCount & 3;
- 泛型簽名字節數 = (16 + paramsCount + pandding + 3 * 4 * (requeireCount) + 4);
因此只要明確了 Flag 各個標記位的含義以及泛型的存儲長度規律,那么就能計算出虛函數表 VTable 的位置以及各個函數的字節位置。
了解了泛型的布局以及 VTable 的位置,是不是就意味著能實現函數指針的修改了呢?答案當然是否定的,因為 VTable 存儲在 __TEXT 段,__TEXT 是只讀段,我們沒辦法直接進行修改。不過最終我們通過 remap 的方式修改代碼段,將 VTable 中的函數地址進行了修改,然而發現在運行時函數并沒有被替換為我們修改的函數。那到底是怎么一回事呢?
6. 基于 Metadata 的方法交換
上述實驗的失敗當然是我們的不嚴謹導致的。在項目一開始我們先研究的是類型存儲描述 TypeContext,主要是類的存儲描述 ClassContextDescriptor。在找到 VTable 后我們想當然的認為運行時 Swift 是通過訪問 ClassContextDescriptor 中的 VTable 進行函數調用的。但是事實并不是這樣。
7. VTable 函數調用
接下來我們將回答下 Swift的函數調用 章節中提的問題,x8 寄存器的函數地址是從哪里來的。還是前文中的 Demo,我們在 helloWorld() 函數調用前打斷點
- let myTest = MyTestClass.init()
- -> myTest.helloWorld()
斷點停留在 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
此時 x0 寄存器中存儲的是 myTest 的地址 x0 = 0x0000000280d08ef0,ldr x8, [x0] 則是將 0x280d08ef0 處存儲的數據放入 x8(注意,這里是只將 *myTest 存入 x8,而不是將 0x280d08ef0 存入 x8)。單步執行后,通過 re read 查看各個寄存器的數據后會發現 x8 存儲的是 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
經過上步單步執行后,當前程序要做的是 ldr x8, [x8, #0x50],即將 type metadata + 0x50 處的數據存儲到 x8。這一步就是跳表,也就是說經過這一步后,x8 寄存器中存儲的就是 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] 執行后,我們再次查看 x8,看看寄存器中是否為函數地址👇
- 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
結果表明 x8 存儲的確實是 helloWorld() 的函數地址。上述實驗表明經過跳轉0x50 位置后,程序找到了 helloWorld() 函數地址。類的 Metadata 位于__DATA 段,是可讀寫的。其結構如下:
- 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]
- ....
- };
上面的代碼在經過0x50 字節的偏移后正好位于 func[0] 的位置。因此要想動態修改函數需要修改Metadata中的數據。
經過試驗后發現修改后函數確實是在運行后發生了改變。但是這并沒有結束,因 為虛函數表與消息發送有所不同,虛函數表中并沒有任何函數名和函數地址的映射,我們只能通過偏移來修改函數地址。
比如,我想修改第1個函數,那么我要找到 Meatadata,并修改 0x50 處的 8 字節數據。同理,想要修改第 2 個函數,那么我要修改 0x58 處的 8 字節數據。這就帶來一個問題,一旦函數數量或者順序發生了變更,那么都需要重新進行修正偏移索引。
舉例說明下,假設當前 1.0 版本的代碼為
- class MyTestClass {
- func helloWorld() {
- print("call helloWorld() in MyTestClass")
- }
- }
此時我們對 0x50 處的函數指針進行了修改。當 2.0 版本變更為如下代碼時,此時我們的偏移應該修改為 0x58,否則我們的函數替換就發生了錯誤。
- class MyTestClass {
- func sayhi() {
- print("call sayhi() in MyTestClass")
- }
- func helloWorld() {
- print("call helloWorld() in MyTestClass")
- }
- }
為了解決虛函數變更的問題,我們需要了解下 TypeContext 與 Metadata 的關系。
8. TypeContext 與 Metadata 的關系
Metadata 結構中的 nominalTypeDescriptor 指向了 TypeContext,也就是說當我們獲取到 Metadata 地址后,偏移 0x40 字節就能獲取到當前這個類對應的 TypeContext地址。那么如何通過 TypeContext 找到 Metadata 呢?
我們還是看剛才的那個 Demo,此時我們將斷點打到 init() 函數上,我們想了解下 MyTestClass 的 Metadata 到底是哪里來的。
- -> let myTest = MyTestClass.init()
- myTest.helloWorld()
此時展開為匯編我們會發現,程序準備調用一個函數。
- -> 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
在執行 bl 0x1040f16a8 指令之前,x0 寄存器為 0。
- x0 = 0x0000000000000000
此時通過 si 單步調試就會發現跳轉到了函數 0x1040f16a8 處,其函數指令較少,如下所示👇
- 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
在執行 0x1040f16a8 函數執行完后,x0 寄存器就存儲了 MyTestClass 的 Metadata 地址。
- x0 = 0x00000001047e6708 type metadata for SwiftDemo.MyTestClass
那么這個被標記為 type metadata accessor for SwiftDemo.MyTestClass at
在上文介紹的 struct ClassContextDescriptor 貌似有個成員是 AccessFunction,那這個 ClassContextDescriptor 中的 AccessFunction 是不是 Metadata 的訪問函數呢?這個其實很容易驗證。
我們再次運行 Demo,此時metadata accessor 為 0x1047d96a8,繼續執行后Metadata地址為 0x1047e6708。
- x0 = 0x00000001047e6708 type metadata for SwiftDemo.MyTestClass
查看 0x1047e6708,繼續偏移 0x40 字節后可以得到 Metadata 結構中的 nominalTypeDescriptor 地址 0x1047e6708 + 0x40 = 0x1047e6748。
查看 0x1047e6748 存儲的數據為 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 字節處,因此對 0x1047df4a0 + 12 可知 AccessFunction 的位置為 0x1047df4ac。繼續查看 0x1047df4ac 存儲的數據為
- (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 為相對地址,因此我們做一次地址計算 0x1047df4ac + 0xffffa1fc - 0x10000000 = 0x1047d96a8,與 metadata accessor 0x1047d96a8 相同,這就說明 TypeContext 是通過 AccessFunction 來獲取對應的Metadata的地址的。
當然,實際上也會有例外,有時編譯器會直接使用緩存的 cache Metadata 的地址,而不再通過 AccessFunction 來獲取類的 Metadata。
9. 基于 TypeContext 和 Metadata 的方法交換
在了解了 TypeContext 和 Metadata 的關系后,我們就能做一些設想了。在 Metadata中雖然存儲了函數的地址,但是我們并不知道函數的類型。這里的函數類型指的是函數是普通函數、初始化函數、getter、setter 等。
在 TypeContext 的 VTable 中,method 存儲一共是 8 字節,第一個4字節存儲的函數的 Flag,第二個4字節存儲的函數的相對地址。
- struct SwiftMethod {
- uint32_t Flag;
- uint32_t Offset;
- };
通過 Flag 我們很容易知道是否是動態,是否是實例方法,以及函數類型 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 的源碼中可以很明顯的看到,類重寫的函數是單獨存儲的,也就是有單獨的OverrideTable。
并且 OverrideTable 是存儲在 VTable 之后。與 VTable 中的 method 結構不同,OverrideTable 中的函數需要 3 個 4 字節描述:
- struct SwiftOverrideMethod {
- uint32_t OverrideClass;//記錄是重寫哪個類的函數,指向TypeContext
- uint32_t OverrideMethod;//記錄重寫哪個函數,指向SwiftMethod
- uint32_t Method;//函數相對地址
- };
也就是說 SwiftOverrideMethod 中能夠包含兩個函數的綁定關系,這種關系與函數的編譯順序和數量無關。
如果 Method 記錄用于 Hook 的函數地址,OverrideMethod 作為被Hook的函數,那是不是就意味著無論如何改變虛函數表的順序及數量,只要 Swift 還是通過跳表的方式進行函數調用,那么我們就無需關注函數變化了。
為了驗證可行性,我們寫 Demo 測試一下:
- class MyTestClass {
- func helloWorld() {
- print("call helloWorld() in MyTestClass")
- }
- }//作為被Hook類及函數
- <--------------------------------------------------->
- class HookTestClass: MyTestClass {
- override func helloWorld() {
- print("\n********** call helloWorld() in HookTestClass **********")
- super.helloWorld()
- print("********** call helloWorld() in HookTestClass end **********\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()
運行后,可以看出 helloWorld() 已經被替換成功👇
- 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 **********
10. 總結
本文通過介紹 Swift 的虛函數表 Hook 思路,介紹了 Swift Mach-O 的存儲結構以及運行時的一些調試技巧。Swift 的 Hook 方案一直是從 Objective-C 轉向 Swift 開發的同學比較感興趣的事情。我們想通過本文向大家介紹關于 Swift 更深層的一些內容,至于方案本身也許并不是最重要的,重要的是我們希望是否能夠從中 Swift 的二進制中找到更多的應用場景。比如,Swift 的調用并不會存儲到 classref 中,那如何通過靜態掃描知道哪些 Swift 的類或 Struct 被調用了?其實解決方案也是隱含在本文中。