上一节我们熟悉了启动优化
中二进制重排
的原理
和方法
。本节继续讲解如何自动
生成order文件
。
- 什么是hook
- clang插桩
- 获取函数符号
- 存储和导出
- swift二进制重排
1. 什么是hook
hook,是钩子。
获取
原有函数符号
的内存地址
和实现
,勾住
它,做
一些自己想做
的事情
。
- 例如: 你遇到在
公路
上拦
到一辆车
。你可以跟
他的车
一起走(附加自己代码
),也可以直接抢
了他的车
自己开(重写实现
)
很明显,我们此刻就是想勾
住启动结束前
的所有函数
,附加
一些代码
,把函数名
按顺序存下来
,生成我们的order文件
。
Q: 有没有
API
,能让我hook一切
我想hook的东西?swift
、oc
、c
函数我都
要hook
?
A: 有,clang插桩
。 语法树都是它生成的,顺序它说了算。
2. clang插桩
官方介绍: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs
官方提供了
LLVM
的代码覆盖监测工具
。其中包含了Tracing PCs
(追踪PC)。我们创建
TranceDemo
项目,按照官方
给的示例
,来尝试开发
2.1 添加trace
-
按照官方描述,可以加入
跟踪代码
,并给出了回调函数
。
打开
TranceDemo
,Build Settings
中搜索Other C
,加入-fsanitize-coverage=trace-pc-guard
-
复制
项目案例
,粘贴
到项目的ViewController
中,去除注释
和extern 声明
,加入几个测试函数
:
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
@interface ViewController ()
@end
@implementation ViewController
+(void)load {}
void (^block)(void) = ^{ printf("123"); };
void test() { block(); }
- (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) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
// __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
test();
}
@end
Command+B
编译,发现找不到
符号__sanitizer_symbolize_pc
(需要导入库),我们暂时把这一行注释掉
-
运行程序:
start
和stop
表示当前文件的开始内存地址
和结束内存地址
。单位是int32
4字节
- 如果
多加
几个函数
,会发现stop
地址值也会
相应的增加
。- 此处是指从
start
到stop
的前闭后开
区间。[ , )
,所以stop地址
往前
偏移4字节
,才是最后
一个函数符号
的地址
。
-
清空
打印区,点击屏幕
,触发touchBegin
。我们发现触发了3次guard
。
- 这3次分别是
touchBegin
、test
、block
三个函数被触发
时的打印
。
我们在
touchBegin
、test
、block
和__sanitizer_cov_trace_pc_guard
都加入断点,运行代码:
【验证一】执行顺序是:
touchBegin
->__sanitizer_cov_trace_pc_guard
->
test
->__sanitizer_cov_trace_pc_guard
->
block
->__sanitizer_cov_trace_pc_guard
【验证二】
touchBegin
时,进入汇编:
确实
每个函数
在触发
时,都调用了__sanitizer_cov_trace_pc_guard
函数。原因:
- 只要在
Other C Flags
处加
入标记
,开启了trace
功能。LLVM
会在每个函数边缘
(开始位置),插入
一行调用__sanitizer_cov_trace_pc_guard
的代码。编译期
就插入
了。所以可以100%覆盖。
- 以上,就是
Clang插桩
。插桩
操作完成
后,我们需要获取
所有函数符号
、存储
并导出order文件
。
3. 获取函数符号
-
__builtin_return_address
: return的地址。
函数
return
,是返回到上一层
的函数
。
- 通过
return
的地址,拿到的是上一层级
的函数信息
。- 参数:
0
: 表示当前函数的上一层
。1
:是上一层
的上一层
地址。
- 导入
#import <dlfcn.h>
,通过Dl_info
拿到函数信息:
typedef struct dl_info {
const char *dli_fname; /* 文件地址*/
void *dli_fbase; /* 起始地址(machO模块的虚拟地址)*/
const char *dli_sname; /* 符号名称 */
void *dli_saddr; /* 内存真实地址(偏移后的真实物理地址) */
} Dl_info;
- 在
__sanitizer_cov_trace_pc_guard
函数加入代码:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if(!*guard) return;
void *PC = __builtin_return_address(0); //0 当前函数地址, 1 上一层级函数地址
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);
}
- 运行程序,可以看到:
dli_fname
: 文件地址dli_fbase
: 起始地址(machO模块的虚拟地址)dli_sname
: 符号名称dli_saddr
: 内存真实地址(偏移后的真实物理内存地址)
- 此时,我们
成功
拿到函数符号
。
4.存储符号
注意:__sanitizer_cov_trace_pc_guard
函数是在多线程
环境下,所以需要注意写入安全
写入安全
,就是上锁
。 可参考【第二十八、第二十九节】,此处我使用OSAtomic原子锁
。存储方式
,也有很多种, 此处我使用队列
进行存储
。
- 导入
#include <libkern/OSAtomic.h>
原子头文件,创建原子队列
,定义节点结构体
:
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h> // 原子操作
@interface ViewController ()
@end
@implementation ViewController
+(void)load {}
void (^block)(void) = ^{ printf("123"); };
void test666() { block(); }
- (void)viewDidLoad {
[super viewDidLoad];
}
// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; // 原子队列初始化
// 定义符号结构体
typedef struct {
void * pc;
void * next;
}SYNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 这里是多线程,会有资源抢夺。
// 这个会影响load函数,所以需要移除哨兵
// if(!*guard) return;
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 (1) {
// 去除链表
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];
[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:@"ht.order"];
// 文件内容
NSData * fielContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 创建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fielContents attributes:nil];
NSLog(@"%@",funcs);
NSLog(@"%@",filePath);
NSLog(@"%@",fielContents);
}
@end
坑点:
if(!*guard) return;
需要去掉,会影响+load
的写入
while循环
,也会触发__sanitizer_cov_trace_pc_guard
:
【现象】:
【原因】:
- 通过看汇编,可以看到while也触发了
__sanitizer_cov_trace_pc_guard
的跳转。原因是,trace
的触发
,并不是
根据函数
来进行hook
的,而是hook
了每一个跳转(bl)
。while
也有跳转
,所以进入了死循环
。【方案】:
Build Settings
的Other C Flags
配置,添加一个func
指定条件:-fsanitize-coverage=func,trace-pc-guard
-
运行代码
,点击屏幕
:
-
根据打印路径,查看
ht.order
文件,完美!
真机的沙盒文件,可以从这里下载:
选择设备
,点击Add ...
- 选择
真机
->选择APP
->点击设置
点击下载
,就可以拿到手机沙盒
信息了
- 包内容中,可以找到
ht.order文件
- 复制
ht.order
文件,放到根目录
,就完成了。
可以根据 上一节的内容,打开
link Map
查看最终
的符号排序
,使用Instruments
检查自己应用的PageFault数量
和耗时
注意
- 【二进制重排
order文件
】需要代码封版后
,再生成
。 (代码还在变动,生成就没意义了)- 【二进制重排
相关代码
】不要写到
自己项目中
去。写个小工具
跑一下,拿到order文件
即可。
5. Swift二进制重排
-
Swift 二进制重排
,与OC一样
。只是LLVM前端不同
。
OC
的前端编译器
是Clang
,所以在other c flags
处添加-fsanitize-coverage=func,trace-pc-guard
Swift
的前端编译器
是Swift
,所以在other Swift Flags
处添加-sanitize=undefined
和-sanitize-coverage=func
-
项目中添加
SwiftTest.swift
文件,创建桥接头
:
-
在
ViewController.m
中导入桥接头
文件:#import "TranceDemo-Swift.h"
-
运行
项目,点击屏幕
,去打印
的目录
下,拿到ht.order
文件:
补充:
1 .
swift符号
自带名称混淆
未改变
代码时,swift符号
不会变。
总之,order文件
,请在代码封版后
,再生成
。
- 至此,
Clang插桩
和自动生成Order文件
,都已完成
。 去实战
试试吧!