二进制重排 & clang插桩
注意:在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多。
下面,我们来进行具体的实践,首先理解几个名词
Link Map
Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分:
-
Object Files 生成二进制用到的link单元的路径和文件编号
-
Sections 记录Mach-O每个Segment/section的地址范围
-
Symbols 按顺序记录每个符号的地址范围
ld
ld是Xcode使用的链接器,有一个参数order_file,我们可以通过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化
所以二进制重排的本质就是对启动加载的符号进行重新排列
。
到目前为止,原理我们基本弄清楚了,如果项目比较小,完全可以自定义一个order文件,将方法的顺序手动添加,但是如果项目较大,涉及的方法特别多,此时我们如何获取启动运行的函数呢?有以下几种思路
-
1、hook objc_msgSend:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend,但是由于objc_msgSend的参数是可变的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法
-
2、静态扫描:扫描 Mach-O 特定段和节里面所存储的符号以及函数数据
-
3、Clang插桩:即批量hook,可以实现100%符号覆盖,即完全获取swift、OC、C、block函数
-
tracing PCs 跟踪CPU执行到的代码
-
设置 Build Setting -> 搜索 Other c flag -> 找到Other C Flag 添加 -fsanitize-coverage=trace-pc-guard
Tracing PCs with guards
With-fsanitize-coverage=trace-pc-guard
the compiler will insert the following code on every edge:
- 报错 找不到以下两个函数
// 使用两个回调函数
// trace-pc-guard-cb.cc
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
// 1.这个函数可以获取项目中符号的个数(函数个数)
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.
}
// 2.这个函数可以拦截到当前项目(machO中)所有正在执行的函数,属性
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
// 返回地址return_address的意思是:调用了__sanitizer_cov_trace_pc_guard的函数,谁调用了__sanitizer_cov_trace_pc_guard,谁的地址就是return_address
void *PC = __builtin_return_address(0);
// Dl_info info;
// dladdr(PC,&info);
// 创建结构体
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL}
// 结构体入栈
OSAtomicEnqueue(&symbolList,node,offsetof(SYNode,next));
printf("%s\n %s\n %s\n %s\n",
info.dli_frame,
info.dli_fbase
info.dli_sname
info.dli_saddr);
// info.bli_sname 是符号名称
// char PcDescr[1024];
// This function is a part of the sanitizer run-time.
// To use it, link with AddressSanitizer or other sanitizer.
// __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
// 定义一个原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定义一个符号的结构体
typedef struct {
void *pc;
void *next;
}SYNode;
// 取出symbolList中的数据
-(void)roc_getSymbolList{
while(YES){
SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc,&info);
printf("%s \n",info.dli_sname);
}
}
// 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
// 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
// 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`
start & stop debug
-
start数据
- 断点调式打印内存 lldb
x
0x1009914b8
- 断点调式打印内存 lldb
-
stop 数据
- 断点调式打印内存 lldb
x
0x1009914f0
- 断点调式打印内存 lldb
-
查看stop
最
后一个数据- 断点调式打印内存 lldb
x
(0x1009914f0-4)
- 断点调式打印内存 lldb
结果 0e 00 00 00 (14个符号,即`是dyld之后调用的函数个数`)
Hook all 的原理
-
__sanitizer_cov_trace_pc_guard (可拦截all函数)
原理分析:
编译之前
只要添加了clang插桩的标记:-fsanitize-coverage=trace-pc-guard
编译之后
编译器就会在实现函数的edge插入一个函数,这个函数就是__sanitizer_cov_trace_pc_guard
so 起到拦截所有函数
的效果,在执行函数内部内容之前
。
相当于在ast(语法树)之后,在IR中每一个函数的函数中的内容之前插入__sanitizer_cov_trace_pc_guard
这也是苹果官方对app上线之前做代码审查
的方案,所以在发布产品时记得去掉 -fsanitize-coverage=trace-pc-guard 配置
提示:如果需要制作
乌龟壳这是至关重要的一部分
拦截获取函数符号
- 打印出all已拦截的符号
哎妈妈呀,还需要
照顾多线程
和循环体
,巨大的坑
1.在 多线程 会影响执行的顺序,需要捋正
2.在 while(YES){} 循环体内部也进行了拦截,会导致死循环,
这个坑好大吗
需要将函数符号按照顺序添加到other文件中
- 解决
多线程
这个坑
通过原子队列保存符号(看上面代码部分的操作 OSAtomicEnqueue & OSAtomicDequeue)
arm64汇编 `bl` == x86_64汇编 `call`
- 解决
循环体
这个坑
在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard` 添加了`func,`
-
func,
仅限方法的调用实现插桩,其它不管
- 查看打印 完美解决
取反 & 去重
-(void)roc_getSymbolList{
NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];
while(YES){
SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc,&info);
printf("%s \n",info.dli_sname);
// const chanr* 转 oc 字符串
NSString *symbolName = @(info.dli_sname);
// 如果是OC的方法直接存储
if ([symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["]){
[symbolNames addObject:symbolName];
continue;
}
[symbolNames addObject:[@"_" stringByAppendingString:symbolName]];
}
// 反向 & 去重
// symbolNames = (NSMutableArray<NSSting*> *)[[symbolNames reverseObjectEnumerator] allObjects];
NSEnumerator *em = [symbolNames reverseObjectEnumerator];
// symbolNames.count 节约内存开销
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *s_name;
while(s_name = [em nextObject]){
if (![funcs containsObject:s_name]){
[funcs addObject:s_name];
}
}
NSLog(@"%@",funcs);
NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
}
// 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
// 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
// 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`
// 秀代码
-(void)roc_getSymbolList{
NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];
while(YES){
SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc,&info);
printf("%s \n",info.dli_sname);
// const chanr* 转 oc 字符串
NSString *symbolName = @(info.dli_sname);
// 如果是OC的方法直接存储
BOOL hasObject = [symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["];
NSString *s_name = hasObject ? symbolName : [@"_" stringByAppendingString:symbolName];
[symbolNames addObject:s_name];
}
// 反向 & 去重
NSEnumerator *em = [symbolNames reverseObjectEnumerator];
// symbolNames.count 节约内存开销
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *s_name;
while(s_name = [em nextObject]){
if (![funcs containsObject:s_name]){
[funcs addObject:s_name];
}
}
NSLog(@"%@",funcs);
// 去掉自己这个调用函数
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
// 写入otther文件
// 把数组里面的内容 变成 字符串加换行
NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
}
funcs 写入 other文件
- 1.运行一遍,找到真机沙盒中的下载之后,打开包内容~找到temp中的hank.other文件 拷贝到
工程的根目录
- 2.在Build Setting 搜索 other file 找到 Other Flie 添加 ./hank.other
-
3.在 Build Setting / Other C Flag 配置中 去掉
-fsanitize-coverage=func,trace-pc-guard
-
4.验证 Build Setting 搜索 link map 找到 Write Link Map File 设置为 YES
验证结果1.找到 产品.app /show in finder/ 找到 [项目名称.build] 找到 [项目名称-LinkMap-normal-arm64.txt]
swift 符号覆盖
- 1.创建一个.swift文件,SwiftTest.swift
- 2.使用.swift
#import "工程名称-Swift.h"
- 3.在 Build Setting 搜索 other swift flag 找到 Other Swift Flags
- 添加1 : sanitize-coverage=func
- 添加2 : sanitize-coverage=undefined
- 4.运行之后打印
网友评论