引言:业内对Swift 的 Hook 大多数都必须借助 OC 的信息分享特点来完成,文中从改动 Swift 的虚函数表的视角,详细介绍了一种新的 Hook 构思。并为此为主导线,关键详细介绍 Swift 的详尽构造及其运用。
因为历史时间负担的缘故,现阶段流行的大中型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 的混编生态环境保护——新项目编号 ”混天“。
一旦混编绿色生态搭建健全,那麼许多难题将得到解决。
文中的技术规范仅对于根据虚函数表启用的涵数开展 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 构架
最先大家必须掌握 Swift 的涵数怎样启用的。与 Objective-C 不一样,Swift 的调用函数存有三种方法,分别是:根据 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>
- 0x1046b8828 < 124>: mov x20, x0
- 0x1046b882c < 128>: bl 0x1046b8998 ; SwiftDemo.MyTestClass.__allocating_init() -> SwiftDemo.MyTestClass at ViewController.swift:22
- 0x1046b8830 < 132>: stur x0, [x29, #-0x30]
- 0x1042b8834 < 136>: adrp x8, 13
- 0x1046b8838 < 140>: ldr x9, [x8, #0x320]
- 0x1046b883c < 144>: stur x0, [x29, #-0x58]
- 0x1046b8840 < 148>: mov x1, x9
- 0x1046b8844 < 152>: str x8, [sp, #0x60]
- 0x1046b8848 < 156>: bl 0x1042bce88 ; symbol stub for: objc_msgSend
- 0x1042b884c < 160>: mov w11, #0x1
- 0x1046b8850 < 164>: mov x0, x11
- 0x1042b8854 < 168>: ldur x1, [x29, #-0x48]
- 0x1046b8858 < 172>: bl 0x1042bcd5c ; symbol stub for:
从上边的汇编代码中大家非常容易看得出启用了详细地址为0x1042bce88的objc_msgSend 涵数。
虚函数表的浏览也是动态性启用的一种方式,只不过根据浏览虚函数表的方法开展启用。
假定或是以上编码,大家将 @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 存储器中的数据信息从哪里来的,留到后边的章节目录论述。
假定或是以上编码,大家再将 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() 的涵数详细地址。
即然根据虚函数表的发放方式也是一种动态性启用,那麼是否认为着只需大家改动了虚函数表中的涵数详细地址,就完成了涵数的更换?
在以往文章内容《从 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(9ait) | 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 中的涵数详细地址开展了改动,殊不知发觉在运作时涵数并沒有被更换为大家改动的涵数。那到底是如何一回事儿呢?
以上试验的不成功自然是大家的不认真细致造成 的。在新项目一开始大家先科学研究的是种类储存叙述 TypeContext,主要是类的储存叙述 ClassContextDescriptor。在寻找 VTable 后大家主观臆断的觉得运作时 Swift 是根据浏览 ClassContextDescriptor 中的 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中的数据信息。
历经实验后发觉改动后涵数的确是在运作后发生了更改。可是这并沒有完毕,因 为虚函数表与信息推送各有不同,虚函数表中并沒有一切涵数名解析函数详细地址的投射,大家只有根据偏位来改动涵数详细地址。
例如,我觉得改动第一个涵数,那麼我想寻找 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 的关联。
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的详细地址的。
自然,事实上也会出现除外,有时候c语言编译器会立即应用缓存文件的 cache Metadata 的详细地址,而不会再根据 AccessFunction 来获得类的 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 中可以包括2个涵数的关联关联,这类关联与涵数的编译程序次序和总数不相干。
假如 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 **********
文中根据详细介绍 Swift 的虚函数表 Hook 构思,详细介绍了 Swift Mach-O 的存储结构及其运作时的一些调节方法。Swift 的 Hook 计划方案一直是以 Objective-C 转为 Swift 开发设计的同学们较为很感兴趣的事儿。大家想根据文中向大伙儿详细介绍有关 Swift 更深层次的一些內容,对于计划方案自身或许并并不是最重要的,关键的是大家期待是不是可以从这当中 Swift 的二进制中寻找大量的应用领域。例如,Swift 的启用并不会储存到 classref 中,那怎样根据静态数据扫描仪了解什么 Swift 的类或 Struct 被启用了?实际上解决方法也是暗含在文中中。