KangQingYu
Articles59
Tags21
Categories9
二进制重排,从熟悉到精通

二进制重排,从熟悉到精通

二进制重排,通过重新排列可执行文件Mach-O中函数的相对布局,将启动阶段需要调用的函数集中在一起,减少缺页中断次数,从而优化冷启动速度。

一 缺页中断

操作系统的虚拟内存机制,是分页的,避免一次把应用程序的可执行文件一次性全部加载到物理内存。而是将其划分为固定的页,比如iOS每页是16KB。
程序访问的时候,是操作系统分配的虚拟内存地址。CPU的内存管理单元(MMU)负责将这些虚拟内存地址,映射为物理内存地址。
当程序执行到某段代码,发现虚拟内存没有对应物理内存的时候,会触发硬件异常,即缺页中断。
缺页中断,操作系统处理流程:

  • 暂停当前进程执行
  • 根据虚拟地址,在页表中发现该页尚未映射到物理内存(触发缺页中断)
  • 操作系统根据映射关系,从磁盘上的可执行文件(Mach-O)或内存映射文件中读取对应页数据
  • 将该页加载到物理内存(RAM)
  • 更新虚拟地址到物理地址的映射关系(页表)
  • 恢复进程执行

虚拟内存与分页机制,是操作系统的一种优化。宏观层面避免数据全部都加载到物理内存。
缺页中断是这个优化过程中的一个异常触发信号。
虽然每次缺页中断耗时不到1ms,但是当启动方法达到几万个,则有一定的优化空间。

linkMap分析与反馈

想要查看APP所有符号的启动顺序,可以通过linkMap。既能在调研阶段查看不同方法调用内存的分布,又能在优化完成之后做量化分析。

LinkMap是iOS编译过程的中间产物,记录了二进制文件的分布。

结构分析,共三部分
1 Object Files
生成二进制用到的link单元的路径和文件编号。
每一行对应一个.o,包括.m/.cpp,其中.h头文件是预处理阶段,原封不动引入的。
2 Section(段/区段列表)。描述了最终生成的Mach-O文件中,各个虚拟内存段(Segment)和区段(Section)的布局信息。
代码段(__TEXT, __text),数据段(__DATA, __data),字符串常量段(__TEXT, __cstring)

3 Symbol(符号表/具体的排列顺序)
每一个方法、全局变量、字符串存放具体内存地址,占了多大空间,属于哪个.o文件(有与第一部分Object Files中对应文件的编号)。

Mach-O二进制体系,内存布局与传统操作系统的内存布局不同。Mach-O内存布局分两级:Segment(段),Section(区段)。
1 __TEXT(代码段Segment),这是最高级别的一大块,核心权限是可读和可执行(r-x)。

__TEXT.__text(Section),这是__TEXT段下面的一个核心区段,是传统操作系统中的text段,存储的是CPU要执行的机器码0101

2 __DATA(数据段Segment),这也是一大块,核心权限是可读可写(rw-),APP运行过程中各种变量的修改都在这里。
传统操作系统的data段(已初始化的全局变量、静态变量),在Mach-O里叫”__DATA.__data”;
传统操作系统的bss段(未初始化的全局变量、静态变量),在Mach-O里叫”__DATA.__bss”。

核心思路

生成Order File(符号表顺序文件),在编译器配置,告诉链接器在打包的时候,将所有启动阶段会调用的函数,紧密的排列在一起,集中放在二进制文件的最前面(Text段的开头)。

二 找到启动的方法的顺序

几种方法的选择

1 dyld trace

dyld(Dynamic Link Editor)是苹果系统的动态链接器。当用户点击 App 图标,在 main() 函数执行之前,其实是 dyld 在干活(加载动态库、绑定符号、执行 +load 和 C++ 静态构造函数)。

  • 作用: 这里的 dyld trace 指的是通过配置系统环境变量(比如早期的 DYLD_PRINT_STATISTICSDYLD_PRINT_BINDINGS),让系统在控制台打印出 main 函数执行前的耗时细节。

  • 局限: 只能说明加载了哪些库、花了多久,不能用来追踪业务代码里每个函数的调用顺序。

2 Instruments (Time Profiler)

  • 原理: 采样(Sampling)。比如每隔 1 毫秒,去偷看一眼当前 CPU 正在执行什么函数。

  • 局限: 无法用于二进制重排! 因为它会漏抓。如果一个函数执行只花了 0.1 毫秒,且正好在两次采样间隔中间执行完了,Time Profiler 根本不知道它执行过。

3 自定义埋点

  • 原理: 利用 OC 的 Runtime 机制,Hook 掉 objc_msgSend,记录每一次方法调用。
  • 局限: 只能抓到 Objective-C 方法,抓不到底层纯 C 函数(比如压缩库 zipOpenNewFileInZip5)或者 Swift 函数,覆盖面不够。

更成熟的方案Clang插桩

目前业界最成熟、覆盖率最高(能同时覆盖 OC、Swift、C、C++ 以及 Block)的方案是利用 LLVM 编译器的Clang SanitizerCoverage功能来进行插桩。

整体流程分为三步:开启编译插桩 -> 拦截并记录函数执行顺序 -> 生成并配置 Order File。

1 开启编译插桩配置

让编译器在每个函数的入口处自动插入一句回调代码,以便“监听”到函数的调用。

  • 在 Xcode 中,找到 Build Settings -> 搜索 Other C Flags
  • 添加配置:-fsanitize-coverage=func,trace-pc-guard
  • 如果有 Swift 代码,搜索 Other Swift Flags,添加:-sanitize-coverage=func-sanitize=undefined

2 编写插桩拦截代码(获取启动函数顺序)

开启配置后,编译器会在每个函数内部调用一个名为 __sanitizer_cov_trace_pc_guard 的底层 C 函数。在代码中自己实现这个函数来收集被调用的函数内存地址,并反向解析出函数名。

在项目启动最早的地方(比如 main.m 或者新建一个纯 C 文件),写入代码。

3 解析符号并生成 Order File

在 App 完成冷启动的标志性时刻(比如首屏 ViewControllerviewDidAppear 中),触发符号解析逻辑,将收集到的内存地址转换为函数名,并导出到文件中。

4 配置 Order File 到项目中

  1. 找到手机沙盒中生成的 app.order 文件,把它拖到 Xcode 工程根目录下。
  2. 在 Xcode 的 Build Settings 中搜索 Order File
  3. 将刚才拖入的 app.order 文件的相对路径填进去(例如:$(SRCROOT)/app.order)。
  4. 关闭之前第 1 步中开启的 Other C Flags 的插桩配置(删掉 -fsanitize-coverage=func,trace-pc-guard)。因为插桩本身是极其耗时的,仅在“获取启动顺序”的调试阶段使用它。

重新编译打包,此时App 二进制文件就已经重排完毕了。可以通过 Link Map 文件来验证函数顺序是否与 app.order 中的一致。

三 优化属于编译中的哪个阶段?

编译阶段,Clang会将.m源文件编译为目标单元.o文件,代码被翻译成了机器码。
链接阶段,链接器会将.o文件以及依赖的动态库、静态库组装在一起,生成最终的Mach-O可执行文件。
链接器的文件顺序,是按照Xcode 中 Build Phases -> Compile Sources 里的编译顺序。如果想要修改这个顺序,就需要传递一个Order File,使这些函数挨个排在Mach-O文件的__TEXT, __text段的最前面。

四 效果衡量指标

指标1:缺⻚中断个数

打开 Instruments,选择 System Trace,运行之后。分析数据如图,选择“Main Thread”,底部的
File Backed Page In即为缺⻚中断个数。

Count 10312个,Duration 1.32s;

指标2:启动时间

虽然我们优化的是缺⻚中断的个数,但其最终目的还是启动时间。统计时间有几种:

1 打开Xcode的DYLDPRINTSTATISTICS选项。
2 Instrument AppLaunch功能。

手动冷启动与杀进程

因为Mach-O加载到内存之后会有缓存,下次不会触发磁盘IO。为了避免缓存所造成的误差,需要杀进程,但杀进程 = 冷启动?显然并非如此,因为如果只是杀进程,因为内存还没有被其他进程使用,所以也没必要清空所有的缓存,苹果做了一些优化。即 杀进程 != 冷启动。那如何保证尽可能得接近冷启动的效果呢?

  • 杀进程之后,再多打开几个其他耗内存很高的APP。 这样可以迫使iOS内存回收。
  • 重启手机
  • 删除 Xcode 缓存:~/Library/Developer/Xcode/DerivedData

这样虽然可以尽可能接近冷启动,但是每次完全编译,分析缺⻚中断的个数、启动时间。再优化 前后对比2次。总共要完全重新编译4次。如果是比较大的项目,可能一个小时都不够。

五 后记

分享之后,领导提到这个技术方案与业务方向的问题。业务上目前对启动速度关注不大,所以这个优化纯粹是技术驱动。
技术是一种手段,而非目的。还是以业务方向为准,进行技术探究更好。

Author:KangQingYu
Link:http://example.com/2022/02/06/20220206%E4%BA%8C%E8%BF%9B%E5%88%B6%E9%87%8D%E6%8E%92%EF%BC%8C%E4%BB%8E%E7%86%9F%E6%82%89%E5%88%B0%E7%B2%BE%E9%80%9A/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×