获取全部符号
通过上文IOS-启动优化(上)知道,我们需要对二进制符号进行重排,就的获取到启动前的全部符号。
- 那么我们可以通过什么方式来获取到启动前的所有符号呢?
- 1,fishhook:可以hook系统函数,可以尝试用
fishhook
hookobjc_msgSend
函数,但是由于objc_msgSend
的参数可变,hook需要编写汇编代码(对于作者来说不可行),而且这种方法只能hookOC
的方法,并不能覆盖全部符号 - 2,编译器插桩:这种方法可以做到全部符号的覆盖
- 1,fishhook:可以hook系统函数,可以尝试用
Clang插桩
LLVM
官方提供了代码覆盖监测工具,LLVM文档- 按照官方给的示例,对我们的demo进行开发
根据案列实现
- 在创建的Demo的
Build Settings
中搜索Other C
,找到Other C Flags
加入-fsanitize-coverage=trace-pc-guard
- 直接编译会报下面的错误
- 复制案列中的代码到启动结束的文件中,我们这里以
ViewController
中的viewDidLoad
方法作为启动完成的结束方法,所以将代码复制到ViewController.m
文件中 - 编译又报错
- 注释掉报错的代码(
__sanitizer_cov_trace_pc_guard
方法中),就可以编译成功了。 - 运行demo,通过lldb调试查看打印的信息
- 增加或者减少方法函数stop存的值会变化,这个stop就是符号的总数
- 点击屏幕,会打印下面数据,说明我们每执行一个方法,函数,或者block都会调用
__sanitizer_cov_trace_pc_guard
函数 - 也可以通过汇编观察,只要在
Other C Flags
处加入-fsanitize-coverage=trace-pc-guard
标记,开启了trace PCs
功能。LLVM会在每个函数边缘位置(开始位置),插入一行调用__sanitizer_cov_trace_pc_guard
的代码。编译期就插入了。所以可以100%的符号覆盖。 - 这就是Clang插桩,操作完成后,我们需要获取所有函数符号、存储并导出到order文件中。
获取函数符号
__builtin_return_address
- 我们在
__builtin_return_address
打下断点,查看PC存储的是什么 - 发现
__builtin_return_address(0)
获取的就是上一个调用函数的地址 - 可以通过点击屏幕进入到
touchesBegan:withEvent:
,而PC的地址就是调用__sanitizer_cov_trace_pc_guard
函数的下一行地址,lldb也是通过__builtin_return_address
查看调用堆栈的,我们也可以传1获取上上一个函数的调用地址
获取符号
- 导入#import <dlfcn.h>,通过
Dl_info
拿到函数信息。Dl_info
是一个结构体
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
- 修改
__sanitizer_cov_trace_pc_guard_init
的实现如下
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
// 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);
Dl_info info;
dladdr(PC, &info); // 读取PC地址,赋值给info
printf("dli_fname:%s \n dli_fbase:%p \n dli_sname:%s \n dli_saddr:%p \n ", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
}
- 我们需要拿到的就是
info.dli_sname
存储符号
- 由于
__sanitizer_cov_trace_pc_guard
函数是在多线程环境下,所以需要注意写入安全 - 这里我们使用原子队列存储,导入#include <libkern/OSAtomic.h>头文件,创建原子队列,定义节点结构体:
#import <libkern/OSAtomic.h> // 原子操作
@interface ViewController ()
@end
@implementation ViewController
// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; // 原子队列初始化
// 定义符号结构体
typedef struct {
void * pc;
void * next;
}SYNode;
- 我们在
__sanitizer_cov_trace_pc_guard
将符号放到队列里面去,在touchesBegan
里面从队列中取出数据
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0); //0 当前函数地址, 1 上一层级函数地址
Dl_info info; // 声明对象
dladdr(PC, &info); // 读取PC地址,赋值给info
// 创建结构体
SYNode * node = malloc(sizeof(SYNode)); // 创建结构体空间
*node = (SYNode){PC, NULL}; // node节点的初始化赋值(pc为当前PC值,NULL为next值)
// 加入结构 (offsetof: 按照参数1大小作为偏移值,给到next)
// 拿到并赋值
// 拿到symbolList地址,偏移SYNode字节,将node赋值给symbolList最后节点的next指针。
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 创建可变数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
// 每次while循环,都会加入一次hook (__sanitizer_cov_trace_pc_guard) 只要是跳转,就会被block
// 直接修改[other c clang]: -fsanitize-coverage=func,trace-pc-guard 指定只有func才加Hook
while (YES) {
// 去除链表
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if(node ==NULL) break;
Dl_info info = {0};
// 取出节点的pc,赋值给info
dladdr(node->pc, &info);
// 释放节点
free(node);
// 存名字
NSString *name = @(info.dli_sname);
// 三目运算符 写法
BOOL isObjc = [name hasPrefix: @"+["] || [name hasPrefix: @"-["];
NSString * symbolName = isObjc ? name : [NSString stringWithFormat:@"_%@",name];
NSLog(@"symbolName:%@",symbolName);
[symbolNames addObject:symbolName];
}
// 反向集合
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
// 创建数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
// 临时变量
NSString * name;
// 遍历集合,去重,添加到funcs中
while (name = [enumerator nextObject]) {
// 数组中去重添加
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
// 移除当前touchesBegan函数 (跟启动无关)
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
// 数组转字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
// 文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"xq.order"];
// 文件内容
NSData * fielContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 创建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fielContents attributes:nil];
NSLog(@"%@",funcs);
NSLog(@"%@",filePath);
NSLog(@"%@",fielContents);
}
- 运行成功后点击屏幕发现进入了死循环
- 通过查看汇编发现,每一次的b跳转都会触发
__sanitizer_cov_trace_pc_guard
,所以每一次while循环都会触发__sanitizer_cov_trace_pc_guard
- 所以我们需要在
Build Settings
中找到Other C Flags
修改为-fsanitize-coverage=func,trace-pc-guard
,这个就是忽略掉一些循环带来的影响。 - 运行完成后我们需要的符号文件就已经存储在沙盒的tmp文件中了,通过下面操作拿到对应的.order文件。
- 打开.order文件,就可以看到我们获取的符号了
swift插桩
- 因为oc和swift使用的编译器前端不一样,所以对swift要进行单独处理
- 知道了Clang插桩了,swift插桩也就简单了,只是修改下命令而已
- 在项目中创建一个
SwiftTest
的swift文件,自动创建桥接头。在SwiftTest
编写测试代码,在ViewController.m
中导入桥接头文件:#import "TracePCsDemp-Swift.h"
,在load方法中执行swift方法 - Swift的前端编译器是Swift,所以在
other Swift Flags
处添加-sanitize=undefined
和-sanitize-coverage=func
- 运行成功后可以看到多出来的几个方法
"+[ViewController load]",
"_$s12TracePCsDemp9SwiftTestC05swiftE0yyFZTo",
"_$s12TracePCsDemp9SwiftTestC05swiftE0yyFZ",
"_$ss5print_9separator10terminatoryypd_S2StFfA0_",
"_$ss5print_9separator10terminatoryypd_S2StFfA1_",
"_main",
"-[AppDelegate window]",
"-[AppDelegate setWindow:]",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[ViewController viewDidLoad]"
- 拿到了.order文件就可以直接在Xcode中测试了