二进制重排笔记

在文章开始之前,先搞明白一个问题,在iOS为什么二进制重排能优化启动时间?

一句话来说,就是减少 Page Fault 带来的性能损失

那么什么是PageFault,我们先从虚拟内存讲起。

为什么会引入虚拟内存

  • 地址空间不隔离,所有程序都可以直接访问物理地址,恶意程序就可以随便修改内存的的值。
  • 内存使用效率低
    • 当A和B读进内存中,忽然要执行c,这时候内存不够,而C差不多要占据整个内存,就要把A和B存回磁盘中。因为这样子大量的数据换出换入,效率低下。
  • 程序运行的地址不确定
    因为在程序编写中,指令和数据的地址是固定的,这样子就涉及了程序的重定位问题。

正是因为上面的3大问题,所以在计算机中引入了虚拟地址的概念。

分段

一开始的方案是使用分段,即把每个程序的对应的虚拟内存地址映身到物理内存中
image
像这种,虽然能解决地址隔离和重定位的问题,但是效率还是不高,因为每次都要把整块地址交换到磁盘中。

分页

分页的基本方法是地址空间人为地等分成固定大小的页,每一页的大小由硬件决定,由操作系统选择决定页的大小。
image
如上图所谓,内存映射表会把每一页虚拟内存映射到 物理内存 中。
如果进程1要访问 Virtual Page 3,这时 page3 不在内存中,就会发生缺页中断,就是 页错误(Page Fault,缺页异常)。然后操作系统接管进程,负现把 Page 3从磁盘中读出来装到内存中。然后内存映射表建立映射关系。

虚拟内存的实现要依靠硬件的实现,一般来说CPU内置着一个叫 MMU (Memory Management Unit)的部件进行页映射。

image

怎么做

  1. 生成Order文件
  2. XcodeBuildSetting里配置Order文件
  3. 生成LinkMap文件,查看是否重排成功
  4. 使用System Trace查看page fault的次数
  5. 最终要看启动时间是否减少

其中的难点就在于怎么生成 Oder 文件,把 App 启动时的调用到的符号尽可能放在一个page里

生成Order的三种方法

clang 静态插桩

静态插桩就是利用clang提供的回调方法,在回调方法里面获取符号。

官方文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

大佬提供的库: https://github.com/yulingtianxia/AppOrderFiles

使用fishhook objc_msgSend 方法

Hook objc_msgSend方法要保证栈平衡。
参考戴老师的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 寄的value放到x12寄存器中,通过寄存器寻址跳转到地址
#define call(b, 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 (#b " x12\n");

// 把x0~x9的寄存器保存起来,
// 保存x9据说为了内存对齐
// 如果这里用到浮点数,还要把q0~q9保存起来
#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");

// 从栈里面把数据读取出来,恢复寄存器
// 调整sp指针
#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" );

__attribute__((__naked__))
static void hook_Objc_msgSend() {
// Save parameters.
/// 保存寄存器的参数到栈里面
/// Step 1
save()

/// Step 2
// 保存lr寄存器到第三个寄存器中,_before_objc_msgSend的方法,第三个参数就是lr
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");

/// 此时的x0、x1,就是objc_msgSend方法里面的id,sel
call(blr, &before_objc_msgSend)

/// 从栈里面取出参数到放到寄存器中
load()

/// 调用原来的msg_send方法
call(blr, orig_objc_msgSend)

/// 保存原来
save()

/// 调用after_objc_msgSend方法,这个方法要把lr寄存器的地址返回。
call(blr, &after_objc_msgSend)

/// 上面调用after_objc_msgSend返回的lr寄存器地址会放到x0里面,所以我们把x0恢复到原来的lr寄存器即可
__asm volatile ("mov lr, x0\n");

/// 恢复上下文
load()

// 调用返回方法
ret()
}

修改静态库的符号表

这个方案来自:静态拦截iOS对象方法调用的简易实现

原理:修改静态库的符号名,在静态链接时就能链接到修改后的方法

定义Person.m 文件

1
2
3
4
5
6
7
@implementation Person

- (void)sayHelloToWorld {
NSLog(@"1+2=3");
}

@end

clang把.m文件生成.o目标文件,

clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk -c Person.m -o Person.o

现在的文件还未链接,跳转NSLog符号的地址还没定,一旦链接到bl跳转的地址就是目标地址

image
image
image
image

这样子就能找到符号了,如果我们把0x0928位置的N字符改成O。就会变成下面OSLog这个方法
image
image

我们在主工程定义了OSLog方法,在静态链接就会链接到这个方法。