二进制重排
前言
需求越来越多,app应用也越来越大,功能越多,导致性能 和体验问题也越来越多,
其他还好说,启动速度最能 直观影响体验,一般我们优化都是减少不必要代码,懒加载
,多线程,删除无用图片代码,压缩文件体积去处理
关于启动
app启动时,会加载二进制,动态库初始化,对象初始化,执行 load 函数执行 c++ 构造函数,最后进入 main 函数,然后执行 App 初始化逻辑,在这个过程中,会执行代码,在寄存器中不停跳转,完成函数调用和上下文切换
我们说,动态库开始初始化App时,会把应用二进制文件映射进内存,当使用到具体内存时,去触发物理内存加载,然后访问,在这个过程中,就会在寄存器中寻址,取指令,译码,比较耗时的就是去指令,取指令会涉及page fault
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。虚拟地址空间的内部又被分为内核空间和用户空间两部分。并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。页表实际上存储在 CPU 的内存管理单元 MMU 中。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行,这是一个次缺页异常(minor page fault)。minor page fault 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。
major page fault指需要访问的内存不在虚拟地址空间,也不在物理内存中,进入内核空间分配物理内存,更新进程页表,还需要swap从磁盘中读取数据换入物理内存中。
![![截屏2020-09-14 上午10.52.32.png](https://upload-images.jianshu.io/upload_images/2318672-1687ccd5fc4fb593.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
](https://upload-images.jianshu.io/upload_images/2318672-a64090a1a4109547.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
什么是page fault
当进程访问它的虚拟地址空间中的PAGE时,如果这个PAGE目前还不在物理内存中,此时CPU是不能干活的,Linux会产生一个hard page fault中断。系统需要从慢速设备(如磁盘)将对应的数据PAGE读入物理内存,并建立物理内存地址与虚拟地址空间PAGE的映射关系。然后进程才能访问这部分虚拟地址空间的内存。
page fault 又分为几种,major page fault、 minor page fault、 invalid(segment fault)。
major page fault 也称为 hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap 回到物理内存也是 hard page fault。
minor page fault 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。
当一个进程在调用 malloc 获取虚拟空间地址后,首次访问该地址会发生一次soft page fault。
通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault
invalid fault 也称为 segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报 segment fault错误
page fault, (严格说, 这里指的是major page fault)名字听起来挺严重, 实际上, 并不是什么"错误".
大致是这样, 一个程序可能占几Mb, 但并不是所有的指令都要同时运行, 有些是在初始化时运行, 有些是在特定条件下才会去运行. 因此linux并不会把所有的指令都从磁盘加载到page内存. 那么当cpu在执行指令时, 如果发现下一条要执行的指令不在实际的物理内存page中时, CPU 就会 raise a page fault, 通知MMU把下面要执行的指令从磁盘加载到物理内存page中
二进制重排原理
实现
首先我们打开项目command + i,打开Instruments调试工具,选择System Trace
二进制重排
- page fault 过多 会导致二进制不停的执行指令,当执行的代码文件偏移过于随机,会导致寄存器不同切换,触发内存加载,导致指令执行耗时,增大崩溃风险
- 如果说待执行的关键指令和代码都紧凑的排列在相邻物理页,就能尽可能减少page fault 次数,崩溃概率也会极大降低
我们可以在XCode配置二进制重排,首先我们要确定符号的顺序,才能知道怎么重排,XCode使用的链接器叫做ld,ld有个参数叫order_file,我们可以将文件的路径告诉XCode,在order_file文件中把符号的顺序写进去,XCode编译的时候就会按照文件中的符号顺序打包成二进制可执行文件。
项目,在build setting 里面搜索order file,发现这里面指定了order的文件路径,因为一旦在这里指定了order file的路径,XCode就会在编译的时候按照文件里面写进去的顺序
编译,如何查看整个项目的符号顺序呢,我们到Build Settings搜索Link Map,Link Map就是我们链接的符号表,我们把它改成YES
command + R我们运行下,然后在Products里面的.app文件,在我们Intermediates.noindex-->项目名.build--->Debug-iphoneos-->项目名.build--->项目名-LinkMap-normal-x86_64.txt,这个文件里面就有链接的符号顺序表
我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 , 一定要清楚这一点
怎么做
针对应用中的 objc,c,c++ 代码和符号我们要怎么知道他们的执行顺序并监控呢?即只要我们能通过某种手段 trace 到所有启动阶段执行的函数符号,然后把这些函数符号按顺序排列好,组成 order file 交给编译器即可
网上的实现方案
- 通过静态扫描和运行时 Trace 等方法确定 order_file (抖音)
参考抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%
流程如下:
1.设置条件触发流程
2.工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物
3.运行一次App到启动结束,Trace动态库会在沙盒生成Trace log
4.以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file
- 基于llvm插桩的方案:(facebook 工具 AppOrderFiles )
简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_ 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。
开启 SanitizerCoverage 的方法是:在 build settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func 和 -sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用。
腾讯大神写了个工具 AppOrderFiles。CocoaPods 接入,程序启动完成函数一行调用,生成 Order File。全在 GitHub 里了:github.com/yulingtianx…
- 手机淘宝团队静态库插桩方案
通过在汇编层面对 pod 编译后的静态库进行插桩。在启动时,插桩后的方法都会调用记录方法,从而获得启动方法的执行顺序。我们编译过的静态库由 .o 文件组成,我们可以对 .o 中的函数代码进行修改,在每个函数的开头插入调用我们指定记录函数的指令。
static OSQueueHead qHead = OS_ATOMIC_QUEUE_INIT;
static BOOL stopCollecting = NO;
typedef struct {
void *pointer;
void *next;
} PointerNode;
// start和stop地址之间的区别保存工程所有符号的个数
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
printf("totasl count %i\n", N);
}
// 每个函数调用时都会先跳转执行该函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// +load方法先于guard_init调用,此时guard为0
// if(!*guard) { return }
if (stopCollecting) {
return;
}
// __builtin_return_address 获取当前调用栈信息,取第一帧地址
void *PC = __builtin_return_address(0);
PointerNode *node = malloc(sizeof(PointerNode));
*node = (PointerNode){PC, NULL};
// 使用原子队列要存储帧地址
OSAtomicEnqueue(&qHead, node, offsetof(PointerNode, next));
}
extern NSArray <NSString *> *getAllFunctions(NSString *currentFuncName) {
NSMutableSet<NSString *> *unqSet = [NSMutableSet setWithObject:currentFuncName];
NSMutableArray <NSString *> *functions = [NSMutableArray array];
while (YES) {
PointerNode *front = OSAtomicDequeue(&qHead, offsetof(PointerNode, next));
if(front == NULL) {
break;
}
Dl_info info = {0};
// dladdr获取地址符号信息
dladdr(front->pointer, &info);
NSString *name = @(info.dli_sname);
// 去除重复调用
if([unqSet containsObject:name]) {
continue;
}
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
// order文件格式要求C函数和block前需要添加_
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[unqSet addObject:name];
[functions addObject:symbolName];
}
return [[functions reverseObjectEnumerator] allObjects];;
}
#pragma mark - public
extern NSArray <NSString *> *getAppCalls(void) {
stopCollecting = YES;
__sync_synchronize();
NSString* curFuncationName = [NSString stringWithUTF8String:__FUNCTION__];
return getAllFunctions(curFuncationName);
}
extern void appOrderFile(void(^completion)(NSString* orderFilePath)) {
stopCollecting = YES;
__sync_synchronize();
NSString* curFuncationName = [NSString stringWithUTF8String:__FUNCTION__];
// 异步存储到文件中
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *functions = getAllFunctions(curFuncationName);
NSString *orderFileContent = [functions.reverseObjectEnumerator.allObjects componentsJoinedByString:@"\n"];
NSLog(@"[orderFile]: %@",orderFileContent);
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"orderFile.order"];
[orderFileContent writeToFile:filePath
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
if(completion){
completion(filePath);
}
});
}