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

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文件,都已完成。 去实战试试吧!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容