IOS-启动优化(下)

1,009 阅读5分钟

获取全部符号

通过上文IOS-启动优化(上)知道,我们需要对二进制符号进行重排,就的获取到启动前的全部符号。

  • 那么我们可以通过什么方式来获取到启动前的所有符号呢?
    • 1,fishhook:可以hook系统函数,可以尝试用fishhookhookobjc_msgSend函数,但是由于objc_msgSend的参数可变,hook需要编写汇编代码(对于作者来说不可行),而且这种方法只能hookOC的方法,并不能覆盖全部符号
    • 2,编译器插桩:这种方法可以做到全部符号的覆盖

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中测试了