Swift Hook 新思路 -- 虚函数表

前端 2023-07-05 17:29:38
188阅读

引言:业内对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. 0x1046b8828 < 124>: mov    x20, x0 
  3. 0x1046b882c < 128>: bl     0x1046b8998               ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22 
  4. 0x1046b8830 < 132>: stur   x0, [x29, #-0x30] 
  5. 0x1042b8834 < 136>: adrp   x8, 13 
  6. 0x1046b8838 < 140>: ldr    x9, [x8, #0x320] 
  7. 0x1046b883c < 144>: stur   x0, [x29, #-0x58] 
  8. 0x1046b8840 < 148>: mov    x1, x9 
  9. 0x1046b8844 < 152>: str    x8, [sp, #0x60] 
  10. 0x1046b8848 < 156>: bl     0x1042bce88               ; symbol stub for: objc_msgSend 
  11. 0x1042b884c < 160>: mov    w11, #0x1 
  12. 0x1046b8850 < 164>: mov    x0, x11 
  13. 0x1042b8854 < 168>: ldur   x1, [x29, #-0x48] 
  14. 0x1046b8858 < 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(9ait) | 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中的数据信息。

历经实验后发觉改动后涵数的确是在运作后发生了更改。可是这并沒有完毕,因 为虚函数表与信息推送各有不同,虚函数表中并沒有一切涵数名解析函数详细地址的投射,大家只有根据偏位来改动涵数详细地址。

例如,我觉得改动第一个涵数,那麼我想寻找 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的详细地址的。

自然,事实上也会出现除外,有时候c语言编译器会立即应用缓存文件的 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 中可以包括2个涵数的关联关联,这类关联与涵数的编译程序次序和总数不相干。

假如 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 被启用了?实际上解决方法也是暗含在文中中。

the end
免责声明:本文不代表本站的观点和立场,如有侵权请联系本站删除!本站仅提供信息存储空间服务。