iOS 底层原理 iOS 进阶之路

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

2020-11-22  本文已影响0人  markhetao

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

2.1 添加trace

image.png
#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(需要导入库),我们暂时把这一行注释掉

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

  • 如果多加几个函数,会发现stop地址值也会相应的增加
  • 此处是指从startstop前闭后开区间。[ , ),所以stop地址偏移4字节,才是最后一个函数符号地址

我们在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%覆盖。

3. 获取函数符号

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

  • 通过return的地址,拿到的是上一层级函数信息
  • 参数: 0: 表示当前函数的上一层1:是上一层上一层地址。
typedef struct dl_info {
        const char      *dli_fname;     /* 文件地址*/
        void            *dli_fbase;     /* 起始地址(machO模块的虚拟地址)*/
        const char      *dli_sname;     /* 符号名称 */
        void            *dli_saddr;     /* 内存真实地址(偏移后的真实物理地址) */
} Dl_info;
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原子锁
  • 存储方式,也有很多种, 此处我使用队列进行存储
#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

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

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

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

注意

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

5. Swift二进制重排

  • OC前端编译器Clang,所以在other c flags处添加-fsanitize-coverage=func,trace-pc-guard
  • Swift前端编译器Swift,所以在other Swift Flags处添加-sanitize=undefined-sanitize-coverage=func
    image.png

补充:

1 . swift符号自带名称混淆

  1. 未改变代码时,swift符号不会变。
    总之,order文件,请在代码封版后,再生成
上一篇下一篇

猜你喜欢

热点阅读