成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Swift Hook 新思路 -- 虛函數表

開發 前端
本文的技術方案僅針對通過虛函數表調用的函數進行 Hook,不涉及直接地址調用和objc_msgSend 的調用的情況。

[[390109]]

摘要:業界對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 的來調用函數。

假設有如下代碼

  1. class MyTestClass :NSObject { 
  2.     @objc dynamic func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5.  
  6. let myTest = MyTestClass.init() 
  7. myTest.helloWorld() 

編譯后其對應的匯編為

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

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

▐ 3.2 虛函數表的訪問

虛函數表的訪問也是動態調用的一種形式,只不過是通過訪問虛函數表的方式進行調用。

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

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5.  
  6. let myTest = MyTestClass.init() 
  7. myTest.helloWorld() 

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

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

從上面匯編代碼可以看出,經過編譯后最終是通過 blr 指令調用了 x8 寄存器中存儲的函數。至于 x8 寄存器中的數據從哪里來的,留到后面的章節闡述。

▐ 3.3 直接地址調用

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

  1. 0x1048c2114 <+40>:  bl     0x1048c24b8               ; type metadata accessor for SwiftDemo.MyTestClass at <compiler-generated> 
  2. 0x1048c2118 <+44>:  add    x1, sp, #0x10             ; =0x10  
  3. 0x1048c211c <+48>:  bl     0x1048c5174               ; symbol stub for: swift_initStackObject 
  4. 0x1048c2120 <+52>:  bl     0x1048c2388               ; SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 
  5. 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) 中的存儲結構。

  1. struct ClassContextDescriptor{ 
  2.     uint32_t Flag; 
  3.     uint32_t Parent; 
  4.     int32_t  Name
  5.     int32_t  AccessFunction; 
  6.     int32_t  FieldDescriptor; 
  7.     int32_t  SuperclassType; 
  8.     uint32_t MetadataNegativeSizeInWords; 
  9.     uint32_t MetadataPositiveSizeInWords; 
  10.     uint32_t NumImmediateMembers; 
  11.     uint32_t NumFields; 
  12.     uint32_t FieldOffsetVectorOffset; 
  13.     <泛型簽名> //字節數與泛型的參數和約束數量有關 
  14.     <MaybeAddResilientSuperclass>//有則添加4字節 
  15.     <MaybeAddMetadataInitialization>//有則添加4*3字節 
  16.     VTableList[]//先用4字節存儲offset/pointerSize,再用4字節描述數量,隨后N個4+4字節描述函數類型及函數地址。 
  17.     OverrideTableList[]//先用4字節描述數量,隨后N個4+4+4字節描述當前被重寫的類、被重寫的函數描述、當前重寫函數地址。 

從上述結構可以看出,ClassContextDescriptor 的長度是不固定的,不同的類 ClassContextDescriptor 的長度可能不同。那么如何才能知道當前這個類是不是泛型?以及是否有 ResilientSuperclass、MetadataInitialization 特征?其實在前一篇文章《從Mach-O 角度談談 Swift 和 OC 的存儲差異》中已經做了說明,我們可以通過 Flag 的標記位來獲取相關信息。

例如,如果 Flag 的 generic 標記位為 1,則說明是泛型。

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

那么泛型簽名到底能占多少字節呢?Swift 的 GenMeta.cpp 文件中對泛型的存儲做了解釋,整理總結如下:

  1. 假設有泛型有paramsCount個參數,有requeireCount個約束 
  2.  
  3. /** 
  4.      16B  =  4B + 4B + 2B + 2B + 2B + 2B 
  5.      addMetadataInstantiationCache -> 4B 
  6.      addMetadataInstantiationPattern -> 4B 
  7.      GenericParamCount -> 2B 
  8.      GenericRequirementCount -> 2B 
  9.      GenericKeyArgumentCount -> 2B 
  10.      GenericExtraArgumentCount -> 2B 
  11.  */ 
  12.  short pandding = (unsigned)-paramsCount & 3; 
  13.  泛型簽名字節數 = (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() 函數調用前打斷點

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

斷點停留在 0x100230ab0 處👇

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

此時 x0 寄存器中存儲的是 myTest 的地址 x0 = 0x0000000280d08ef0,ldr x8, [x0] 則是將 0x280d08ef0 處存儲的數據放入 x8(注意,這里是只將 *myTest 存入 x8,而不是將 0x280d08ef0 存入 x8)。單步執行后,通過 re read 查看各個寄存器的數據后會發現 x8 存儲的是 type metadata 的地址,而不是 TypeContext 的地址。

  1. x0 = 0x0000000280d08ef0 
  2. x1 = 0x0000000280d00234 
  3. x2 = 0x0000000000000000 
  4. x3 = 0x00000000000008fd 
  5. x4 = 0x0000000000000010 
  6. x5 = 0x000000016fbd188f 
  7. x6 = 0x00000002801645d0 
  8. x7 = 0x0000000000000000 
  9. x8 = 0x000000010023e708  type metadata for SwiftDemo.MyTestClass 
  10. x9 = 0x0000000000000003 
  11. x10= 0x0000000280d08ef0 
  12. x11= 0x0000000079c00000 

經過上步單步執行后,當前程序要做的是 ldr x8, [x8, #0x50],即將 type metadata + 0x50 處的數據存儲到 x8。這一步就是跳表,也就是說經過這一步后,x8 寄存器中存儲的就是 helloWorld() 的地址。

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

那是否真的是這樣呢?ldr x8, [x8, #0x50] 執行后,我們再次查看 x8,看看寄存器中是否為函數地址👇

  1. x0 = 0x0000000280d08ef0 
  2. x1 = 0x0000000280d00234 
  3. x2 = 0x0000000000000000 
  4. x3 = 0x00000000000008fd 
  5. x4 = 0x0000000000000010 
  6. x5 = 0x000000016fbd188f 
  7. x6 = 0x00000002801645d0 
  8. x7 = 0x0000000000000000 
  9. x8 = 0x0000000100231090  SwiftDemo`SwiftDemo.MyTestClass.helloWorld() -> () at ViewController.swift:23 
  10. x9 = 0x0000000000000003 

結果表明 x8 存儲的確實是 helloWorld() 的函數地址。上述實驗表明經過跳轉0x50 位置后,程序找到了 helloWorld() 函數地址。類的 Metadata 位于__DATA 段,是可讀寫的。其結構如下:

  1. struct SwiftClass { 
  2.     NSInteger kind; 
  3.     id superclass; 
  4.     NSInteger reserveword1; 
  5.     NSInteger reserveword2; 
  6.     NSUInteger rodataPointer; 
  7.     UInt32 classFlags; 
  8.     UInt32 instanceAddressPoint; 
  9.     UInt32 instanceSize; 
  10.     UInt16 instanceAlignmentMask; 
  11.     UInt16 runtimeReservedField; 
  12.     UInt32 classObjectSize; 
  13.     UInt32 classObjectAddressPoint; 
  14.     NSInteger nominalTypeDescriptor; 
  15.     NSInteger ivarDestroyer; 
  16.     //func[0] 
  17.     //func[1] 
  18.     //func[2] 
  19.     //func[3] 
  20.     //func[4] 
  21.     //func[5] 
  22.     //func[6] 
  23.     .... 
  24. }; 

上面的代碼在經過0x50 字節的偏移后正好位于 func[0] 的位置。因此要想動態修改函數需要修改Metadata中的數據。

經過試驗后發現修改后函數確實是在運行后發生了改變。但是這并沒有結束,因 為虛函數表與消息發送有所不同,虛函數表中并沒有任何函數名和函數地址的映射,我們只能通過偏移來修改函數地址。

比如,我想修改第1個函數,那么我要找到 Meatadata,并修改 0x50 處的 8 字節數據。同理,想要修改第 2 個函數,那么我要修改 0x58 處的 8 字節數據。這就帶來一個問題,一旦函數數量或者順序發生了變更,那么都需要重新進行修正偏移索引。

舉例說明下,假設當前 1.0 版本的代碼為

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 

此時我們對 0x50 處的函數指針進行了修改。當 2.0 版本變更為如下代碼時,此時我們的偏移應該修改為 0x58,否則我們的函數替換就發生了錯誤。

  1. class MyTestClass { 
  2.     func sayhi() { 
  3.         print("call sayhi() in MyTestClass"
  4.     } 
  5.  
  6.     func helloWorld() { 
  7.         print("call helloWorld() in MyTestClass"
  8.     } 

為了解決虛函數變更的問題,我們需要了解下 TypeContext 與 Metadata 的關系。

8. TypeContext 與 Metadata 的關系

Metadata 結構中的 nominalTypeDescriptor 指向了 TypeContext,也就是說當我們獲取到 Metadata 地址后,偏移 0x40 字節就能獲取到當前這個類對應的 TypeContext地址。那么如何通過 TypeContext 找到 Metadata 呢?

我們還是看剛才的那個 Demo,此時我們將斷點打到 init() 函數上,我們想了解下 MyTestClass 的 Metadata 到底是哪里來的。

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

此時展開為匯編我們會發現,程序準備調用一個函數。

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

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

  1. x0 = 0x0000000000000000 

此時通過 si 單步調試就會發現跳轉到了函數 0x1040f16a8 處,其函數指令較少,如下所示👇

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

在執行 0x1040f16a8 函數執行完后,x0 寄存器就存儲了 MyTestClass 的 Metadata 地址。

  1. 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。

  1. x0 = 0x00000001047e6708  type metadata for SwiftDemo.MyTestClass 

查看 0x1047e6708,繼續偏移 0x40 字節后可以得到 Metadata 結構中的 nominalTypeDescriptor 地址 0x1047e6708 + 0x40 = 0x1047e6748。

查看 0x1047e6748 存儲的數據為 0x1047df4a0。

  1. (lldb) x 0x1047e6748 
  2. 0x1047e6748: a0 f4 7d 04 01 00 00 00 00 00 00 00 00 00 00 00  ..}............. 
  3. 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 存儲的數據為

  1. (lldb) x 0x1047df4ac 
  2. 0x1047df4ac: fc a1 ff ff 70 04 00 00 00 00 00 00 02 00 00 00  ....p........... 
  3. 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字節存儲的函數的相對地址。

  1. struct SwiftMethod { 
  2.     uint32_t Flag; 
  3.     uint32_t Offset; 
  4. }; 

通過 Flag 我們很容易知道是否是動態,是否是實例方法,以及函數類型 Kind。

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

Kind 枚舉如下👇

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

從 Swift 的源碼中可以很明顯的看到,類重寫的函數是單獨存儲的,也就是有單獨的OverrideTable。

并且 OverrideTable 是存儲在 VTable 之后。與 VTable 中的 method 結構不同,OverrideTable 中的函數需要 3 個 4 字節描述:

  1. struct SwiftOverrideMethod { 
  2.     uint32_t OverrideClass;//記錄是重寫哪個類的函數,指向TypeContext 
  3.     uint32_t OverrideMethod;//記錄重寫哪個函數,指向SwiftMethod 
  4.     uint32_t Method;//函數相對地址 
  5. }; 

也就是說 SwiftOverrideMethod 中能夠包含兩個函數的綁定關系,這種關系與函數的編譯順序和數量無關。

如果 Method 記錄用于 Hook 的函數地址,OverrideMethod 作為被Hook的函數,那是不是就意味著無論如何改變虛函數表的順序及數量,只要 Swift 還是通過跳表的方式進行函數調用,那么我們就無需關注函數變化了。

為了驗證可行性,我們寫 Demo 測試一下:

  1. class MyTestClass { 
  2.     func helloWorld() { 
  3.         print("call helloWorld() in MyTestClass"
  4.     } 
  5. }//作為被Hook類及函數 
  6.  
  7. <---------------------------------------------------> 
  8.  
  9. class HookTestClass: MyTestClass  { 
  10.     override func helloWorld() { 
  11.         print("\n********** call helloWorld() in HookTestClass **********"
  12.         super.helloWorld() 
  13.         print("********** call helloWorld() in HookTestClass end **********\n"
  14.     } 
  15. }//通過繼承和重寫的方式進行Hook 
  16.  
  17. <---------------------------------------------------> 
  18.    
  19. let myTest = MyTestClass.init() 
  20.  myTest.helloWorld() 
  21.  
  22.  //do hook 
  23.  print("\n------ replace MyTestClass.helloWorld() with   HookTestClass.helloWorld() -------\n"
  24.  
  25.  WBOCTest.replace(HookTestClass.self); 
  26.  
  27.  //hook 生效 
  28.  myTest.helloWorld() 

運行后,可以看出 helloWorld() 已經被替換成功👇

  1. 2021-03-09 17:25:36.321318+0800 SwiftDemo[59714:5168073] _mh_execute_header = 4368482304 
  2. call helloWorld() in MyTestClass 
  3.  
  4. ------ replace MyTestClass.helloWorld() with HookTestClass.helloWorld() ------- 
  5.  
  6.  
  7. ********** call helloWorld() in HookTestClass ********** 
  8. call helloWorld() in MyTestClass 
  9. ********** call helloWorld() in HookTestClass end ********** 

10. 總結

本文通過介紹 Swift 的虛函數表 Hook 思路,介紹了 Swift Mach-O 的存儲結構以及運行時的一些調試技巧。Swift 的 Hook 方案一直是從 Objective-C 轉向 Swift 開發的同學比較感興趣的事情。我們想通過本文向大家介紹關于 Swift 更深層的一些內容,至于方案本身也許并不是最重要的,重要的是我們希望是否能夠從中 Swift 的二進制中找到更多的應用場景。比如,Swift 的調用并不會存儲到 classref 中,那如何通過靜態掃描知道哪些 Swift 的類或 Struct 被調用了?其實解決方案也是隱含在本文中。

 

責任編輯:姜華 來源: Swift 社區
相關推薦

2022-07-18 15:32:37

C++虛函數表

2010-01-18 17:38:54

C++虛函數表

2017-01-23 11:18:16

戴爾

2009-12-03 10:32:21

2011-09-01 11:12:02

Restaurant 美食應用餐飲應用

2022-05-23 09:18:55

RocketMQ存儲中間件

2015-05-07 14:24:36

everRun

2025-02-04 10:00:30

Spring支付系統

2025-05-12 02:45:00

2016-05-31 10:11:51

2022-05-26 08:53:47

Go函數代碼

2013-08-08 10:06:07

CA TechnoloCA Expo

2013-01-16 10:07:30

加密解密破解Android軟件

2009-01-11 10:27:00

小型辦公室網絡組建

2010-12-03 10:49:11

Virtuozzo

2013-10-12 13:40:09

2017-01-10 14:28:01

數據管理大數據SAP

2024-03-07 09:47:24

高精地圖模型

2022-08-05 23:16:29

元宇宙科技虛擬交互
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品视频免费看 | 久久精品国产一区 | 视频一区 亚洲 | 一级黄色毛片子 | 欧美网站一区 | 在线视频中文字幕 | 久久国产精品免费一区二区三区 | 国产精品福利在线 | www.国产精| 久久午夜国产精品www忘忧草 | 精品久久久久久久久久久久久久久久久 | 国产精品一区二区三区四区 | com.国产| 欧美精品一区二区在线观看 | 中文字幕91 | 精品国产欧美一区二区三区不卡 | 国产成人在线一区二区 | 国产在线视频在线观看 | 国产精品免费一区二区三区四区 | 久久久www成人免费无遮挡大片 | 99精品一区二区三区 | 国产精品一区二区不卡 | 激情小视频 | 久久久久久亚洲 | 中文字幕精 | 狠狠干网站 | 青青草av网站 | 亚洲黄色高清视频 | 欧美中文一区 | 亚洲精品乱码久久久久久9色 | 国产一区二区三区在线免费观看 | 久久精品一区二区三区四区 | 久久久久网站 | 国产精品爱久久久久久久 | 国产精品视频一区二区三区四蜜臂 | 精品欧美一区二区三区精品久久 | 精品久久久久久亚洲综合网 | 国产精品日韩欧美一区二区 | 国产在线精品一区二区三区 | 国产一区二区视频在线 | www.国产精|