Clang插桩之生成Order文件

301 阅读4分钟

# iOS启动优化之二进制重排文章已经了解了App启动优化和order文件的作用,今天我们研究clang插桩和咱们自动生成Order文件。

一.  探索插桩

进程如果能之间访问物理内存无疑是很不安全的,所以操作系统在物理内存的上层又建立了一层虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读入数据。

通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:

image.png 重排order:目的就是通过排列启动时需要使用的函数,把启动时需要函数分配到开头的虚拟内存Page中,来减少Page Fault可能的次数,进而提升App启动的效率。

核心问题

  • 重排效果怎么样 - 获取启动阶段的Page Fault次数
  • 重排成功了没 - 拿到当前二进制的函数布局
  • 如何重排 - 让链接器按照指定顺序生成Mach-O
  • 重排的内容 - 获取启动时候用到的函数 (参考资料:字节跳动:Leo的二进制重排提升App启动速度25%)

1、获取符号数量,启动时刻符号

参考:Clang技术文档

新建一个工程TraceDemo:然后Build Settings->Other c file 配置 -fsanitize-coverage=func,trace-pc-guard 参数如图: image.png 再次编译工程就会报错: image.png 找不到这两个函数,那么系统在我们配置完后需要实现这两个函数。

//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)

static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

typedef struct {

    void * pc;

    int abc;

} 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;//内存平移

    NSLog(@"%d",N);

}

//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

    NSLog(@"%s",__func__);

}

image.png 断点运行发现,start 内存地址是以4字节连续平移存储的,然后在__sanitizer_cov_trace_pc_guard_init中计算平移可得到地址数据。

image.png 运行代码发现这个工程启动需要27个符号。

void test(void) {

    NSLog(@"%s",__func__);

}
void (^block) (void) = ^{

    NSLog(@"%s",__func__);

};

添加这两个函数在运行代码: image.png 发现27增加到了29了。所以不管是隐藏函数,函数,block都会在启动函数中。

APP在启动时每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数。验证添加代码:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSLog(@"%s",__func__);

    test();

    block();
}

image.png 看到打印了,前执行回调在执行方法,每次都会在执行方法前回调一次__sanitizer_cov_trace_pc_guard函数。

//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

//    NSLog(@"%s",__func__);

    if (!*guard) return;

    void *PC = __builtin_return_address(0);

    //可以获取一个函数名称和地址

    Dl_info info;

    dladdr(PC, &info);

    printf("dli_fname = %s\n",info.dli_fname);

    printf("dli_fbase = %p\n",info.dli_fbase);

    printf("dli_sname = %s\n",info.dli_sname);

    printf("dli_saddr = %p\n",info.dli_saddr);

}

运行代码查看打印: image.png 我们就得到了APP运行的启动需要调用的函数符号。 (PS:这些代码是我抄的,其实我自己写不出来)

2、保存符号,生成Order文件

//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    //可以获取一个函数名称和地址
    Dl_info info;
    dladdr(PC, &info);
    printf("dli_sname = %s\n",info.dli_sname);
}
打印结果:
**INIT: 0x1014cf8d0 0x1014cf948**
**2022-07-10 16:36:28.586884+0800 TraceDemo[31461:846896] 30**
**dli_sname = main**
**dli_sname = -[AppDelegate application:didFinishLaunchingWithOptions:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate setWindow:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate scene:willConnectToSession:options:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[ViewController viewDidLoad]**
**dli_sname = -[SceneDelegate sceneWillEnterForeground:]**
**dli_sname = -[SceneDelegate sceneDidBecomeActive:]**

看到打印,我们要生成Order文件 需要去除重复,记录或者写入不重复的符号到Order文件中。

第一步保存所有字符:

//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct {
    void * pc;
    int abc;
} SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    NSLog(@"%s",__func__);
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,0};
    //插入结构体
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
}

第二步生成order文件,并去除重复:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//创建数组
    NSMutableArray *symbleNames = [NSMutableArray array];
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, abc));
        if (node == NULL) {
            break;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        //转为OC字符串方便操作
        NSString *name = @(info.dli_sname);
        //OC方法 直接添加到数组
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbleName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbleName];
    }
    NSEnumerator *em = [symbleNames reverseObjectEnumerator];
    //新数组存储
    NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString *name;
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }

    //数组转为字符串
    NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
    //文件路径
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lg.order"];

    //文件内容
    NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    //写入文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
}

运行代码可知: image.png image.png (swift 需要配置才能被编译器执行到order中): image.png

3、总结

1.目的就是通过排列启动时需要使用的函数,把启动时需要函数分配到开头的虚拟内存Page中,来减少Page Fault可能的次数,进而提升App启动的效率。

2.需要了解系统编译运行过程,及clang相关知识。才有能力思考解决相关问题。

3.在通过一步一步验证,实现最终生成order文件提高App启动效率的目的。