在上篇 启动优化 中我们最后使用二进制重排方法,将启动相关的符号方法提前加载到内存,从而减少 缺页中断(Page Fault) 来提高启动速度,但我们如何确定需要将哪些方法提前呢?本篇就来介绍寻找这些符号的方法 — Clang插桩

1、Clang插桩配置

  • LLVM 内置了一个简单的代码覆盖率检测工具(SanitizerCoverage),它在 函数级安全教育平台作业登录二进制本块级边缘级 上插入对用户定义函数的调用,通过这种方式,可以顺利对 OC 方法C函数BlockSwift 的方法/函数进行全面HOOK(objc_二进制换成十进制算法msgSend的Hook仅试用于OC)
  • 使用 SanitizerCoverage 需要到 Clang官网 查找二进制转八进制帮助

1.1、环境配置

在官网中我们需要找到T二进制换成十进制算法racing PCs跟踪CPU执行到的代码

19、iOS底层探索-Clang插桩

  • 跟踪二进制的运算规则时需要向Clang中添加下边的标记

    -fsanitize-coverage=trace-pc-guard

  • Build Settings –> Other C Flag线程安全s 中添加上边的标记

    19、iOS底层探索-Clang插桩

  • 编译一下,会报错

    19、iOS底层探索-Clang插桩

  • 原因就是这两个符号的方法没有对应的实现,官网的Example中给出了解决方式,我们只需把这数组排序线程池的七个参数片代码copy进项目,并做一些微调即可

    19、iOS底层探索-Clang插桩

    #import "ViewController.h"
    // 按照官网示例添加3个头文件
    #include <stdint.h>
    #include <stdio.h>
    #include <sanitizer/coverage_interface.h>
    @interface ViewController ()
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    }
    // 此处不需要 extern "C",删掉 
    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 %pn", start, stop);
      // ++进行的指针运算
      for (uint32_t *x = start; x < stop; x++)
        *x = ++N; // Guards should start from 1.
    }
    // 此处不需要 extern "C",删掉 
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
      if (!*guard) return; // Duplicate the guard check.
      // 被插入标记的函数的地址
      void *PC = __builtin_return_address(0);
      char PcDescr[1024];
      // 这个方法会报错,可以注释掉
      //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
      printf("guard: %p %x PC %sn", guard, *guard, PcDescr);
    }
    @end
    

1.2二进制、__sanitizer_cov_trace_pc_guard_init

  • 记录了项目中符号的个数
  • 当开启跟踪配置后,系统会回调 __sanitizer_cov_trace_pc_guard_init 函数,并开辟一段连续空间,空间起始位置为 start、结束位置为 s安全教育手抄报top,它们都是 uint32_t 类型,占 4 字节二进制的运算规则
  • ++安全教育平台x 进行的指针运算,start 每次步长+1向 stop 逼近1指针位置相当swiftly于+4字节,所以最后一个值记录了一共有多少个方法、函数、block,且应该是 stop安全教育平台登录入口址的基础上减去 4字节
验证
  • 不用进行断点,运行结束后点这个暂停符号进入二进制怎么算lldb
    19、iOS底层探索-Clang插桩
  • 打印地址用 x命令 或者 x二进制换成十进制算法/ngx 都可以,能看到每4字节记录的安全教育平台作业登录数+1代表有一个方法、函数、block被数组c语言记录到start~stop的空间中最后在stop-4位置数值不再增长,表示数量全部被安全期计算器记录了
    19、iOS底层探索-Clang插桩

1.3、__sanitiz数组公式er_cov_tracswift翻译e_pc_guard

  • 拦截当前项目中的所有 方法、函数、block(包括 settergetter),二进制重排排的也是本项目的,不会去排外部符号
    19、iOS底层探索-Clang插桩
    19、iOS底层探索-Clang插桩

1.4、插桩原理

  • 修改了二进制文件,在线程数是什么所有的 方法、函数、block 的实现的第一句插入一数组公式__sanitizer_cov_trace_pc_guard() 代码

2、获取符号

通过函数地址可以通过dladdr方法得到Dl_info结构体,然二进制后获取函数信息

// 需要导入 dlfcn.h 头文件
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  // 被插入标记的函数的地址
    void *PC = __builtin_return_address(0);
 
  Dl_info info;
  dladdr(PC, &info);
    printf("fname:%snfbase:%pnsname:%snsaddr:%pnnnn",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
}
  • __builtin_return_addr安全教育日ess(0):返回被插入标记的函数的地址
/*
* Structure filled in by dladdr().
*/
typedef struct dl_info {
    const char   *dli_fname;   /* Pathname of shared object */
    void      *dli_fbase;   /* Base address of shared object */
    const char   *dli_sname;   /* Name of nearest symbol */
    void      *dli_saddr;   /* Address of nearest symbol */
} Dl_info;
  • dli_fname:当前MachO路径线程和进程的区别是什么
  • dli_fbase:当前MachO基地址
  • dli_二进制转化为十进制sname:函数名称
  • dli_saddr:函数地址

19、iOS底层探索-Clang插桩

  • 因为插入线程数是什么安全教育平台作业登录位置是函数实现的线程安全第0行,所以先打印H安全教育平台作业登录ook中自定义的内数组指针容再打印数组指针了函数具体实现

小结

验证打印

19、iOS底层探索-Clang插桩

  • 部分数组函数会被重复调用,产生多余符号,因此还需要去重
  • 个人验证打印函数个数会比打印的具体线程撕裂者函数名多,试了几次多出的暂时固定为3个,因此考虑可能有几个函二进制数被记入Hook函数数,但是未走 __sanitizer_cov_trace_pc_guard 回调,仅供参考

3、收集保存符号

_线程和进程的区别是什么_sanitizer_cov_trace_pc_guard 中的打二进制换成十进制算法印是跟着函数的线程走的,如果函数在子线程,那么这个回调也会在子线程中,因此收集符号时需要保证线程安全

#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
// 创建原子队列需要导入头文件
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property(nonatomic,copy)NSString *name;
@end
@implementation ViewController
void (myBlock)(void) = (void) {
  NSLog(@"myBlock函数");
};
+ (void)load {
  NSLog(@"load方法");
  myBlock();
}
- (void)viewDidLoad {
  [super viewDidLoad];
  NSLog(@"viewDidLoad方法");
  self.name = @"lz";
}
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 %pn", start, stop);
   for (uint32_t *x = start; x < stop; x++)
        *x = ++N; // Guards should start from 1.
}
// 定义原子队列
static OSQueueHead symbolist = OS_ATOMIC_QUEUE_INIT;
// 定义符号结构体
typedef struct {
  void *pc;
  void *next;
} SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  void *PC = __builtin_return_address(0);
  SYNode *node = malloc(sizeof(SYNode));
  // 结构体指针指向 SYNode 结构体(pc属性赋值PC,next赋值NULL)
  *node = (SYNode){PC,NULL};
  // 结构体入栈:参数1:链表地址,参数2:存入的节点、参数3:offsetof(类型,参数2的属性)
  OSAtomicEnqueue(&symbolist, node, offsetof(SYNode, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  while (YES) {
        // 从链表中取出节点(取一个少一个)
    SYNode *node = OSAtomicDequeue(&symbolist, offsetof(SYNode, next));
    // 链表被取空则跳出while循环
        if (node == NULL) {
      break;
    }
    Dl_info info;
    dladdr(node->pc, &info);
    printf("%sn",info.dli_sname);
  }
}
@end
  • 使用原子队列 OSQueueHead,一是为了原子线程相对安全,二是为了队列方便后续 去重 操作

  • OSAtomicEnqueue(&ampswift翻译;链表, 待存入节点, offsetof(待存入节点类型, 待存入节点的属性))

    • OSAtomicEnqueue:入队列
    • offsetof:宏,参数1:传入类型来计算类型大小,参数2:将下一个节点的地址返回给 OSAtomicEswiftlynqueue的第二个参数(因为是链表不用能角标,所以通过计线程池算类型大二进制的运算规则小来算出尾部,然后赋值给 OSAtomicEnqueue 第二个节点的 用于指向下个节点的属性(一般都自定义为neswiftlyxt)

3.1、无限循环BUG坑点

  • 点击屏幕会出现无限调用touchesBegan方法的情况,是因为SanitizerCoverage不但拦截 方法、函数、Block还会对循环进行 HOOK
  • 像例子中写为 w线程数越多越好吗hile(YES),出循环条件为链表被取空,但while被hook导致每执行一次while的判断进入一次回调,链表又被追加延长了,因此永远无法将链表取空;而且回调函数中存入队列的还是 touchesBegan 的函数地址
解决方案
  • 修改数组c语言插桩标记,限定为funcBuild Sswift国际结算系统ettings –> Other C Flags

    -fsanitize-coverage=func,trace-pc-guard

    19、iOS底层探索-Clang插桩

3.2、取反、去重

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
  while (YES) {
    SYNode *node = OSAtomicDequeue(&symbolist, offsetof(SYNode, next));
    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 * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
    [symbolNames addObject:symbolName];
  }
  // 反向遍历数组
  //symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
  //NSLog(@"%@",symbolNames);
    // 反向遍历迭代器
  NSEnumerator *em = [symbolNames reverseObjectEnumerator];
  NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
  NSString *name;
  while (name = [em nextObject]) {
    // 已包含的符号不再入组
    if (![funcs containsObject:name]) {
      [funcs addObject:name];
    }
  }
    // 因为是在touchBegan这个方法中实现的功能,但我们启动优化并不需要touchBegan方法,因此去掉
    [funcs removeObject:[NSString stringWithFormat:@"%s", __func__ ]];
}

4、生成.order文件

紧接着上边的 去重、取反 操作后,将符号集生成 .order 文件

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    ......
    // 将数组转换为string字符串
    NSString *funcStr = [funcs componentsJoinedByString:@"n"];
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"/pagefault.order"];
    NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
}

5、配置.o数组的定义rder文件

  • 拿到 .order 文件,选择Add Additional Sim安全教育平台ulators...

    19、iOS底层探索-Clang插桩

  • 选中案例 App,点击Downlad Container..线程数越多越好吗.

    19、iOS底层探索-Clang插桩

  • 选择路径,下载.xcappdswiftlyata文件线程池,右键显示包内容,在AppData/tmp目录下,找到 .order 文件,将 .order 文件拷贝到工程根目录,在 Build Setting –> Order File进行配置(图中.order文件名搞错了)

    19、iOS底层探索-Clang插桩

  • Build Settings –&g安全教育日t; Write Linswift是什么k Map File,设置为YES

    19、iOS底层探索-Clang插桩

    • Write Link Map File:在Xcode二进制转换器生成可执行文件的同时生成的链接信息文件,用于描述可执行文件的构造部分,包括了代码段和数据段的分布情况
  • 编译项目,打开LinkMap文件,查看是否配置成功

  • 找到生成的文件配置到 Orswift是什么der File 中即可,然后上架前删除除.order外的这些插桩内容,因为会大幅损耗性能

6、收集Swift符号

OC 配置 Other C Flagswift系统s 不同,Swift 因为使用的是swiftc编译器,因此要配置 Other Swift二进制亡者列车 Flags,配置内容也稍有不同,需要配置两个

-sanitize-coverage=func
-sanitize=undefined

19、iOS底层探索-Clang插桩

Swift收集符号检验

19、iOS底层探索-Clang插桩

19、iOS底层探索-Clang插桩