上一节我们熟悉了启动优化中二进制重排的原理和方法。本节继续讲解如何自动生成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
-
按照官方描述,可以加入
跟踪代码,并给出了回调函数。
image.png 打开
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(需要导入库),我们暂时把这一行注释掉
-
运行程序:
image.png
start和stop表示当前文件的开始内存地址和结束内存地址。单位是int324字节
- 如果
多加几个函数,会发现stop地址值也会相应的增加。- 此处是指从
start到stop的前闭后开区间。[ , ),所以stop地址往前偏移4字节,才是最后一个函数符号的地址。
-
清空打印区,点击屏幕,触发touchBegin。我们发现触发了3次guard。
image.png - 这3次分别是
touchBegin、test、block三个函数被触发时的打印。
我们在
touchBegin、test、block和__sanitizer_cov_trace_pc_guard都加入断点,运行代码:
image.png
【验证一】执行顺序是:
touchBegin->__sanitizer_cov_trace_pc_guard->
test->__sanitizer_cov_trace_pc_guard->
block->__sanitizer_cov_trace_pc_guard【验证二】
touchBegin时,进入汇编:
image.png确实
每个函数在触发时,都调用了__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:
【现象】:
image.png【原因】:
- 通过看汇编,可以看到while也触发了
__sanitizer_cov_trace_pc_guard的跳转。原因是,trace的触发,并不是根据函数来进行hook的,而是hook了每一个跳转(bl)。while也有跳转,所以进入了死循环。【方案】:
Build Settings的Other C Flags配置,添加一个func指定条件:-fsanitize-coverage=func,trace-pc-guard
image.png
-
运行代码,点击屏幕:
image.png -
根据打印路径,查看
ht.order文件,完美!
image.png
真机的沙盒文件,可以从这里下载:
选择设备,点击Add ...
image.png- 选择
真机->选择APP->点击设置
image.png点击下载,就可以拿到手机沙盒信息了
image.png- 包内容中,可以找到
ht.order文件
- 复制
ht.order文件,放到根目录,就完成了。
image.png
可以根据 上一节的内容,打开
link Map查看最终的符号排序,使用Instruments检查自己应用的PageFault数量和耗时
注意
- 【二进制重排
order文件】需要代码封版后,再生成。 (代码还在变动,生成就没意义了)- 【二进制重排
相关代码】不要写到自己项目中去。写个小工具跑一下,拿到order文件即可。
5. Swift二进制重排
-
Swift 二进制重排,与OC一样。只是LLVM前端不同。
OC的前端编译器是Clang,所以在other c flags处添加-fsanitize-coverage=func,trace-pc-guardSwift的前端编译器是Swift,所以在other Swift Flags处添加-sanitize=undefined和-sanitize-coverage=func
image.png
-
项目中添加
SwiftTest.swift文件,创建桥接头:
image.png
image.png -
在
ViewController.m中导入桥接头文件:#import "TranceDemo-Swift.h"
image.png -
运行项目,点击屏幕,去打印的目录下,拿到ht.order文件:
image.png
补充:
1 .
swift符号自带名称混淆
未改变代码时,swift符号不会变。
总之,order文件,请在代码封版后,再生成。
- 至此,
Clang插桩和自动生成Order文件,都已完成。 去实战试试吧!

















