背景
在上一篇文章中,我们介绍了 ARM64 中的汇编基础,并且知道了在汇编当中,调用一个方法的前后在栈空间是怎样表现的,以及内存中的几个特殊寄存器是如何操作的。
今天这篇文章,我们来详细详细分析一下使用汇编来 Hook objc_msgSend 方法的全部流程。
Hook 思路梳理
对于 objc_msgSend 这个我们要 Hook 的方法,我们首先要搞清楚,这是一个什么样的方法?我需要用什么方案才能 Hook 到它的入口。首先我们来整理以下我们拥有的 Hook 方案:
- 基于 Objective-C Runtime 的 Method Swizzling:也就是我们经常使用的
class_replaceMethod方法; - 基于 fishhook 的 Hook:由于在 Mach-O 当中,有 Bind 和 Lazy Bind 的两个概念,所以 Facebook 通过修改
__la_symbol和__nl_symbol两个表的指针,在二次调用的时候,直接通过__la_symbol_ptr找到函数地址直接调用,从而不用多次繁琐的进行函数寻址; - 基于 Dobby 的 Inline Hook:Dobby 是通过插入
__zDATA段和__zTEXT段到 Mach-O 中。__zDATA用来记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。__zText用来记录每个 Hook 函数的跳转指令。Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O 上进行操作,而是重新生成并替换。关于 Dobby 中的奇技淫巧还有很多,如果有可能后续会出一个分析文章(插旗子)。
当然,成熟的 Hook 方案还有很多,并且这些经常出现在逆向工程中,我这里只是列举了最常用的三个。
什么是 Inline Hook
首先给个定义:Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。

上图展示了 Inline Hook 大致的思路:
- 将原函数的前 N 个字节搬运到 Hook 函数的前 N 个字节;
- 然后将原函数的前 N 个字节填充跳转到 Hook 函数的跳转指令;
- 在 Hook 函数末尾几个字节填充跳转回原函数 +N 的跳转指令;
以上的 N 有多大,取决于你的跳转指令写得有多大(占用了多少指令)。
相较与 Inline Hook, fishhook 使用的是很 Trick 的方式,通过劫持 stub 从而达到替换的目的。
在罗巍的「iOS 应用逆向与安全之道」中将 fishhook 归类成 Inline Hook。从广义的定义上来说,只要完成重定向到我们自己的方法,并在远方法前后可定制处理就可以算作 Inline Hook。但是我和页面仔讨论的结果是,这里的 Inline 应该要理解为 Inline Modification,这种技术通常如上图所示,覆盖方法开头指令中的前几个字节,完成 Hook 方法的重定向工作。
fishhook 完成跳转
汇编实现的 objc_msgSend 为什么可以当作 C 方法?
通过查看 objc_msgSend ,我们知道 Runtime 的 Method Swizzling 并不适用,因为它并不是 Objective-C 方法,调用时并不会有我们经常说的“消息转发”;通过查看 Runtime 源码,我们发现 objc_msgSend 是使用纯汇编实现函数,通过汇编文件我们可以看到以下定义:
ENTRY _objc_msgSend
这里的 ENTRY 是什么意思呢?在文件中继续搜索 ENTRY 我们找到了这么一个宏:
.macro ENTRY /* name */
.text
.align 5
.globl $0
$0:
.endmacro
这里定义了一个汇编宏,表示在 text 段定义一个 global 的 _objc_msgSend ,$0 其实就是这个宏传入的参数,也就是一个方法入口。我们可以手动将这个宏来展开:
.text
.align 5
.globl _objc_msgSend
; ...
这里我们发现,在第三行的位置通过 C 的 name mangling 命名规则,将符号 _objc_msgSend 映射为 C 的全局方法符号。也就是说,这段汇编可以通过头文件声明,便已完成了 C 的函数定义。我们在后续处理的时候可以将其视为 C 方法。
当然我们也可以使用 MachOView 来验证这个符号名。

这里如果不太明白如何使用纯汇编实现 C 方法,可以看高级页面仔的这篇文章「在Xcode工程中嵌入汇编代码」。
fishhook 实现的基础
既然我们将 objc_msgSend 已经视作 C 方法,那么我就可以使用 fishhook 来完成 Inline Hook 的第一步:跳到 Hook 方法。fishhook 是如何做的呢?它是在什么阶段完成这个动作的?来看下图:

我们知道,Apple 自身的共享缓存库其实不会编译进我们自己的 Mach-O 中的,而是在 App 启动后的动态链接才会去做重绑定操作。这里我们要如何去验证呢?首先我们写一个 fishhook 的 demo:
#import "ViewController.h"
#import "fishhook.h"
@implementation ViewController
static void (*ori_nslog)(NSString * format, ...);
void new_nslog(NSString * format, ...) {
//自定义的替换函数
format = [format stringByAppendingFormat:@" Gua "];
ori_nslog(format);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"hello world");
struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = new_nslog;
nslog.replaced = (void *)&ori_nslog;
rebind_symbols((struct rebinding[1]){nslog}, 1);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"hello world");
}
@end
编译运行之后发现输出 hello world ,点击屏幕后,成功 Hook NSLog 方法,输出 hello world Gua 。
2020-08-02 23:41:50.846939+0800 TestHook[13516:519138] hello world
2020-08-02 23:41:53.681944+0800 TestHook[13516:519138] hello world Gua
为什么可以 Hook?函数地址不是在编译之后就确定了吗?
其实并不是,我们可以使用 nm -n 命令来查看一下所有的方法符合及其对应地址:
$ nm -n TestHook.app/TestHook
U _NSLog
U _NSStringFromClass
U _OBJC_CLASS_$_UIResponder
U _OBJC_CLASS_$_UISceneConfiguration
U _OBJC_CLASS_$_UIViewController
...
U _objc_msgSend
U _objc_msgSendSuper2
U _objc_opt_class
...
0000000100006690 b _ori_nslog
0000000100006698 b __rebindings_head
在这里我们就可以发现,其实 NSLog 方法其实并没有地址,这些系统库函数并不会打入到我们的 App 包中;当我们使用它们时,dyld 就要从共享的动态库中查找对应方法,然后将具体的函数地址绑定到之前声明的地方,从而实现系统库方法的调用。
另外说句题外话作为了解,对于这种可在主存中任意位置正确地执行,并且不受其绝对地址影响的技术,在计算机领域称之为 PIC(Position Independent Code)技术。
fishhook 对于 Mach-O 利用
首先我们要知道 Mach-O 中 __DATA 段有两个 Section 与动态符号绑定有关系:
__nl_symbol_ptr:存储了non-lazily绑定的符号,这些符号在 Mach-O 加载的时候绑定完成;__la_symbol_ptr:存储了lazy绑定的方法,这些方法在第一次调用时,由dyld_stub_binder进行绑定;
既然 __la_symbol_ptr 存储了所有 lazy 绑定的方法,那也就是说在这些位置应该存储了对应方法的地址。我们通过 lldb 来验证一下。
我们在第一个 hello world 的位置增加断点,并使用 image list 命令来获取 App 的基地址:

在这个例子中,我们的 App 基地址为 0x00000001063b6000 。当然你做实验的时候可能基地址会有所改变,因为 ASLR(Address Spce Layout Randomizatio) 的缘故。
然后我们使用 MachOView 来查看在 __la_symbol_str 中的偏移量。

发现是 0x5000 ,所以我们在 lldb 中使用 x 命令来查看 0x00000001063b6000 + 0x5000 的数据是什么:
(lldb) x 0x00000001063b6000+0x5000
0x1063bb000: f0 83 3b 06 01 00 00 00 69 98 93 25 ff 7f 00 00 ..;.....i..%....
0x1063bb010: 23 53 32 49 ff 7f 00 00 54 84 3b 06 01 00 00 00 #S2I....T.;.....
发现在此位置的数据是 0x01063b83f0 。使用反汇编 dis 命令, 来看对应地址所指向的代码段:
(lldb) dis -s 0x01063b83f0
0x1063b83f0: pushq $0x0
0x1063b83f5: jmp 0x1063b83e0
0x1063b83fa: pushq $0xd
0x1063b83ff: jmp 0x1063b83e0
0x1063b8404: pushq $0x127 ; imm = 0x127
0x1063b8409: jmp 0x1063b83e0
那么这段代码到底是我们的 NSLog 的代码吗?我们可以直接对当前断点对栈顶进行一次反汇编来确定一下结果。在 lldb 中直接输入 dis 命令:

可以看到汇编中 callq 命令对应对地址是 0x1063b8354 。对这个地址再次进行 dis -s 反汇编来查看:

我们发现其中 0x00000001063b83f0 这个待跳转的地址,就是上面 0x00000001063b6000 + 0x5000 这个位置存储的地址。
再这之后,我们再对点击事件中的 NSLog 方法下一个断点,并且点击一下模拟器屏幕来触发一下。我们再使用 x 和 dis -s 两个命令来查看一下 0x00000001063b6000 + 0x5000 中的新数据:

我们发现,其指向地址已经变成了 0x01063b7380 ,使用反汇编 dis 命令来查看的时候,也给出了相应的函数符号 new_nslog 。至此,fishhook Hook C 方法已经完成。
fishhook 思路总结
其实 fishhook 的 Hook 思路,也就是我们上述所描述的,当第一次调用系统动态库中 C 方法时,去替换掉 __la_symbol_str 的指针。但是它的逻辑要比这个思路还是要复杂一些,比如 fishhook 要解决以下问题:
- 使用数据结构来描述所有 Hook 方法?
- 如何通过方法名来找到对应的 Lazy 指针?
- 如何计算对应方法的地址?
- 如果是非 Lazy 表中要如何处理?
- 如何查找到对应符号名称?
诸如此类的问题还有很多。如果想看具体的实现,推荐去阅读源码。当然我们归纳 fishhook 来修改 C 方法的本质,那就是:dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol 方法更新两个符号位置来进行符号的重新绑定。

内联汇编实现 Hook
上文讲述了使用 fishhook 来 Hook 系统库中的 C 方法,那么我们已经完成了以下这两个阶段(绿色位置):

换句话说也就是我们通过 fishhook 已经完成了入口的重定向。但是 objc_msgSend 这个原方法我们又是不能不去调用的,因为我们只是希望在 Hook 的前后,增加自定义的事件,并不想去完整替换原先的消息转发逻辑。
所以,我们只要在 Hook 方法中调用原函数就可以解决问题了。
但是新的问题又出现了!objc_msgSend 到底应该如何传参呢?
objc_msgSend 方法定义
在 message.h 文件中,objc_msgSend 有如下定义:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
看了函数定义,难点其实就是不定参数。既然是不定参数,其实我们可以根据不定参数的原理,在 va_list 来解析数组,就可以获取到所有参数了。虽然使用 va_list 是行的通的,但是我们需要考虑 va_list 在不同平台上的数据结构差异。这一点可以参看前辈 bang 在开发 JSPatch 的时候所做的笔记「JSPatch 实现原理详解」。
这个其实我也和页面仔讨论了一番。页面仔说其实使用
va_list可变参数应该会有很大的坑,因为在objc_msgSend方法中不是传统意义上的可变参数。可以参考这篇文章「Apple 为什么要修改 objc_msgSend 的原型」。
但既然都已经对不同平台做差异化处理了,那么干脆就直接使用内联汇编来实现传参逻辑就可以了?在目前的 Hook 方案中,也就是直接使用 ARM 汇编来处理的。
记录上下文
通过上一篇文章「为什么使用汇编可以 Hook objc_msgSend(上)- 汇编基础」,我们了解到 X0 - X7 用来存储传递参数,我们仅从一个方法体的角度上来讲,可能参数已经是上下文的全部内容了。但是,我们从寄存器的角度上来看问题,上下文内容还有其他的东西,比如 Q0 - Q7 这八个浮点数寄存器。
所以为了保存方法的上下文,我们通过将 X0 - X9、Q0 - Q7 这些寄存器的值压栈,从而记录下调用 objc_msgSend 方法的所有上下文,其压栈方法直接模仿上文中调用函数时的压栈方法即可:
; sp 指向栈顶
; 保存 {q0 - q7} ,偏移地址到 sp 寄存器
stp q6, q7, [sp, #-32]!
stp q4, q5, [sp, #-32]!
stp q2, q3, [sp, #-32]!
stp q0, q1, [sp, #-32]!
; 保存 {x0 - x8, lr}
stp x8, x9, [sp, #-16]!
stp x6, x7, [sp, #-16]!
stp x4, x5, [sp, #-16]!
stp x2, x3, [sp, #-16]!
stp x0, x1, [sp, #-16]!
为什么这里处理参数 X0-X7 要记录,还需要记录 X8 和 X9 两个寄存器呢?因为我们在 pre_objc_msgSend 会对其改变。为了还原之前所有寄存器的状态,最保险的方式就是全部记录。
调用 pre_objc_msgSend 方法
在保存完上下文之后,定义一个 pre_objc_msgSend 的方法,其作用是用来自己定制 objc_msgSend 之前发生的事情。
往往我们 Hook objc_msgSend 的目的,就是想记录方法的上下文信息。例如我们想度量慢函数的时间,则我们需要其方法名、所在的 Class 以及其上层调用方法和历史堆栈等。那么在定义 pre_objc_msgSend 方法的如参时,我们可以将 id self (self 实例指针),SEL _cmd (方法 ID),uintptr_t lr (LR 寄存器,函数调用后的返回地址)传入即可。
这里的 LR 记录其实并不是为我们业务而服务的,而是需要为堆栈记录,这样在 Hook 的最后可以跳转回上一层方法。
对应的,我们定义一个带有这三个参数的 pre_objc_msgSend 方法:
void pre_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
// pre action...
}
为了将对应的参数传入,我们利用 X0-X2 寄存器来传参。因为 X0 和 X1 对应的就是 self 和 _cmd ,所以我们只需要将 LR 传递到 X2 中即可:
; 将 lr 传入 x2
mov x2, lr
接下来我们来实现调用 pre_objc_msgSend 方法。
我先给出汇编,然后我们来解析它的意思:
__asm volatile ("stp x8, x9, [sp, #-16]!\n");
__asm volatile ("mov x12, %0\n"
:
: "r"(&pre_objc_msgSend));
__asm volatile ("ldp x8, x9, [sp], #16\n");
__asm volatile ("blr x12\n");
第一行和第三行是用来记录和恢复 X8 和 X9 两个寄存器,其原因是因为使用了 %0 这个指令操作数,这是个什么东西呢?
为了讲解方便,举一个其他的例子。假如我想使用内联汇编写一个加法函数,我可以写出以下代码:
int arm_sum(int a, int b) {
int sum = 0;
asm volatile("add %0, %1, %2" // 1
: "=r" (sum) // 2
: "r" (a), "r" (b) // 3
:);
return sum;
}
- (void)viewDidLoad {
[super viewDidLoad];
int a = arm_sum(2, 3);
NSLog(@"sum = %d", a); // sum = 5
}
- 这一行称作汇编语句模版。
%0、%1、%2代表指令操作数,也可以代表通用寄存器(需要注意的是,这不是 ARM 汇编,而是基于汇编的上层语言)。分别代表sum、a和b(为什么是这个顺序?可以看下面的 2 和 3)。这种操作数在汇编语句模版中只能有 10 个,即%0 - %9。当然这里的代码在 Xcode 可能会有 warning,Xcode 建议我们修改成%w0,这个w代表这个变量的宽度,w是 32 位,x是 64 位。 - 第一个冒号
:分割的位置是输出参数。=r其实是两个操作符,我们可以分开来看。**=代表sum变量是输出操作符,r代表按照顺序与某个通用寄存器相关联,由于它是第一个关联的,自然就进入了%0**。 - 第二个冒号
:分割的位置是输入参数。继续使用操作符r将a和b以此放入通用寄存器%1和%2中。
搞懂了这个你应该就可以明白上面那个:
__asm volatile ("mov x12, %0\n"
:
: "r"(&pre_objc_msgSend));
是什么意思。对的,其实就是把 pre_objc_msgSend 的地址取出,然后放到 X12 寄存器。
那么为什么要记录和恢复 X8 和 X9 两个寄存器呢?我们在这里先记住一个结论:当使用汇编语句模版时,就会用到 X8 、X9 寄存器,因为它的下层实现是通过这些通用寄存器来做的。
所以为了恢复之前的上下文,我们就再一次的利用栈来保存一下这两个通用寄存器,在正式调用pre_objc_msgSend 方法之前将其复原即可。
最后一行使用 blr x12 ,正式调用 pre_objc_msgSend 方法。
恢复上下文并调用原函数
恢复上下文和之前的记录上下文是逆操作,所以不做过多分析:
ldp x0, x1, [sp], #16
ldp x2, x3, [sp], #16
ldp x4, x5, [sp], #16
ldp x6, x7, [sp], #16
ldp x8, x9, [sp], #16
ldp q0, q1, [sp], #32
ldp q2, q3, [sp], #32
ldp q4, q5, [sp], #32
ldp q6, q7, [sp], #32
继续使用我们上述方法进行调用的内联汇编代码来调用远方法 objc_msgSend 。在这里假设我们已经使用 fishhook 将其实现 Hook 到我们新的内联汇编方法体 hook_objc_msgSend ,原方法放到 origin_objc_msgSend 上。所以对应的内联汇编代码:
__asm volatile ("stp x8, x9, [sp, #-16]!\n");
__asm volatile ("mov x12, %0\n"
:
: "r"(&origin_objc_msgSend));
__asm volatile ("ldp x8, x9, [sp], #16\n");
__asm volatile ("blr x12\n");
调用 post_objc_msgSend 并恢复 LR
在上一篇文章我们讲述过 LR 寄存器是用来记录函数调用完成时的返回地址的。但是经历了咱们多次其他方法的调用,我们没有办法确定 LR 的值是正确的。所以在 hook_objc_msgSend 方法的最后,我们需要将 LR 值进行恢复。
那么我们用什么方法来恢复 LR 寄存器值呢?这里有一种比较 trick 的方法,就是用一个全局数组来记录 LR 寄存器的值。我们用数组来模拟一个栈结构,其实就可以对应的找到其 LR (也许你会说这里会有线程安全的问题。是的,我在这里只是做一个示例,如果你有更加健壮的方法,也可以去扩写)。
我们改写一下 pre_objc_msgSend 方法来记录 LR 的值:
// 假设方法调用最多有 10000 层
uintptr_t l_ptr_t[10000];
// LR 栈的游标
int cur = 0;
void pre_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
printf("before objc msgSend\n");
// 记录 lr,且游标 +1
l_ptr_t[cur ++] = lr;
}
根据之前的知识,我们知道方法的返回值在 ret 之后,会被存入 X0 寄存器。所以我们在 post_objc_msgSend 中将上文记录的 LR 值返回,然后在使用 X0 寄存器来恢复 LR 寄存器。
// 返回记录的 lr 值
uintptr_t post_objc_msgSend() {
if (cur != 0) {
cur --;
}
return l_ptr_t[cur];
}
// hook_objc_msgSend 内部
...
// 省略 保存上下文过程
...
__asm volatile ("stp x8, x9, [sp, #-16]!\n");
__asm volatile ("mov x12, %0\n"
:
: "r"(&post_objc_msgSend));
__asm volatile ("ldp x8, x9, [sp], #16\n");
__asm volatile ("blr x12\n");
// 恢复 lr
__asm volatile ("mov lr, x0\n");
// 省略 恢复上下文过程
...
// return
__asm volatile ("ret\n");
如此,我们完成了使用内联汇编来 Hook objc_msgSend 方法的全部实现代码。
简单封装
在上面的全流程中,我们已经全部解析了 Hook objc_msgSend 的全部实现细节,但是我们发现,其实主要的子操作就分成 3 个:
- 记录上下文;
- 恢复上下文;
- 调用方法;
所以我们可以使用宏来对这三个子操作进行抽象封装。
在下面代码中,我们将三个操作分别封装成了 save() 、load() 和 call(value) 三个宏,这样在 Hook 流程上就一目了然了。
#import "objc_msgSend_hook.h"
#import "fishhook.h"
#include <dispatch/dispatch.h>
#define call(value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile ("blr x12\n");
#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");
#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );
__unused static id (*orig_objc_msgSend)(id, SEL, ...);
uintptr_t l_ptr_t[10000];
int cur = 0;
void pre_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
printf("pre action...\n");
// 做一个简单对测试,输出 ObjC 方法名
printf("\t%s\n", object_getClassName(self));
printf("\t%s\n", _cmd);
l_ptr_t[cur ++] = lr;
}
uintptr_t post_objc_msgSend() {
printf("post action...\n");
if (cur != 0) {
cur --;
}
return l_ptr_t[cur];
}
__attribute__((__naked__))
static void hook_Objc_msgSend() {
// 记录上下文
save()
// 将 lr 传入 x2 用于 pre_objc_msgSend 传参
__asm volatile ("mov x2, lr\n");
// 调用 pre_objc_msgSend
call(&pre_objc_msgSend)
// 还原上下文
load()
// 调用 objc_msgSend 原方法
call(orig_objc_msgSend)
// 记录上下文
save()
// 调用 post_objc_msgSend
call(&post_objc_msgSend)
// 还原 lr
__asm volatile ("mov lr, x0\n");
// 还原上下文
load()
// return
__asm volatile ("ret\n");
}
#pragma mark public
// 启动Hook 入口
void hookStart() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
rebind_symbols((struct rebinding[6]){
{
"objc_msgSend",
(void *)hook_Objc_msgSend,
(void **)&orig_objc_msgSend
},
}, 1);
});
}
为什么我在 load() 和 save() 方法中没有记录和复原 Q0 - Q7 寄存器情况?因为我们在整个的流程中并没有用到浮点数。所以这些寄存器是不会被修改的,因此我们可以简化这个写法。
从这个点可以引出,其实 Hook 方法和内联汇编是具有耦合的(通用寄存器就那么几个,大家公用),你需要为 Hook 方法中的实现来定制内联汇编的实现,才能够满足整体的需求实现。
我在 pre_objc_msgSend 中增加了打印方法名和类名的测试方法,运行后可以看到效果如下图:

看到这个输出,聪明的你一定已经想到了线程安全的问题了😄。
这是留给你后面的延伸作业,希望你可以继续这个问题的探究和思考。
总结
本文探究了使用 fishhook + 内联汇编实现 Hook objc_msgSend 的全部实现及其内在原理。其中知识包括:
- 什么是 Inline Hook 技术?
- fishhook 的实现原理是什么?
- 为什么 fishhook 可以 Hook
objc_msgSend方法? - 如何使用内联汇编来进行记录上下文和还原上下文操作?
- 如何使用内联汇编通过方法地址调用方法?
- 汇编语句模版的简单使用。
以上问题也用于考察你是否对这篇文章完全掌握,如果没有建议添加收藏再次阅读。
当然这个实现只是一个工具,你可以用它来做很多你需要的事情。
另外,搞懂了这篇文章之后,我推荐你去再去看一看戴铭老师的「iOS 开发高手课 - 01 | App 启动速度怎么做优化与监控」(并无利益,单纯推荐。这一章节可免费试读),我相信很多难点地方都会迎刃而解了。
文章书籍引用与鸣谢
特别感谢好友 @高级页面仔、@Boyang、@Jadyn、@酸菜鱼、@linxi 对于文章的斧正。
- 「在Xcode工程中嵌入汇编代码 · 高级页面仔」
- 「iOS 应用逆向与安全之道 · 罗巍」
- 「跟戴铭学 iOS 编程 - 理顺核心知识点 · 戴铭」
