前言
iOS App启动优化《二进制重排》我们讲述了App的pre-main阶段的流程以及二进制重排的原理,接着我们就用这篇文章来实现二进制重排。
1 配置Clang插桩
我们打开Clang的官方文档# Clang 13 documentation
在这里有一个Tracing PCs,PC指的是PC寄存器,CPU在读取代码的指针即读取虚拟内存的那一行代码。
所以Tracing PCs跟踪的是CPU执行到的代码。
如何使用的呢,官方文档有详细介绍,我们来配置下,我们先添加一个标记
-fsanitize-coverage=trace-pc-guard
如图
1
我们编译一下,如图
2
这里报链接错误,找不到符号,这是因为还需要实现两个回调函数
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;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
我们再次编译,编译成功。
这里有一行printf("INIT: %p %p\n", start, stop);代码,我们看下这个start和stop是什么,我们运行一下,看下效果,如图
这里打了两个地址,我们来分析下这些是什么。
start和stop都是uint32_t即unsigned int类型的指针,这说明上面打印的地址存放的是unsigned int类型数据,这些unsigned int类型数据到底是什么,我们看下,如图
4
这里的start和stop代表符号个数,我们看下最后个数据,如图
5
其中11000000是最后一个数据,stop往上走4个字节读取最后一个数据。
*for (uint32_t *x = start; x < stop; x++)
x = ++N;
这里就是从start位置到stop这个位置读取符号个数,这里是11个符号,我们来验证下
void test() {
}
我们在ViewController.m加入
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
再次运行,如下所示
6
这里变成了13,我们刚才增加了两个方法,加进去了,这说明我们的方法和函数都拦截到了。
我们再来尝试下block,如下
void (^block)(void) = ^(void) {
};
我们再来看,如图
7
这说明我们的block也拦截住了。
这个Clang的Trace是全局的,其它的文件一样可以拦截。
+ (void)load {
}
+ (void)initialize {
}
我们再加上这两个函数,调试如下图
说明load和initialize方法可以拦住的。
__sanitizer_cov_trace_pc_guard我们来调试下这个函数,打个断点,如图
9
运行项目,点击屏幕,看下堆栈,如图
10
我们可以看到touchesBegan这个方法调起了__sanitizer_cov_trace_pc_guard这个函数,我们改下touchesBegan的代码,如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"屏幕点击了");
}
再次运行,如图
11
经过测试发现,__sanitizer_cov_trace_pc_guard这个函数在NSLog之前调用了,我们在block,test()函数加上打印,发现都在NSLog之前调用了__sanitizer_cov_trace_pc_guard这个函数,说明它可以拦截当前项目中的所有符号,系统库和三方库不会拦截。
我们重新排列的是代码的实现的二进制,系统库和三方库不在我们
项目中生成Mach-o文件。
我们自定义的属性生成的get和set方法是可以拦截到的。
2 Clang的原理分析
__sanitizer_cov_trace_pc_guard这个函数是HOOK一切的回调函数,我们来分析下它是如何做到的。
我们在这个函数打个断点,然后运行,过掉所有断点,再次打上断点,点击屏幕,断住之后,我们来分析下它的汇编,如图
我们看下touchBegan,如图
13
这里可以看出当touchBegan被调起的时候,立马进入了__sanitizer_cov_trace_pc_guard这个回调函数,
我们看到汇编bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29,这是bl到了__sanitizer_cov_trace_pc_guard这个函数,这说明我们只要添加了Clang插桩的标记,编译器就会在所有的方法,函数,block的代码实现的边缘的加上一句bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard代码,在实现函数的代码之前加上了这句代码,同样在函数和block中都有这样的代码。
这里相当于修改了二进制文件,如何修改的,我们分析下。
在所有的方法前面插入一行代码,只有编译器能做到,编译器在读到我们的方法,函数,block时就会插入这行代码。
我们是通过Other c Flags 添加的标记,所以肯定是在编译期做这个插入代码的动作。
3 获取到符号
我们的目标是最终要生成order文件,那就需要获取到符号名称和顺序,怎么才能获取到呢,我们分析看看。
我们先打开void *PC = __builtin_return_address(0);这个函数,__builtin_return_address这个函数返回的是上一个函数的地址,也就是调用者,这个PC就是上一个函数的地址,也就是函数的第一行代码的地址,第0行插入了bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29代码
我们可以通过个地址获取到符号的名字,代码如下
Dl_info info;
dladdr(PC, &info);
这里需要导入#import <dlfcn.h>这个头文件,函数的信息会存在info这个结构体中,我们看下这个结构体的定义,如下
/*
* Structure filled in by dladdr().
*/
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;
- dli_fname m文件名字
- dli_fbase m文件的地址
- dli_sname 函数或方法的名字
- dli_saddr 地址
我们来验证下,代码如下
printf("fname:%s\nfbase:%p\nsname:%s\nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname, info.dli_saddr);
运行,效果如下
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:+[ClangTrace load]
saddr:0x100029a30
INIT: 0x10002d788 0x10002d7e0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:main
saddr:0x100029e5c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate setWindow:]
saddr:0x100029d7c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr:0x100029ad0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[ViewController viewDidLoad]
saddr:0x1000297a4
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
这里就获取了符号名称以及调用顺序。
4 利用原子队列保存符号
我们在__sanitizer_cov_trace_pc_guard加入
NSLog(@"%@", [NSThread currentThread]);
然后顺ViewController.m文件加入代码
+ (void)load {
}
+ (void)initialize {
}
void test() {
NSLog(@"test函数执行");
}
void (^block)(void) = ^(void) {
NSLog(@"block函数执行");
};
- (void)sleepT{
sleep(3);
}
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:@selector(sleepT) withObject:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSLog(@"屏幕点击了");
test();
}
运行项目,看下效果,如下所示
2021-09-02 13:40:26.424 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController load]
2021-09-02 13:40:26.425 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
main
2021-09-02 13:40:27.311 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController initialize]
2021-09-02 13:40:27.312 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.318 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate setWindow:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate application:didFinishLaunchingWithOptions:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.325 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.330 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[ViewController viewDidLoad]
2021-09-02 13:40:27.336 ClangTrace[2817:871748] <NSThread: 0x12ed20eb0>{number = 2, name = (null)}
-[ViewController sleepT]
2021-09-02 13:40:27.346 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
这里打印了线程的信息,其中<NSThread: 0x12ed20eb0>{number = 2,name = (null)}这里说明了在子线程也是可以获取的。
所以__sanitizer_cov_trace_pc_guard这个回调也是多线程的,我们的方法在子线程执行的话,这个回调函数也是在子线程执行的。在这里存储数据的话,就有多线程的访问,就会造成线程不安全。
我们就用线程安全的队列OSAtomic来处理,代码如下
// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定义符号的结构
typedef struct {
void * pc; // 函数地址
void * next; // 下一个函数节点
}SymboNode;
我们在修改__sanitizer_cov_trace_pc_guard这个函数的代码,如下
/// HOOK一切的回调函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
// 创建结构体
SymboNode *node = malloc(sizeof(SymboNode));
// 先给node赋值,下个节点暂时先为空
*node = (SymboNode){PC, NULL};
// 结构体入栈,node存入symbolList,并把下一个地址给到node的next属性
OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}
我们在touchesBegan加入代码
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//获取符号信息
Dl_info info;
dladdr(node->pc, &info);
printf("%s\n",info.dli_sname);
}
运行看下效果,如下
[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
发现死循环了,这是为什么呢?
__sanitizer_cov_trace_pc_guard这个函数把我们的循环也给拦截了,如何解决呢,我们需要修改Other C Flags的标记,如下
-fsanitize-coverage=func,trace-pc-guard
我们再运行一下项目看下结果,如下
-[ViewController touchesBegan:withEvent:]
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:]
-[ViewController sleepT]
-[ViewController viewDidLoad]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
+[ViewController initialize]
-[AppDelegate application:didFinishLaunchingWithOptions:]
main
+[ViewController load]
结果正常了,所以我们应该只拦截方法。
5 方法顺序调整和去除重复的符号
从上面的运行结果可以看出,这个顺序是反的,并且这里还有很多重复,需要我们处理一下,代码如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 定义数组
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//获取符号信息
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 区分函数,block和OC方法的符号,函数与block是一样的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍历数组
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍历去除重复的符号
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
NSLog(@"%@",funArray);
}
6 生成order文件
最后一步,就把这些拦截到的符号写入到文件中,代码如下
// 定义数组
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//获取符号信息
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 区分函数,block和OC方法的符号,函数与block是一样的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍历数组
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍历去除重复的符号
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
NSLog(@"%@",funArray);
//去掉自己
[funArray removeObject:[NSString stringWithFormat:@"%s", __func__]];
// 写入order文件
// 变成字符串
NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
// 存储路径
NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
// 文件
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 创建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@", funcStr);
运行之后, 我们把真机上的文件download下来,找到这个clangTrace.order文件,如图
13
这样就保存下来了,我们可以使用下order文件,如图
14
运行,我们看下map文件的内容,如图
15
跟我们order文件的顺序一模一样的。
7 Swift符号覆盖
我们创建一个swift文件,如下
import UIKit
class SwiftTest: NSObject {
@objc class public func swifttest () {
print("swifttest......")
}
}
然后在ViewController.m文件中的load方法加入
+ (void)load {
[SwiftTest swifttest];
}
运行下,如图
这是没有拦截到swift的方法,这个时候需要怎么解决呢?
我们需要加一下配置,如图
17
sanitize-coverage=func和-sanitize=undefined参数,运行,如图
18
这时候可以看到swift的方法也拦截住了,这里的swift符号是经过混淆的,这是编译器自动添加的。
总结
这篇文章我们通过Clang的插桩实现了二进制重排,并在此过程解决了很多坑,本人在这个过程中也学习到了很多知识,文章有很多不足之处,不过还是希望可以给大家带来知识。
附完整的代码
// OC插桩标记
-fsanitize-coverage=func,trace-pc-guard
// swift插桩标记
sanitize-coverage=func
-sanitize=undefined
ClangTrace.h代码如下
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClangTrace : NSObject
/// 生成order文件
/// @param filePath 文件路径
void generateOrderFile(NSString *filePath);
@end
NS_ASSUME_NONNULL_END
ClangTrace.m代码如下
#import "ClangTrace.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定义符号的结构
typedef struct {
void * pc; // 函数地址
void * next; // 下一个函数节点
}SymboNode;
@implementation ClangTrace
/// 生成order文件
/// @param filePath 文件路径
void generateOrderFile(NSString *filePath) {
// 定义数组
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//获取符号信息
Dl_info info;
dladdr(node->pc, &info);
// 转字符串
NSString *name = @(info.dli_sname);
// 区分函数,block和OC方法的符号,函数与block是一样的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍历数组
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍历去除重复的符号
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
//去掉自己
[funArray removeObject:[@"_" stringByAppendingFormat:@"%s", __func__]];
// 写入order文件
// 变成字符串
NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
if ([ClangTrace isBlankString:filePath]) {
// 存储路径
filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
}
// 文件
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 创建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"\n%@", funcStr);
}
/// 项目中的符号个数
/// @param start 起始位置
/// @param stop 结束位置
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.
}
/// HOOK一切的回调函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
// 创建结构体
SymboNode *node = malloc(sizeof(SymboNode));
// 先给node赋值,下个节点暂时先为空
*node = (SymboNode){PC, NULL};
// 结构体入栈,node存入symbolList,并把下一个地址给到node的next属性
OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}
/// 判断字符串是否为空,返回YES字符串为空,NO相反
/// @param str 字符串
+ (BOOL)isBlankString:(NSString *)str {
NSString *string = str;
if (string == nil || string == NULL) {
return YES;
}
if ([string isKindOfClass:[NSNull class]]) {
return YES;
}
if ([[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length]==0) {
return YES;
}
return NO;
}
@end
网友评论