Clang 插桩二进制重排

224 阅读3分钟

Clang官方文档:llvm的子库

背景

二进制重排,可减少编译阶段的Page fault(缺页)产生的数量,对启动起到一定的优化作用,缺页会导致物理内存不连续

Page faultInstruments的表现:搜索 Main Thread 底部输出日志筛选:Virtual Memory 查看File Backed Page In 其中耗时时长及count表现出缺页带来的影响

一、项目工程 Build Setting 中设置

当前步骤中的配置操作,在Relase打包上线时删除\color{#FF0000}{当前步骤中的配置操作,在Relase打包上线时删除}

关于OC:

搜索 Other C Flags 设置如下( 推荐第二个 )
-fsanitize-coverage=trace-pc-guard

//仅拦截方法设置,加参数func,防止死循环,取一个后再加入一个(无穷无尽)
-fsanitize-coverage=func,trace-pc-guard

关于Swift:

搜索 Other Swift Flags 设置如下

-sanitize-coverage=func

-sanitize=undefined

二、任意类中写入代码

引入头文件:

#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#include <sanitizer/coverage_interface.h>

触发2个C函数:
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop)
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)

写入以下代码:

调用outOrderFile生成重排二进制文件\color{#FF0000}{调用o utOrderFile 生成重排二进制文件}

//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
} QSNode;

//里面反应了项目中符号的个数!!
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;
}


//HOOK一切的回调函数!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    //创建结构体
    QSNode * node = malloc(sizeof(QSNode));
    *node = (QSNode){PC,NULL};
    //结构体入栈
    OSAtomicEnqueue(&symbolList, node, offsetof(QSNode, next));
}

//生成order文件!!
- (void)outOrderFile {
    //定义数组
    NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    
    while (YES) {//循环体内!进行了拦截!!
        QSNode * node = OSAtomicDequeue(&symbolList, offsetof(QSNode,next));
        if (node == NULL) { break; }
  
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);//转字符串
        /*!
         给函数名称添加 _
           Obj-C 的类方法,带前缀 +[
           Obj-C 的实例方法,带前缀 -[
           C 方法、block调用、swift方法调用,手动加前缀, _
         */
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
    }
    //反向遍历数组
    //    symbleNames = (NSMutableArray<NSString *> *)[[symbleNames reverseObjectEnumerator] allObjects];
    // 数组去重:symbleNames = (NSMutableArray<NSString *> *)[symbleNames valueForKeyPath:@"@distinctUnionOfObjects.self"];
    //    NSLog(@"%@",symbleNames);
    
    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.如果是模拟器编译,文件路径:filePath可以直接找到;
     2.真机编译(上线使用真机编译文件),在Xcode工具栏:Window -> Devices and Simulators (快捷键:Command + Shift + 2), 真机中选中当前的app(下方有“+”、“-”、设置按钮图标),点击设置选择Download Container下载内容,右击显示包内容:AppData/tmp/dataReset.order
     */
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"dataReset.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];

    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    NSLog(@"\n\n====文件路径:%@====数量:%ld",filePath,funcs.count);
    NSLog(@"\n\n%@",funcStr);
}

三、使用order文件及补充知识内容

参考Demo:JABaseUIKit
应用启动时间打印:
Eidt Sehemes Run -> Arguments -> Environments Variables
添加:DYLD_PRINT_STATISTICS ,可设置值为YES1,也可不设值

启动控制台输出结果:

Total pre-main time: 721.74 milliseconds (100.0%)

1. dylib loading time: 601.12 milliseconds (83.2%)
   动态库载入时长(不宜过多,建议6个,过多可以考虑动态库合并)

2. rebase/binding time:  26.70 milliseconds (3.6%)
   重定位和符号绑定(Mach-O)
   虚拟内存:(rebase:  ASLR(程序启动的随机偏移地址) + offset(偏移量) 
   了解虚拟内存与物理内存的发展史

3. ObjC setup time:  66.36 milliseconds (9.1%)
   OC(动态语言)类的注册(runtime维护映射SEL、IMP信息表),多余废弃的类移除

4. initializer time:  27.54 milliseconds (3.8%)
   执行load、构造函数(c/c++等)的耗时

5. Debug时产生,不需要关注
   slowest intializers :
      libSystem.B.dylib :   2.92 milliseconds (0.4%)
      libMainThreadChecker.dylib :  16.67 milliseconds (2.3%)

Command + i,选择运行程序(打开Instruments)

第一次启动:冷启动,耗时长,杀掉立即重启:热启动,耗时较短,物理内存中存在缓存
顶部搜索:Main Thread
底部输出日志筛选:Virtual Memory
File Backed Page In = Page fault(缺页) 耗时优化(查看时长和count)

二进制重排:\color{#FF0000}{二进制重排:}新建一个.order文件(上面步骤代码中生成的:dataReset.order)并在 Build setting 中(Order File)设置.order文件夹所在的路径(./dataReset.order一般放置项目工程同级目录下)

order文件格式如:
_main
+[ViewController load]
+[AppDelegate load]
_objctest(不存在时会自动优化)

生成编译二进制文件:\color{#FF0000}{生成编译二进制文件:}Build settingWrite Link Map File设置为YES,编译完成后搜索Path to Link Map File中的Debug/Release文件路径查看-LinkMap--.text 文件,即为重排后的二进制文件(# Address Size File Name),快速小技巧:编译完成后,在Products文件下会生成.app文件,Show In Finder 后查看上层目录,找到Buiid或者Build下层Intermediates.noindex文件夹