美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理三十四:启动优化(Clang插桩)

OC底层原理三十四:启动优化(Clang插桩)

作者: markhetao | 来源:发表于2020-11-22 18:09 被阅读0次

    OC底层原理 学习大纲

    上一节我们熟悉了启动优化二进制重排原理方法。本节继续讲解如何自动生成order文件

    1. 什么是hook
    2. clang插桩
    3. 获取函数符号
    4. 存储和导出
    5. swift二进制重排

    1. 什么是hook

    hook,是钩子。
    获取原有函数符号内存地址实现勾住它,一些自己想做事情

    • 例如: 你遇到在公路一辆车。你可以他的一起走(附加自己代码),也可以直接了他的自己开(重写实现

    很明显,我们此刻就是想启动结束前所有函数附加一些代码,把函数名按顺序存下来,生成我们的order文件

    Q: 有没有API,能让我hook一切我想hook的东西?swiftocc函数我hook?
    A: 有,clang插桩。 语法树都是它生成的,顺序它说了算。

    2. clang插桩

    官方介绍: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

    • 官方提供了LLVM代码覆盖监测工具。其中包含了Tracing PCs(追踪PC)。

    • 我们创建TranceDemo项目,按照官方给的示例,来尝试开发

    2.1 添加trace

    • 按照官方描述,可以加入跟踪代码,并给出了回调函数

      image.png
    • 打开TranceDemo , Build Settings中搜索Other C,加入-fsanitize-coverage=trace-pc-guard

    image.png
    • 复制项目案例粘贴到项目的ViewController中,去除注释extern 声明,加入几个测试函数:
    #import "ViewController.h"
    #include <stdint.h>
    #include <stdio.h>
    #include <sanitizer/coverage_interface.h>
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    +(void)load {}
    
    void (^block)(void) = ^{ printf("123"); };
    
    void test() { block(); }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    }
    
    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.
    }
    
    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 %s\n", guard, *guard, PcDescr);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        test();
    }
    
    @end
    

    Command+B编译,发现找不到符号__sanitizer_symbolize_pc(需要导入库),我们暂时把这一行注释掉

    • 运行程序:


      image.png

    startstop表示当前文件的开始内存地址结束内存地址。单位是int32 4字节

    • 如果多加几个函数,会发现stop地址值也会相应的增加
    • 此处是指从startstop前闭后开区间。[ , ),所以stop地址偏移4字节,才是最后一个函数符号地址
    • 清空打印区,点击屏幕,触发touchBegin。我们发现触发了3次guard
      image.png
    • 这3次分别是touchBegintestblock三个函数被触发时的打印

    我们在touchBegintestblock__sanitizer_cov_trace_pc_guard都加入断点,运行代码:

    image.png
    【验证一】执行顺序是:
    touchBegin -> __sanitizer_cov_trace_pc_guard ->
    test -> __sanitizer_cov_trace_pc_guard ->
    block -> __sanitizer_cov_trace_pc_guard

    【验证二】touchBegin时,进入汇编:

    image.png

    确实每个函数触发时,都调用了__sanitizer_cov_trace_pc_guard函数。

    原因:

    • 只要在Other C Flags标记,开启了trace功能。LLVM会在每个函数边缘(开始位置),插入一行调用__sanitizer_cov_trace_pc_guard的代码。编译期插入了。所以可以100%覆盖。
    • 以上,就是Clang插桩插桩操作完成后,我们需要获取所有函数符号存储导出order文件

    3. 获取函数符号

    • __builtin_return_address: return的地址。

    函数return,是返回到上一层函数

    • 通过return的地址,拿到的是上一层级函数信息
    • 参数: 0: 表示当前函数的上一层1:是上一层上一层地址。
    • 导入#import <dlfcn.h>,通过Dl_info拿到函数信息:
    typedef struct dl_info {
            const char      *dli_fname;     /* 文件地址*/
            void            *dli_fbase;     /* 起始地址(machO模块的虚拟地址)*/
            const char      *dli_sname;     /* 符号名称 */
            void            *dli_saddr;     /* 内存真实地址(偏移后的真实物理地址) */
    } Dl_info;
    
    • __sanitizer_cov_trace_pc_guard函数加入代码:
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        
        if(!*guard) return;
        void *PC = __builtin_return_address(0); //0 当前函数地址, 1 上一层级函数地址
        Dl_info info; // 声明对象
        dladdr(PC, &info); // 读取PC地址,赋值给info
        printf("dli_fname:%s \n dli_fbase:%p \n dli_sname:%s \n dli_saddr:%p \n ", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
        
    }
    
    • 运行程序,可以看到:
    image.png
    • dli_fname: 文件地址
    • dli_fbase: 起始地址(machO模块的虚拟地址)
    • dli_sname: 符号名称
    • dli_saddr: 内存真实地址(偏移后的真实物理内存地址)
    • 此时,我们成功拿到函数符号

    4.存储符号

    注意:__sanitizer_cov_trace_pc_guard函数是在多线程环境下,所以需要注意写入安全

    • 写入安全,就是上锁。 可参考【第二十八、第二十九节】,此处我使用OSAtomic原子锁
    • 存储方式,也有很多种, 此处我使用队列进行存储
    • 导入#include <libkern/OSAtomic.h>原子头文件,创建原子队列,定义节点结构体
    #import "ViewController.h"
    #include <stdint.h>
    #include <stdio.h>
    #include <sanitizer/coverage_interface.h>
    #import <dlfcn.h>
    #import <libkern/OSAtomic.h> // 原子操作
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    +(void)load {}
    
    void (^block)(void) = ^{ printf("123"); };
    
    void test666() { block(); }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    }
    
    // 定义原子队列
    static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; // 原子队列初始化
    
    // 定义符号结构体
    typedef struct {
        void * pc;
        void * next;
        
    }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;
    }
    
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        
        // 这里是多线程,会有资源抢夺。
        // 这个会影响load函数,所以需要移除哨兵
    //    if(!*guard) return;
        
        void *PC = __builtin_return_address(0); //0 当前函数地址, 1 上一层级函数地址
        Dl_info info; // 声明对象
        dladdr(PC, &info); // 读取PC地址,赋值给info
        
        // 创建结构体
        SYNode * node = malloc(sizeof(SYNode)); // 创建结构体空间
        *node = (SYNode){PC, NULL}; // node节点的初始化赋值(pc为当前PC值,NULL为next值)
        
        // 加入结构 (offsetof: 按照参数1大小作为偏移值,给到next)
        // 拿到并赋值
        // 拿到symbolList地址,偏移SYNode字节,将node赋值给symbolList最后节点的next指针。
        OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
        
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        // 创建可变数组
        NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    
        // 每次while循环,都会加入一次hook (__sanitizer_cov_trace_pc_guard)   只要是跳转,就会被block
        // 直接修改[other c clang]: -fsanitize-coverage=func,trace-pc-guard 指定只有func才加Hook
        while (1) {
    
            // 去除链表
            SYNode * node =  OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
    
            if(node ==NULL) break;
            
            Dl_info info = {0};
            // 取出节点的pc,赋值给info
            dladdr(node->pc, &info);
    
            // 释放节点
            free(node);
    
            // 存名字
            NSString *name = @(info.dli_sname);
            // 三目运算符 写法
            BOOL isObjc = [name hasPrefix: @"+["] || [name hasPrefix: @"-["];
            NSString * symbolName = isObjc ? name : [NSString stringWithFormat:@"_%@",name];
            [symbolNames addObject:symbolName];
    
        }
        
        // 反向集合
        NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
        
        // 创建数组
        NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    
        // 临时变量
        NSString * name;
        
        // 遍历集合,去重,添加到funcs中
        while (name = [enumerator nextObject]) {
            // 数组中去重添加
            if (![funcs containsObject:name]) {
                [funcs addObject:name];
            }
        }
    
        // 移除当前touchesBegan函数 (跟启动无关)
        [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    
        // 数组转字符串
        NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    
        // 文件路径
        NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ht.order"];
    
        // 文件内容
        NSData * fielContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        
        // 创建文件
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:fielContents attributes:nil];
    
        NSLog(@"%@",funcs);
        NSLog(@"%@",filePath);
        NSLog(@"%@",fielContents);
    }
    @end
    

    坑点:

    1. if(!*guard) return;需要去掉,会影响+load写入

    2. while循环,也会触发__sanitizer_cov_trace_pc_guard
      【现象】:

      image.png

    【原因】:

    • 通过看汇编,可以看到while也触发了__sanitizer_cov_trace_pc_guard的跳转。原因是,trace触发,并不是根据函数来进行hook的,而是hook每一个跳转(bl)
    • while也有跳转,所以进入了死循环

    【方案】:

    • Build SettingsOther C Flags 配置,添加一个func指定条件: -fsanitize-coverage=func,trace-pc-guard
      image.png
    • 运行代码点击屏幕

      image.png
    • 根据打印路径,查看ht.order文件,完美!

      image.png

    真机的沙盒文件,可以从这里下载:

    • 选择设备,点击Add ...
      image.png
    • 选择真机 -> 选择APP -> 点击设置
      image.png
    • 点击下载,就可以拿到手机沙盒信息了
      image.png
    • 包内容中,可以找到ht.order文件
    • 复制ht.order文件,放到根目录,就完成了。
      image.png

    可以根据 上一节的内容,打开link Map查看最终符号排序,使用Instruments检查自己应用的PageFault数量耗时

    注意

    1. 【二进制重排order文件】需要代码封版后再生成。 (代码还在变动,生成就没意义了)
    2. 【二进制重排相关代码不要写到自己项目中去。写个小工具跑一下,拿到order文件即可。

    5. Swift二进制重排

    • Swift 二进制重排,与OC一样。只是LLVM前端不同
    • OC前端编译器Clang,所以在other c flags处添加-fsanitize-coverage=func,trace-pc-guard
    • Swift前端编译器Swift,所以在other Swift Flags处添加-sanitize=undefined-sanitize-coverage=func
      image.png
    • 项目中添加SwiftTest.swift文件,创建桥接头:

      image.png
      image.png
    • ViewController.m中导入桥接头文件:#import "TranceDemo-Swift.h"

      image.png
    • 运行项目,点击屏幕,去打印目录下,拿到ht.order文件:

      image.png

    补充:

    1 . swift符号自带名称混淆

    1. 未改变代码时,swift符号不会变。
      总之,order文件,请在代码封版后,再生成
    • 至此,Clang插桩自动生成Order文件,都已完成。 去实战试试吧!

    相关文章

      网友评论

        本文标题:OC底层原理三十四:启动优化(Clang插桩)

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