iOS 启动优化-Clang 插桩

作者: 冼同学 | 来源:发表于2021-10-21 13:48 被阅读0次

    前言

    APP启动过程详解+优化(二进制重排)
    一文中了解了由于缺页中断导致启动耗时,我们可以编译的时候根据我们.order方法进行排列,但是我们项目比较大的话,找到方法进行排列就比较困难。而这篇文章就是解决这个问题,拭目以待吧。

    准备工作

    1. Clang插桩配置

    LLVM内置一个简单的代码覆盖仪表SanitizerCoverage)。它将用户定义的函数插入函数、基本块和边缘级别的调用。提供了这些回调的默认实现,并实现简单的覆盖报告和可视化。Clang代码覆盖文章一文有详细的说明,如下:

    1.1 配置

    如果开发的是oc项目的话,在 Build Settings 里的 Other C Flags 中添加如下配置:

    -fsanitize-coverage=trace-pc-guard
    
    Other C Flags配置
    在任意一个控制器或者文件添加文档说明的方法实现,编译通过。如下:
    编译方法
    运行后打印出方法的实现,如下:
    运行打印方法的实现
    __sanitizer_cov_trace_pc_guard_init其中的startstop代表是方法的个数起始和结束的个数,进行for循环,当起始地址的值和终止的相同时return。如下:
    查看起止个数
    这里0e表示14个符号,我们添加个 方法 和block,如下:
    添加block与方法测试
    十六进制10表示16,那么添加一个方法和block相当于添加了两个符号进去了。

    补充:
    我们如果swift项目或者混编的话,在 Build Settings 里的 Other Swift Flags 中添加如下配置:

    -sanitize-coverage=func
    -sanitize=undefined
    
    swift环境配置

    1.2 原理

    我们使用惯用的手法打开汇编断点查看如下:

    main函数中查看汇编断点
    汇编断点中可以看出main函数后会调用__sanitizer_cov_trace_pc_guard,然后继续追踪断点,如下:
    断点追踪
    发现[AppDelegate application:didFinishLaunchingWithOptions:]函数会调用__sanitizer_cov_trace_pc_guard

    经过多次的断点跟踪,后续发现在实现我们的方法后都会调用__sanitizer_cov_trace_pc_guard,相当于系统在编译的时候会给我们项目中方法添加hook,进行标记,定位我们的方法。我们是通过Other c Flags添加的标记,所以肯定是在编译期做这个插入代码的动作

    2. 具体实现

    之前分析了只要调用我们自身的方法就会调用系统为我们添加的hook
    __builtin_return_address(0)这个函数返回的是上一个函数的地址,也就是调用者,这个PC就是上一个函数的地址,表示第0行插入的__sanitizer_cov_trace_pc_guard。我们取出这个地址的信息,我们通过Dl_info来保存该方法的信息。

    Dl_info info;
    dladdr(PC, &info);
    

    导入#import <dlfcn.h>Dl_info的组成,包括路径方法名地址。如下:

    dl_info结构体
    那么知道了信息的保存,就可以运行项目查看方法的名字,如下:
    查看方法名字
    打印的时候没有+load方法,我们打断点发现确实进入了,但是guad0,因此写入的时候去除这个判断。如下:
    查看+load方法

    2.1 原子操作保存方法名

    我们添加1个方法进行获取方法名,我们在存储的时候,多线程调用方法,这个hook也会在多线程,这个时候写入操作的话,会造成线程不安全,因此我们采用原子操作

    我们导入#import <libkern/OSAtomic.h>定义原子队列定义符号结构体,如下:

    声明院子队列和符号结构体
    hook中写入方法,如下:
    hook操作
    这里添加next节点地址也就是PC的信息,方便我们取出的时候判断是否时最后一个最后一个没有下个节点信息的

    touchesBegan中添加for循环,如下:

    touchesBegan添加for循环查看
    结果死循环了,hook把我们的for循环也捕获了,导致了表一直插入进入死循环,我们修改下标记,如下:
    -fsanitize-coverage=func,trace-pc-guard
    
    运行结果
    如果项目中存在swift代码的话,或者block等则需要判断,如下:
    swift与block判断添加
    然后进行添加反向遍历,并去除本身调用的方法,如下:
    添加和反向遍历
    最后写入order文件,如下:
    写入order文件
    运行并打印结果,按照执行的顺序排列,如下:
    打印结果

    2.3 链接orderFile

    写入在OrderFile中进行链接,编译后方法的顺序就是我们启动时执行的顺序,如下:

    链接orderFile

    3. 总结

    通过官方对方法的hook,定位到实现的方法,存入数组,最后按.order文件进行读取,从而减少page fault的次数提高启动速度。写入的具体代码:

    static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
    
    //定义符号结构体
    
    typedef struct {
    
        void * pc;
    
        void * next;
    
    } KBNode;
    
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    
    //  if (!*guard) return;
    
      void *PC = __builtin_return_address(0);
    
        
    
    //    Dl_info info;
    
    //    dladdr(PC, &info);
    
    
    //    NSLog(@"%s\n,\n",info.dli_sname);
    
        
    
        KBNode *node = malloc(sizeof(KBNode));
    
        
    
        *node = (KBNode){PC,NULL};//赋值,强转,next表示下个节点的地址
    
        
    
        OSAtomicEnqueue(&symbolList, node, offsetof(KBNode, next));//原子列表写入方法,并把下个节点的地址写入。
    
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    
    {
    
        //定义数组
    
        NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    
        while (YES) {
    
            KBNode *node = OSAtomicDequeue(&symbolList, offsetof(KBNode, next));
    
            if (node == NULL) {
    
                break;//没有的话结束
    
            }
    
            Dl_info info;
    
            dladdr(node->pc, &info);
    
    //        printf("%s\n",info.dli_sname);
    
            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:@"test.order"];
    
        NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
        
    
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
        
    
        NSLog(@"%@",funcStr);
    
    }
    

    相关文章

      网友评论

        本文标题:iOS 启动优化-Clang 插桩

        本文链接:https://www.haomeiwen.com/subject/otcxaltx.html