二进制重排优化
在早期的操作系统中,任何进程被加载到内存中运行时,都是按照顺序完全加载的。内存中的地址就是真实的物理地址,并且进程所需要的全部指令和数据都是一次性加载到内存中。这样就会有出现两个明显的问题:
- 数据安全问题: 由于各个进程是按照顺序依次加载到内存中的,并且内存中的地址就是真实的物理地址。这样在一个进程中就可以内存地址偏移区访问到任意的内存空间,包括一些非当前进程的数据信息,这就导致进程本身的数据并不安全,给黑客造成了侵入的机会;
- 内存浪费:由于每个进程都是完整加载的内存中的,所以随着进程的增多,内存的消耗会越来越严重,而事实上用户在同一时间操作用到一个进程,并且不太会一次性用到进进程中的所有功能,这就导致了严重的效率问题。
虚拟内存
为了解决上述问题,操作系统引入虚拟内存的概念:针对于每一个进程系统通过一张映射表,将内存中的地址和真实的物理空间做了一个映射,使得每一个进程都认为自己分配到了一块完整且连续的内存空间。而事实上,这个内存地址只是一个虚拟的地址,需要通过映射表才能获取到真实的物理地址。
非法内存访问
这样,内存访问的地址必须经过映射表映射之后才能获取到真实的物理地址,而经过映射表映射的地址都在进程对应的内存既定范围之内,这样就解决了非法内存地址访问的问题。所以引入虚拟内存之后的cpu寻址过程就变成了这样:
- 通过虚拟内存地址找到对应的映射表;
- 通过映射表将虚拟内存地址映射为真实的物理地址,获取到物理地址中的数据.
内存浪费
使用虚拟内存解决了通过指针偏移造成的非法数据访问问题,那么如何解除由于完整加载进程数据导致的内存和效率问题呢?
事实上,虚拟内存和物理内存之间的映射关系并不是一一对应的,而是以内存分页的方进行对应的。也就是说,映射表只会将虚拟内存地址映射到具体的物理内存分页上,而不是映射到具体的地址上。在 linux 系统中 , 一页内存大小为 4KB , 在不同平台可能各有不同 .
- Mac OS 系统中 , 一页为 4KB ,
- iOS 系统中 , 一页为 16KB .
这样在映射表中的分页就可以选择在需要的时候进行加载,而不是在加载进程时全部一次性加载。已经加载的分页会在被标记为1,而未被加载的分页被标记为0。当虚拟内存访问到了被标记为0的分页时,系统就会阻断进程,进行分页加载,这种类似于懒加载机制,被称之为缺页中断(page fault).这样就可以需要的数据都在内存中,同时又不会造成内存浪费,提高了访问效率.
虽然虚拟内存很好地解决了非法数据访问和内存浪费问题,但是又出现了一个新的问题,那就是进程每次运行加载运行时,固定的方法会出现在固定的位置,使用地址偏移依然可以访问到指定的内存空间,从而为非法的hook操作对原始实现进行入侵提供了可能。
ASLR
为了解决进程每次运行固定方法都会出在的固定的虚拟内存上的问题,Apple在iOS4.3之后引入了**ASLR(Address Space layout Radomization)**技术.其实现原理就是每次在虚拟地址映射到真实物理地址之前,随机增加一个偏移值。虽然只是增加了一个看似不起眼的偏移值,却将黑客的门槛拉高了好几个等级。
由于ASLR的存在,也使得尝试提前使用偏移值对原始实现进行hook的黑操作被有效遏制。
二进制重排
二进制重排原理
尝试这样一种场景,在应用启动过程中,调用到的方法处在不同的内存分页上,那么在启动过程中就会不停地触发缺页中断,导致进程阻塞,从而引起启动时间变长。而如果可以尝试将启动过程所需要的方法尽可能集中在较少的分页上,通过减少缺页中断的触发次数,就可以将启动时间缩短。这就二进制重排优化启动的基本原理。
如何判断缺页中断耗时
既然需要进行优化,那就需要有个衡量的标准,来进行优化前后的对比来查看优化是否达到预期效果。这里主要有两种方式。
使用Instruments工具
在Xcode中可以使用Instruments工具中的System Trace工具来查看在应用启动阶段中缺页中断的触发次数.为了能够更加真实的反应数据 , 最好是将应用杀掉重新安装 , 因为冷热启动的界定其实由于进程的原因并不一定后台杀掉应用重新打开就是冷启动。
- 打开 Instruments , 选择 System Trace .
- 选择真机 , 选择工程 , 点击启动 , 当首个页面加载出来点击停止 .
- 等待分析完成 , 查看缺页次数
设置Xcode调试参数
通过Xcode启动参数设置可以查看到启动过程的耗时,从侧面做一个验证。打开项目在Xcode中,通过Edit Scheme->Arguments(或者快捷键组合cmd+shift+,)设置打印加载数据分析参数来查看启动参数
这样就可以在启动之后查看到启动加载相关操作的耗时:
如何查看自己工程的符号顺序
不得不说,Xcode是开发神器,你需要的功能它几乎都有。Link Map 是编译期间产生的产物 , ( ld 的读取二进制文件顺序默认是按照 Compile Sources - GUI 里的顺序 ) , 它记录了二进制文件的布局,这时候就可以通过设置Write Link Map File来查看。
然后运行项目,就会在Products的同级目录中找到关于项目的一个.txt文件,这里就保存了在编译期间的二进制分布信息( 这个符号顺序明显是按照 Compile Sources 的文件顺序来排列的 .)
在这个.txt文件中可以看到符号的加载顺序:
如何改变二进制符号的加载顺序
在Xcode中可以通过设置Order File来认为干预编译期间的符号加载顺序。随便定义一个symbols.order文件:
在Xcode编译配置中设置symbols.order的路径:
清理项目编译,重新编译,然后查看编译生成的.txt文件,就会发现设置的order文件确实改变了文件的编译顺序.
获取启动期间加载的所有符号
基于以上的实践基础,可以使用clang静态插桩来获取启动期间调用到的函数符号.
编写order文件
获取到启动期间的所有符号之后对符号进行调整顺序去重之后,写入.order文件用以改变编译期间的二进制布局,达到减少触发缺页中断,缩短启动时间的目的.