启动优化-Clang 插桩

·  阅读 1704
启动优化-Clang 插桩

我们在前面讲了二进制重排启动优化的原理,在没有重排之前每一个数据页中的代码有可能是启动时刻的代码,也有可能不是,这样就造成了浪费。二进制重排之后,就会把所有启动时刻的代码都排到最前面,这样就减少了缺页中断的次数,也就节约了启动时间。

配置 Clang 插桩

  • 步骤 1

image.png

首先我们打开 clang 文档,这里 Traching PC 就是追踪当前 cpu 执行的指令。

  • 步骤 2

image.png

image.png

image.png

image.png

接着往下查看文档告诉我们在项目中配置 -fsanitize-coverage=trace-pc-guard,配置完成之后我们编译项目会发现报错,就是告诉我们找不到 __sanitizer_cov_trace_pc_guard_init__sanitizer_cov_trace_pc_guard 这两个函数的实现。所以我们下面实现这两个函数。

  • 步骤 3
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

@implementation ViewController

- (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) {
  
}

@end
复制代码

在项目中实现这两个函数之后再编译就可以了。

__sanitizer_cov_trace_pc_guard_init 函数调试

image.png

image.png

这里 starstop 是无符号整型,代表开始位置与结束位置,x 结束位置 - 4 就代表符号个数。第一次输出的符号个数是 3。第二次我们增加了 oc 方法, c函数, block 之后再输出个数是 6。这里就说明 __sanitizer_cov_trace_pc_guard_init 可以收集到符号的数量并回调。

__sanitizer_cov_trace_pc_guard_init 函数调试

image.png

image.png

image.png

通过对 __sanitizer_cov_trace_pc_guard_init 函数进行断点可以看到,当我们执行 oc 方法, c函数, block 之后紧接着都会执行 __sanitizer_cov_trace_pc_guard_init,说明 __sanitizer_cov_trace_pc_guard_init 函数能拦截一切方法, 函数, block 的执行。这里需要注意的是这里拦截的都是当前项目中的符号,系统的库及三方库中不在当前项目二进制文件中的符号不会被拦截。

这里我们知道了 __sanitizer_cov_trace_pc_guard_init 函数能拦截所以的符号,那么它拦截的原理又是什么呢?我们接着往下看。

Clang 插桩的原理

image.png

image.png

image.png

通过汇编我们可以看到,在执行方法, 函数, block 的时候都能看到 bl 0x100732684 这句汇编代码。也就是说只要添加了 clang 插桩的标记,编译器就会在所有的方法, 函数, block 的边缘添加一句 __sanitizer_cov_trace_pc_guard 代码。也就相当于修改了二进制文件。而这个事情只有编译器能做,编译器在将高级语言翻译成汇编代码的时候做了这些事情。至于在前端做的还是在后端做的这个官方没有给出说明,我们只能去分析,在前端 IR 阶段的时候或者后端都可以做这件事情。但是查桩肯定是损耗性能的,所以我们一般都是在做代码审查的时候用。

获取到符号名称

//HOOK一切的回调函数!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    NSLog(@"fname:%s\n fbase:%p\n sname:%s\n saddr:%p\n",
          info.dli_fname,
          info.dli_fbase,
          info.dli_sname,
          info.dli_saddr);
}
复制代码
typedef struct dl_info {
        const char      *dli_fname;     /* 文件名称,也就是 mach-o 文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 函数名称 */
        void            *dli_saddr;     /* 函数地址 */
} Dl_info;
复制代码

image.png

这里 __builtin_return_address 函数的意思就是获取到当前函数的内部返回地址,也就是上一个函数的地址,当前函数的调用者。所以这里 *PC 就是上一个函数的第 0 句代码的地址。这里我们就有机会拿到上一个函数的符号名称。这里我们导入 #import <dlfcn.h> 头文件,dladdr(node->pc, &info) 这一句相当于把 PC 指向的地址数据信息赋值给 info 这个结构体。最后通过打印也输出了对应的信息。这里就能拿到所有符号的名称。

通过原子队列保存符号生成 order 文件

#import "ViewController.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;
} SYNode;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    test();
}

void(^block)(void) = ^(void){
    NSLog(@"block函数执行!");
};

void test(){
    NSLog(@"test函数执行!");
    block();
}

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);
    //创建结构体
    SYNode * node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //把结构体 node 写入到 symbolList,offsetof(SYNode, next) 表示设置 node 的 next,下一个节点,就是在上一个 next 的基础上加上 SYNode 大小
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}

//生成order文件!!
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //定义数组
    NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    
    while (YES) {//循环体内!进行了拦截!!这里需要注意的是 while 也会被拦截,要做工程中做配置只拦截方法
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
        if (node == NULL) {
            break;
        }
        
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);//转字符串
        //给函数名称添加 _,这里要判断是否是函数,是函数的话要添加_
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
    }
    
    //反向遍历数组,因为入栈函数调用顺序是反的,所以这里要取反
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    // 这里要对数组进行去重,因为有的方法会多次调用
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {//数组没有name
            [funcs addObject:name];
        }
    }
    
    //去掉自己!也就是当前方法
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
    //写入文件
    //1.编成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"chenxi.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    //写入到沙盒路径
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    // 打印数组字符串
    NSLog(@"%@",funcStr);
}
复制代码

上面我们已经通过拦截获取到所以符号的名称,那么我们可以通过代码,讲拦截到的符号进行保存并生成 order,代码如上。但是这里要注意几个问题。

  • 多线程的问题,如果是子线程中的方法,那么 __sanitizer_cov_trace_pc_guard 函数也会在子线程中执行,所以这里写入的时候要注意多线程影响,这里通过原子队列保存。

  • while 循环的影响,因为 __sanitizer_cov_trace_pc_guard 函数也会拦截到 while 循环,这样写入的时候就会造成递归调用,所以配置 other c flags 参数的时候要多加一个条件 -fsanitize-coverage=func,trace-pc-guard,只拦截方法。

  • 生成 order 文件的时候要对符号方法进行取反,因为写入的时候是倒序的,以及对符号名称去重。

  • 函数名称前面要添加_

image.png

下载沙盒文件并打开 order 文件可以看到确实把 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 只前的方法都收集到了,这时候我们就可以把 order 配置到工程。这时候就完成了二进制重排。

Swift 符号覆盖

image.png 当我们添加了要给 swift 的类,并调用了 swift 类的方法,发现并没有收集到 swift 的方法,这是因为 swift 需要做单独的配置。

image.png 需要在other swift flags 这里添加 -sanitize-coverage=func-sanitize-undefined 这两个参数。

image.png 这时候再打印可以看到 swift 方法被收集到了,而且编译器对 swift 符号名称做了混淆,这也可以看出 swift 相对于 OC 会更安全。

分类:
iOS
收藏成功!
已添加到「」, 点击更改