前言
在上一篇 启动优化-概念篇 中讲解一些启动优化相关的知识,最后得到减少缺页中断(pageFault)
可以达到启动优化的目的,本文将使用二进制重排与Clang
插桩来实现优化的目的。
文件和方法的编译顺序
-
在
Build Setting
中搜索Link Map
,然后将Write Link Map File
设置为YES
:-
然后编译后在
DerivedData
找到对应的工程(本工程名为WeChatTest
),然后Build
->Intermediates.noindex
->WeChatTest.build
->Debug-iphoneos
->WeChatTest.build
->WeChatTest-LinkMap-normal-arm64.txt
-
然后打开
WeChatTest-LinkMap-normal-arm64.txt
文件,可以发现代码实现的排列顺序:Address
是代码的地址,它加上ASLR
就是物理内存中的地址Size
是内存大小File
是文件编号Name
是方法
-
-
其中代码文件的顺序是根据
编译顺序确定
的,文件内部的代码是按照书写顺序从上到下
的- 可以通过修改文件的编译顺序和文件中代码来验证,先调整文件编译顺序:
- 然后在
AppDelegate
中增加方法
- 编译后查看
.txt
文件
-
根据调整后方法的打印,得以验证:
-
- 文件的顺序是根据编译顺序来确定
-
- 文件中代码的编译顺序是根据书写从上到下编译的顺序确定的
-
二进制重排
-
在
objc4-818.2
源码中能看到一个libobjc.order
文件:- 文件里面方法的都是符号名称,这个是给编译器看的,编译器看到后就按照
order
中符号的顺序去对二进制进行排列。下面进行验证
- 文件里面方法的都是符号名称,这个是给编译器看的,编译器看到后就按照
-
新建一个
WushuangDemo1
工程,然后在AppDelegate
和ViewController
中分别添加方法:// AppDelegate.m void wushuang() { } @implementation AppDelegate + (void)load { } // ViewController.m @implementation ViewController + (void)load { }
-
然后在根目录先创建一个
wushuang.order
文件-
在
wushuang.order
里写一些符号wushuang666
和wushuangSay
方法不存在
-
-
然后在
Build Setting
中搜索Order File
,然后配置wushuang.order
路径: -
最后将
Build Setting
的Write Link Map File
设置为YES
,然后在真机上编译,之后打开生成的.txt
文件:- 原来的
_main
在第三个顺序现在在第一位,顺序已经换了,并且没有的符号也会忽略不计
- 原来的
虽然搞定了二进制重排,但我们的目标拿到启动相关的符号,启动涉及的东西很多,方法嵌套函数,函数里又有网络请求,然后又block
等等,太多了眼睛都看不过来。那么怎么知道启动时调用了哪些方法,这个时候就需要用到Clang
插桩,Clang
会读所有的代码,可以无死角的覆盖所有的函数、block
等
Clang插桩
-
进入 clang官方文档 的
SanitizerCoverage
处可以看到Tracing PC
,PC
是CPU
读取代码的指针,Tracing PC
是跟踪CPU
执行到的代码。SanitizerCoverage
是LLVM
内置的一个简单的代码覆盖率工具,它会在函数
,方法
和block
处插入回调,并提供这些回调的默认实现,并实现简单的覆盖率和可视化。
Hook函数调用
-
在文档的
Tracing PCs with guards
处,在编译器中加入-fsanitize-coverage=trace-pc-guard
配置,系统会默认实现两个函数:__sanitizer_cov_trace_pc_guard(&guard_variable) __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop)
下面来验证下:
- 创建个新工程,在
Build Setting
中的Other C Flags
中添加-fsanitize-coverage=trace-pc-guard
- 然后在
真机
环境下编译,会出现找不到这两个方法符号的错误
- 所以加上
flag
后,系统会调用这两个函数,需要手动去实现。根据文档中给的实现案例,复制到ViewController
中
- 此时编译就成功了
- 创建个新工程,在
-
__sanitizer_cov_trace_pc_guard_init
函数反映了项目中符号的个数- 其中参数
start
和stop
都是代表4字节
的数据,for
循环是遍历start
到stop
的位置然后讲初始化的4字节
变量自增然后放到遍历的相应位置 - 运行后打印得到
start
和stop
的地址,使用x
命令读取start
内存可以得到存储的数据
- 那么最后一个数据就是
stop
内存减去4
后得到第一个4字节
数据
- 增加方法
touchesBegan:
方法,然后再运行查看最后一个数据,发现增加了1
- 再添加一个
wushuang
函数,查看是否会再增加1
- 结果最后一个数据依然会
+1
,说明函数也能监控到,再定义一个block
,然后再运行
- 还是会自增
- 其中参数
结论:
__sanitizer_cov_trace_pc_guard_init
能够监控项目中所有方法,函数以及block
,也就是反映了项目中符号的个数
-
__sanitizer_cov_trace_pc_guard
方法是拦截项目中的方法,相当于Hook
- 在
touchesBegan:
中调用wushaung
函数,然后函数中调用block
- 然后运行起来后,在
__sanitizer_cov_trace_pc_guard
函数处打断点,再点击屏幕触发touchesBegan:
- 观察堆栈可知
touchesBegan:
会调用__sanitizer_cov_trace_pc_guard
函数,过掉断点发现每走一个方法都会触发这个函数。 - 然后在这个方法处打上断点重新启动项目,就监控到了启动方法的执行顺序
- 在
结论:
__sanitizer_cov_trace_pc_guard
是由项目内部方法,函数,block
函数调用,也就是Hook
了项目中一切的函数调用
原理
在项目运行起来后,然后在touchesBegan:
函数处打断点,点击屏幕查看汇编
- 通过观察发现在
touchesBegan:
的头部实现栈平衡相关
代码后会插入__sanitizer_cov_trace_pc_guard
函数,后面才是touchesBegan:
的实现,然后在wushuang
函数和block
处都打断点发现都是一样的 - 所以,项目中只要添加了
Clang
插桩的标记,编译器就会在所有方法、函数、block
代码实现的 边缘 添加一句__sanitizer_cov_trace_pc_guard
代码 - 编译器在所有代码
(自己写的代码)
中添加这个函数,也就是修改了二进制文件,只有编译器能干这个事情 - 那么可以在上线前
代码审查
时使用,上线时,将Other C Flag
中的标记清除即可
获取启动符号
接下来需要获取启动符号,首先我们知道现在每个方法都会调用__sanitizer_cov_trace_pc_guard
函数,用__builtin_return_address(0)
函数就可以获取__sanitizer_cov_trace_pc_guard
的上一个调用者地址。具体方法如下
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0); // 获取上一个调用函数的地址
Dl_info info; // 符号信息结构体
dladdr(PC, &info); // 将从PC中获取的符号信息存入结构体中
printf("Dl_info : \nfname: %s\nfbase: %p\nsname: %s\nsaddr: %p\n\n", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
}
-
Dl_info
是一个结构体,用来存储符号信息:dli_fname
: 当前项目路径dli_fbase
: 是MachO
文件的起始位置dli_sname
: 符号名字dli_saddr
: 函数地址
-
dladdr
作用是将获取函数的信息存入Dl_info
结构体中 -
验证结果如下:
-
于是可以用
dli_sname
直接输出符号名字,就得到了所有方法的调用顺序
坑点分析及解决方案
上面的方式虽然获取到启动时调用的方法,但是有个坑点,就是会在多线程中访问数据 代码如下:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
NSLog(@"%@", [NSThread currentThread]);
printf("%s\n", info.dli_sname);
}
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:@selector(testWushuang) withObject:nil];
}
- (void)testWushuang {
sleep(3);
}
-
很明显
testWushuang
方法的调用是在子线程,打印__sanitizer_cov_trace_pc_guard
的线程得到如下结果:- 在子线程调用的方法,
__sanitizer_cov_trace_pc_guard
也在子线程,这时就产生了多线程访问,会出现数据错乱的问题。这个时候就需要使用OSAtomicEnqueue()
和OSAtomicDequeue()
来将启动的方法进行安全存取。 - 先导入头文件
#import <libkern/OSAtomic.h>
,再将Other C Flag
标识符改成:-fsanitize-coverage=func,trace-pc-guard
,里面添加了func
是仅限方法使用,因为方法里的其他函数也会调用__sanitizer_cov_trace_pc_guard
,为了排除其他的调用就需要添加这个flag
。其他核心代码如下:
// 定义一个原子队列 static OSQueueHead list = OS_ATOMIC_QUEUE_INIT; // 链表结构体 typedef struct { void *pc; //存获取到的PC void *next; //指向下一个节点 } Node; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { void *PC = __builtin_return_address(0); Node *node = malloc(sizeof(Node)); // 给node节点分配内存 *node = (Node){PC, NULL}; // 将PC赋值给node的pc OSAtomicEnqueue(&list, node, offsetof(Node, next)); // 在队列中将新的node节点存入上个节点的next位置 } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { while (YES) { Node *node = OSAtomicDequeue(&list, offsetof(Node, next)); // 在队列上个节点的next位置获取node if (node == NULL) { break; } Dl_info info; dladdr(node->pc, &info); //将pc信息存入info printf("sname: %s\n", info.dli_sname); } }
- 此时就可以安全的取到启动符号了
- 在子线程调用的方法,
生成Order文件并配置
将打印出来的启动方法放到wushuang.order
文件,然后取出来进行二进制重排
,代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray<NSString *> *nameArray = [NSMutableArray array];
while (YES) {
Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
printf("sname: %s\n", info.dli_sname);
NSString *funcName = @(info.dli_sname); // 转成OC字符串
BOOL isOCFunc = NO;
if ([funcName hasPrefix:@"-["] || [funcName hasPrefix:@"+["]) {
isOCFunc = YES;
}
// 生成方法名字
funcName = isOCFunc ? funcName : [@"_" stringByAppendingString:funcName];
// 去重
if (![nameArray containsObject:funcName]) {
[nameArray insertObject:funcName atIndex:0];
}
}
// 去掉自己
[nameArray removeObject:[NSString stringWithFormat:@"%s", __func__]];
// 写入文件
NSString *funcString = [nameArray componentsJoinedByString:@"\n"];
NSLog(@"\nfuncString: \n%@", funcString);
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"wushuang.order"];
NSData *fileData = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL isSuccess = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
if (isSuccess) {
NSLog(@"写入成功 🎉");
} else {
NSLog(@"写入失败 😭");
}
}
-
运行后得到的方法顺序为
-
在
Window
->Devices and Simulators
选中自己的手机,然后选择当前的Demo
,再Download Container
下载到桌面- 在右键显示包内容,在
temp
里找到wushuang.order
文件,将文件放到工程根目录,然后在Build Setting
的Order File
配置好路径,然后将Write Link Map File
设置为YES
,最后删除Other C Flag
内容,运行后找到txt
文件并打开
此时得到的顺序和打印的一致!
- 在右键显示包内容,在
Swift符号获取
获取Swift
的符号需要在Other Swift Flag
处配置-sanitize-coverage=func
,与-sanitize=undefined
,核心代码如下:
class Wushuang: NSObject {
@objc class func say() {
print(" wushuang say NB ");
}
}
// ViewController.h
+ (void)load {
[Wushuang say];
block();
}
于是就得到了swift
符号