我们在前面讲了二进制重排启动优化的原理,在没有重排之前每一个数据页中的代码有可能是启动时刻的代码,也有可能不是,这样就造成了浪费。二进制重排之后,就会把所有启动时刻的代码都排到最前面,这样就减少了缺页中断的次数,也就节约了启动时间。
配置 Clang 插桩
- 步骤 1
首先我们打开 clang 文档,这里 Traching PC 就是追踪当前 cpu 执行的指令。
- 步骤 2
接着往下查看文档告诉我们在项目中配置 -fsanitize-coverage=trace-pc-guard,配置完成之后我们编译项目会发现报错,就是告诉我们找不到 __sanitizer_cov_trace_pc_guard_init 与 __sanitizer_cov_trace_pc_guard 这两个函数的实现。所以我们下面实现这两个函数。
- 步骤 3
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_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.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}
@end
在项目中实现这两个函数之后再编译就可以了。
__sanitizer_cov_trace_pc_guard_init 函数调试
这里 star 跟 stop 是无符号整型,代表开始位置与结束位置,x 结束位置 - 4 就代表符号个数。第一次输出的符号个数是 3。第二次我们增加了 oc 方法, c函数, block 之后再输出个数是 6。这里就说明 __sanitizer_cov_trace_pc_guard_init 可以收集到符号的数量并回调。
__sanitizer_cov_trace_pc_guard_init 函数调试
通过对 __sanitizer_cov_trace_pc_guard_init 函数进行断点可以看到,当我们执行 oc 方法, c函数, block 之后紧接着都会执行 __sanitizer_cov_trace_pc_guard_init,说明 __sanitizer_cov_trace_pc_guard_init 函数能拦截一切方法, 函数, block 的执行。这里需要注意的是这里拦截的都是当前项目中的符号,系统的库及三方库中不在当前项目二进制文件中的符号不会被拦截。
这里我们知道了 __sanitizer_cov_trace_pc_guard_init 函数能拦截所以的符号,那么它拦截的原理又是什么呢?我们接着往下看。
Clang 插桩的原理
通过汇编我们可以看到,在执行方法, 函数, block 的时候都能看到 bl 0x100732684 这句汇编代码。也就是说只要添加了 clang 插桩的标记,编译器就会在所有的方法, 函数, block 的边缘添加一句 __sanitizer_cov_trace_pc_guard 代码。也就相当于修改了二进制文件。而这个事情只有编译器能做,编译器在将高级语言翻译成汇编代码的时候做了这些事情。至于在前端做的还是在后端做的这个官方没有给出说明,我们只能去分析,在前端 IR 阶段的时候或者后端都可以做这件事情。但是查桩肯定是损耗性能的,所以我们一般都是在做代码审查的时候用。
获取到符号名称
//HOOK一切的回调函数!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"fname:%s\n fbase:%p\n sname:%s\n saddr:%p\n",
info.dli_fname,
info.dli_fbase,
info.dli_sname,
info.dli_saddr);
}
typedef struct dl_info {
const char *dli_fname; /* 文件名称,也就是 mach-o 文件 */
void *dli_fbase; /* 文件地址 */
const char *dli_sname; /* 函数名称 */
void *dli_saddr; /* 函数地址 */
} Dl_info;
这里 __builtin_return_address 函数的意思就是获取到当前函数的内部返回地址,也就是上一个函数的地址,当前函数的调用者。所以这里 *PC 就是上一个函数的第 0 句代码的地址。这里我们就有机会拿到上一个函数的符号名称。这里我们导入 #import <dlfcn.h> 头文件,dladdr(node->pc, &info) 这一句相当于把 PC 指向的地址数据信息赋值给 info 这个结构体。最后通过打印也输出了对应的信息。这里就能拿到所有符号的名称。
通过原子队列保存符号生成 order 文件
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
void * pc;
void * next;
} SYNode;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
test();
}
void(^block)(void) = ^(void){
NSLog(@"block函数执行!");
};
void test(){
NSLog(@"test函数执行!");
block();
}
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_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.
}
//HOOK一切的回调函数!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
//创建结构体
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//把结构体 node 写入到 symbolList,offsetof(SYNode, next) 表示设置 node 的 next,下一个节点,就是在上一个 next 的基础上加上 SYNode 大小
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
//生成order文件!!
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//定义数组
NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
while (YES) {//循环体内!进行了拦截!!这里需要注意的是 while 也会被拦截,要做工程中做配置只拦截方法
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);//转字符串
//给函数名称添加 _,这里要判断是否是函数,是函数的话要添加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
//反向遍历数组,因为入栈函数调用顺序是反的,所以这里要取反
NSEnumerator * em = [symbleNames reverseObjectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString * name;
// 这里要对数组进行去重,因为有的方法会多次调用
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {//数组没有name
[funcs addObject:name];
}
}
//去掉自己!也就是当前方法
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
//写入文件
//1.编成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"chenxi.order"];
NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
//写入到沙盒路径
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
// 打印数组字符串
NSLog(@"%@",funcStr);
}
上面我们已经通过拦截获取到所以符号的名称,那么我们可以通过代码,讲拦截到的符号进行保存并生成 order,代码如上。但是这里要注意几个问题。
-
多线程的问题,如果是子线程中的方法,那么
__sanitizer_cov_trace_pc_guard函数也会在子线程中执行,所以这里写入的时候要注意多线程影响,这里通过原子队列保存。 -
while循环的影响,因为__sanitizer_cov_trace_pc_guard函数也会拦截到while循环,这样写入的时候就会造成递归调用,所以配置other c flags参数的时候要多加一个条件-fsanitize-coverage=func,trace-pc-guard,只拦截方法。 -
生成
order文件的时候要对符号方法进行取反,因为写入的时候是倒序的,以及对符号名称去重。 -
函数名称前面要添加
_。
下载沙盒文件并打开 order 文件可以看到确实把 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 只前的方法都收集到了,这时候我们就可以把 order 配置到工程。这时候就完成了二进制重排。
Swift 符号覆盖
当我们添加了要给
swift 的类,并调用了 swift 类的方法,发现并没有收集到 swift 的方法,这是因为 swift 需要做单独的配置。
需要在
other swift flags 这里添加 -sanitize-coverage=func 跟 -sanitize-undefined 这两个参数。
这时候再打印可以看到
swift 方法被收集到了,而且编译器对 swift 符号名称做了混淆,这也可以看出 swift 相对于 OC 会更安全。