一、通过插桩获取方法符号
LLVM
内置了一个简单的代码覆盖率检测工具(SanitizerCoverage)。它在函数级、基本块级和边缘级上插入对用户定义函数的调用,通过这种方式可以顺利对OC方法、C函数、Block、Swift的方法/函数
进行全面HOOK
。
1.概念
编译器插桩就是在代码编译期间修改已有的代码或生成新代码。
编译期时,在每一个函数内部二进制源数据添加 hook 代码来实现全局 hook 效果。
2.实现原理及过程(具体实现看步骤二)
(1)设置工程Other C Flags
添加配置 Target -> Build Setting -> Custom Complier Flags -> Other C Flags
添加 -fsanitize-coverage=func,trace-pc-guard
(2)配置获取方法符号代码
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.
}
//收集符号
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
//获取上一个函数的地址,通过这个地址就能拿到函数的符号名称
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
2.查看工程输出结果
工程跑起来看输出结果:3. __sanitizer_cov_trace_pc_guard_init
start 里面存的是一堆序号,而stop 里面的并不是序号
打个断点重新运行发现 start 和 stop 中间是 01 ~ 0e,十进制的 1~14
14
这会不会是函数的序号呢?给他安排个函数试试看。工程中新增个btnClick方法
注意:14加了个1,变成了15
加个block和C函数看看:
15加了2,变成了17。经验证这是函数的序号
上述
__sanitizer_cov_trace_pc_guard_init
方法里面可以获取到所有方法的数量,那么肯定也有办法获取方法具体的相关信息。重点就是接下来要分析的__sanitizer_cov_trace_pc_guard
4. __sanitizer_cov_trace_pc_guard
a.测试获取原方法地址
在[ViewController touchesBegan:withEvent:] 断点运行后,点击屏幕查看:
可以看到在调用方法的时候插入了
__sanitizer_cov_trace_pc_guard
方法。其实是执行了 __sanitizer_cov_trace_pc_guard 后返回到了原来 [ViewController touchesBegan:withEvent:] 的首行。
也就是说,我们可以在 __sanitizer_cov_trace_pc_guard 函数里面拿到
原方法的地址!
a.获取方法符号
(1)dlfcn.h
#import <dlfcn.h>
dlfcn.h 中有一个dladdr()
方法,可以通过函数内部地址找到函数符号。该方法需要用到Dl_info结构体
fbase:共享对象的基址
sname:最近的符号名称
saddr:最近的符号地址
(2)获取Dl_info
打印结果:
通过编译器插桩获取了方法符号,而且顺序正是调用函数的顺序!
那下一步我们可以通过获取的方法符号,写入到
link.order
二.生成link.order文件及具体实现过程
先附上demo代码
ViewController.m文件
#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self btnClick];
}
+ (void)load{
}
- (void)btnClick{
block();
}
void(^block)(void) = ^(void) {
};
// 初始化院子队列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
// 定义节点结构体
typedef struct {
void *pc; // 存下获取到的PC
void *next; // 指向下一个节点
} YZDode;
// 添加处理c函数以及block前缀部分内容
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray *arr = [NSMutableArray array];
while (1) {
YZDode *node = OSAtomicDequeue(&list, offsetof(YZDode, next));
if (node == NULL) { // 退出机制
break;
}
// 获取函数信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
// 处理c函数以及block前缀
// 获取符号名称,如果不是+[和-[开头,视为c函数或Block,前面加_
BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
// c函数及block需要在开头添加下划线
sname = isObjc ? sname : [@"_" stringByAppendingString:sname];
// 去重复
if (![arr containsObject:sname]) {
// 入栈
[arr insertObject:sname atIndex:0];
}
// 打印看看
// NSLog("%s \n", info.dli_sname);
}
// 去掉touchBegan方法(因为启动时,不会调用它)
[arr removeObject:[NSString stringWithFormat:@"%s", __FUNCTION__]];
NSLog(@"%@",arr);
// 将数组合成字符串
NSString *funcStr = [arr componentsJoinedByString:@"\n"];
// 写入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
NSLog(@"path: %@", filePath);
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;
//获取上一个函数的地址,通过这个地址就能拿到函数的符号名称
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
//创建结构体
YZDode *node = malloc(sizeof(YZDode));
*node = (YZDode){PC, NULL};
//结构体入栈
//offsetOf()计算出列尾 OSAtomicEnqueue()把node加入list尾巴
OSAtomicEnqueue(&list, node, offsetof(YZDode, next));
}
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.
}
@end
接下来进入正题
1.收集符号
(1).创建原子队列
启动的相关方法可能在不同的线程执行,如果我们用一个数组直接收集这些符号,会出现线程问题。
多线程问题会想到锁,但是锁耗费性能比较多不推荐使用。建议使用原子队列解决这个问题。
原子队列是栈结构,通过 队列结构 + 原子性 保证顺序。
#import <libkern/OSAtomic.h>
// 初始化院子队列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
// 定义节点结构体
typedef struct {
void *pc; // 存下获取到的PC
void *next; // 指向下一个节点
} YZDode;
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray *arr = [NSMutableArray array];
while (1) {
YZDode *node = OSAtomicDequeue(&list, offsetof(YZDode, next));
// 退出机制
if (node == NULL) { break; }
// 获取函数信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
// 去重复
if (![arr containsObject:sname]) {
[arr insertObject:sname atIndex:0]; // 入栈
}
printf("%s \n", info.dli_sname);
}
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
//获取上一个函数的地址,通过这个地址就能拿到函数的符号名称
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
//创建结构体
YZDode *node = malloc(sizeof(YZDode));
*node = (YZDode){PC, NULL};
//结构体入栈
//offsetOf()计算出列尾 OSAtomicEnqueue()把node加入list尾巴
OSAtomicEnqueue(&list, node, offsetof(YZDode, next));
}
运行查看打印结果:一直在-[ViewController touchesBegan:withEvent:]里死循环
// 控制台打印
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
......
文档里说 __sanitizer_cov_trace_pc_guard 会在
每个边缘级别插入
,那么每执行一次 while 循环应该算是一次边界!
解决方案: 修改为只 hook 函数, Target -> Build Setting -> Custom Complier Flags ->
Other C Flags
修改为-fsanitize-coverage=func,trace-pc-guard
(2).处理load函数
在当前类添加 load 方法后执行看输出,发现 load 并没有被打印。
load
方法调用时插入的 __sanitizer_cov_trace_pc_guard
参数 guard 为0
,默认的函数实现会直接return,导致无法捕获到 load。
只需要屏蔽掉 __sanitizer_cov_trace_pc_guard
中的 if (!*guard) return;
即可:
(3).处理c函数和block的符号
// 添加处理c函数以及block前缀部分内容
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray *arr = [NSMutableArray array];
while (1) {
YZDode *node = OSAtomicDequeue(&list, offsetof(YZDode, next));
if (node == NULL) { // 退出机制
break;
}
// 获取函数信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
// 处理c函数以及block前缀
// 获取符号名称,如果不是+[和-[开头,视为函数或Block,前面加_
BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
// c函数及block需要在开头添加下划线
sname = isObjc ? sname : [@"_" stringByAppendingString:sname];
// 去重复
if (![arr containsObject:sname]) {
// 入栈
[arr insertObject:sname atIndex:0];
}
// 打印看看
printf("%s \n", info.dli_sname);
}
}
2.生成Order File
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray *arr = [NSMutableArray array];
while (1) {
YZDode *node = OSAtomicDequeue(&list, offsetof(YZDode, next));
if (node == NULL) { // 退出机制
break;
}
// 获取函数信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
// 处理c函数以及block前缀
// 获取符号名称,如果不是+[和-[开头,视为c函数或Block,前面加_
BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
// c函数及block需要在开头添加下划线
sname = isObjc ? sname : [@"_" stringByAppendingString:sname];
// 去重复
if (![arr containsObject:sname]) {
// 入栈
[arr insertObject:sname atIndex:0];
}
// 打印看看
// NSLog("%s \n", info.dli_sname);
}
// 去掉touchBegan方法(因为启动时,不会调用它)
[arr removeObject:[NSString stringWithFormat:@"%s", __FUNCTION__]];
NSLog(@"%@",arr);
// 将数组合成字符串
NSString *funcStr = [arr componentsJoinedByString:@"\n"];
// 写入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
NSLog(@"path: %@", filePath);
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
运行输出路径:
获取真机路径下的link.order文件,往下看:
(1)选择Devices and Simulators
(2)选择Downlad Container
.xcappdata文件
,右键显示包内容
,在AppData/tmp
目录下找到link.order文件
,link.order文件
内容如下图:(3)将link.order文件拷贝到工程根目录
(4)Write Link Map File
在Build Settings ->
Write Link Map File
设置为YES(5)查看LinkMap文件
clear一下command+shift+K,再重新编译command+B后,查看Link Map File
查看
Link Map File
:可以看到Link Map File中的输出顺序和link.order中写的方法符号顺序一致!