美文网首页
启动优化3

启动优化3

作者: f8d1cf28626a | 来源:发表于2022-06-23 18:34 被阅读0次

    二进制重排 & clang插桩

    注意:在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多。

    下面,我们来进行具体的实践,首先理解几个名词

    Link Map

    Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分:

    • Object Files 生成二进制用到的link单元的路径和文件编号

    • Sections 记录Mach-O每个Segment/section的地址范围

    • Symbols 按顺序记录每个符号的地址范围

    ld

    ld是Xcode使用的链接器,有一个参数order_file,我们可以通过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化

    所以二进制重排的本质就是对启动加载的符号进行重新排列

    到目前为止,原理我们基本弄清楚了,如果项目比较小,完全可以自定义一个order文件,将方法的顺序手动添加,但是如果项目较大,涉及的方法特别多,此时我们如何获取启动运行的函数呢?有以下几种思路

    • 1、hook objc_msgSend:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend,但是由于objc_msgSend的参数是可变的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法

    • 2、静态扫描:扫描 Mach-O 特定段和节里面所存储的符号以及函数数据

    • 3、Clang插桩:即批量hook,可以实现100%符号覆盖,即完全获取swift、OC、C、block函数

    • tracing PCs 跟踪CPU执行到的代码

    • 设置 Build Setting -> 搜索 Other c flag -> 找到Other C Flag 添加 -fsanitize-coverage=trace-pc-guard

    Tracing PCs with guards
    With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge:

    • 报错 找不到以下两个函数
    
    // 使用两个回调函数
    
    // trace-pc-guard-cb.cc
    #include <stdint.h>
    #include <stdio.h>
    #include <sanitizer/coverage_interface.h>
    #import <dlfcn.h>
    #import <libkern/OSAtomic.h>
    
    
    // 1.这个函数可以获取项目中符号的个数(函数个数)
    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.
    }
    
    // 2.这个函数可以拦截到当前项目(machO中)所有正在执行的函数,属性
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    
      if (!*guard) return;  // Duplicate the guard check.
      
    // 返回地址return_address的意思是:调用了__sanitizer_cov_trace_pc_guard的函数,谁调用了__sanitizer_cov_trace_pc_guard,谁的地址就是return_address
    
      void *PC = __builtin_return_address(0);
    
      // Dl_info info;
    
      // dladdr(PC,&info);
    
      // 创建结构体
      SYNode *node = malloc(sizeof(SYNode));
      *node = (SYNode){PC,NULL}
    
      // 结构体入栈
      OSAtomicEnqueue(&symbolList,node,offsetof(SYNode,next));
    
      
      
      printf("%s\n %s\n %s\n %s\n",
                                   info.dli_frame,
                                   info.dli_fbase
                                   info.dli_sname
                                   info.dli_saddr);
      // info.bli_sname 是符号名称
    
      // 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);
    }
    
    
    
    // 定义一个原子队列
    static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
    
    // 定义一个符号的结构体
    typedef struct {
      void *pc;
      void *next;
    }SYNode;
    
    

    // 取出symbolList中的数据

    
    -(void)roc_getSymbolList{
    
       while(YES){
    
       SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
       
       if (node == NULL) break;
    
       Dl_info info;
    
       dladdr(node->pc,&info);
    
       printf("%s \n",info.dli_sname);
    
     }
    }
    
       // 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
       // 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
       // 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`
    
    

    start & stop debug

    • start数据

      • 断点调式打印内存 lldb x 0x1009914b8
    start数据
    • stop 数据

      • 断点调式打印内存 lldb x 0x1009914f0
    stop数据
    • 查看stop后一个数据

      • 断点调式打印内存 lldb x (0x1009914f0-4)
    stop最后一个数据 stop读取图解
    结果 0e 00 00 00 (14个符号,即`是dyld之后调用的函数个数`)
    

    Hook all 的原理

    • __sanitizer_cov_trace_pc_guard (可拦截all函数)

      原理分析:
      

    编译之前只要添加了clang插桩的标记:-fsanitize-coverage=trace-pc-guard

    编译之后编译器就会在实现函数的edge插入一个函数,这个函数就是__sanitizer_cov_trace_pc_guard
    so 起到拦截所有函数的效果,在执行函数内部内容之前

    • 相当于在ast(语法树)之后,在IR中每一个函数的函数中的内容之前插入__sanitizer_cov_trace_pc_guard
    汇编形式的拦截原理

    这也是苹果官方对app上线之前做代码审查的方案,所以在发布产品时记得去掉 -fsanitize-coverage=trace-pc-guard 配置

    提示:如果需要制作乌龟壳这是至关重要的一部分

    拦截获取函数符号

    • 打印出all已拦截的符号
    已拦截的all符号
    • 哎妈妈呀,还需要照顾多线程循环体,巨大的坑

    1.在 多线程 会影响执行的顺序,需要捋正

    2.在 while(YES){} 循环体内部也进行了拦截,会导致死循环,这个坑好大吗

    需要将函数符号按照顺序添加到other文件中
    • 解决 多线程 这个坑
    通过原子队列保存符号(看上面代码部分的操作 OSAtomicEnqueue & OSAtomicDequeue)
    
      arm64汇编 `bl` == x86_64汇编 `call`
    
    • 解决 循环体 这个坑
    在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard` 添加了`func,`
    
    • func,仅限方法的调用实现插桩,其它不管
    func,
    • 查看打印 完美解决
    完美解决坑点之后的打印

    取反 & 去重

    
    -(void)roc_getSymbolList{
    
    
       NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];
    
    
       while(YES){
    
       SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
       
       if (node == NULL) break;
    
       Dl_info info;
    
       dladdr(node->pc,&info);
    
       printf("%s \n",info.dli_sname);
    
       // const chanr* 转 oc 字符串
       NSString *symbolName = @(info.dli_sname);
       // 如果是OC的方法直接存储
       if ([symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["]){
          
           [symbolNames addObject:symbolName];
           continue;
       }
       
       [symbolNames addObject:[@"_" stringByAppendingString:symbolName]];
     }
    
      // 反向 & 去重
    
      // symbolNames = (NSMutableArray<NSSting*> *)[[symbolNames reverseObjectEnumerator] allObjects];
    
      NSEnumerator *em = [symbolNames reverseObjectEnumerator];
    
      // symbolNames.count 节约内存开销
    
      NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    
      NSString *s_name;
    
      while(s_name = [em nextObject]){
    
        if (![funcs containsObject:s_name]){
    
           [funcs addObject:s_name];
       }
     }
    
      NSLog(@"%@",funcs);
      NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
      NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
      NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
      [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
    
    }
    
       // 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
       // 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
       // 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`
    
    

    // 秀代码

    -(void)roc_getSymbolList{
    
       NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];
    
       while(YES){
    
       SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
       
       if (node == NULL) break;
    
       Dl_info info;
    
       dladdr(node->pc,&info);
    
       printf("%s \n",info.dli_sname);
    
       // const chanr* 转 oc 字符串
    
       NSString *symbolName = @(info.dli_sname);
    
       // 如果是OC的方法直接存储
    
       BOOL hasObject = [symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["];
    
       NSString *s_name = hasObject ? symbolName : [@"_" stringByAppendingString:symbolName];
    
       [symbolNames addObject:s_name];
    
     }
    
       // 反向 & 去重
    
       NSEnumerator *em = [symbolNames reverseObjectEnumerator];
    
      // symbolNames.count 节约内存开销
    
      NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    
      NSString *s_name;
    
      while(s_name = [em nextObject]){
    
        if (![funcs containsObject:s_name]){
    
           [funcs addObject:s_name];
       }
     }
    
      NSLog(@"%@",funcs);
    
      // 去掉自己这个调用函数
      [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
      // 写入otther文件
      // 把数组里面的内容 变成 字符串加换行
      NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
      NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
      NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
      [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
       
    }
    
    
    

    funcs 写入 other文件

    • 1.运行一遍,找到真机沙盒中的下载之后,打开包内容~找到temp中的hank.other文件 拷贝到 工程的根目录
    真机沙盒
    • 2.在Build Setting 搜索 other file 找到 Other Flie 添加 ./hank.other
    hankOther
    • 3.在 Build Setting / Other C Flag 配置中 去掉 -fsanitize-coverage=func,trace-pc-guard

    • 4.验证 Build Setting 搜索 link map 找到 Write Link Map File 设置为 YES

    验证linkmap

    1.找到 产品.app /show in finder/ 找到 [项目名称.build] 找到 [项目名称-LinkMap-normal-arm64.txt]

    验证结果

    swift 符号覆盖

    • 1.创建一个.swift文件,SwiftTest.swift
    • 2.使用.swift #import "工程名称-Swift.h"
    • 3.在 Build Setting 搜索 other swift flag 找到 Other Swift Flags
      • 添加1 : sanitize-coverage=func
      • 添加2 : sanitize-coverage=undefined
    otherSwift
    • 4.运行之后打印

    相关文章

      网友评论

          本文标题:启动优化3

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