启动优化clang插桩(二)

3,296 阅读4分钟

一、前言

上一篇的方法给到的是个数,但不是符号,个数并没有什么作用,甚至给了全部的符号也没什么用,因为二进制重排仅仅需要的是启动阶段所需要的符号,这就需要下面这个函数:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}

添加断点,点击箭头,可以看到绿色框中的函数调用栈:

Pasted Graphic.png

函数调用栈跟之前讲到的app名称.LinkMap-normal-arm64.txt文件里面的数据格式一样。

把上面断点过掉,给touchBegan方法添加断点::

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
///
}

点击上面的绿色箭头:

Pasted Graphic 1.png

这里就出现了touchesBegan,那大概推出下面这个函数是系统每调用一个方法,都会调用这个__sanitizer_cov_trace_pc_guard函数:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}

二、调试验证

验证是否为真给下面两个函数添加打印方法:

void test(void) {
    NSLog(@"%s",__func__);
}

void (^block) (void) = ^{
    NSLog(@"%s",__func__);
};

同时在touchesBegan方法里面为添加打印和调用两个函数:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s",__func__);
    test();
    block();
}

这个追踪方法也添加打印:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    NSLog(@"%s",__func__);
}

cmd + r启动,先清空打印,点击屏幕:

TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] -[ViewController touchesBegan:withEvent:]
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] test
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] block_block_invoke

可以看出先__sanitizer_cov_trace_pc_guard 再调用方法、函数、block,这说明不管是方法、函数、block它都会去回调这个函数,而且这个函数的调用是我们在调用这个函数之前,也就是这个函数拦截或者hook了所有的方法、函数包括block, 这就搞定了我们没有其他操作就能拦截到app启动时候调用了那些方法和函数

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}

我们在程序启动时候__sanitizer_cov_trace_pc_guard拦截到的方法函数:

Pasted Graphic 3.png

把这些写入到.order文件里面这样二进制重排就搞定了

_sanitizer_cov_trace_pc_guard,这个函数是如何做到这一点的?给_sanitizer_cov_trace_pc_guard添加断点,在Xcode的Debug选择Debug WorkFlow选择显示汇编,选择main

Pasted Graphic 4.png

可以看到在main之前,系统插入了_sanitizer_cov_trace_pc_guard这个符号:

Pasted Graphic 5.png

AppDelegate页面也是插入了这个符号:

Pasted Graphic 6.png

SceneDelegate同样如此:

Pasted Graphic 7.png

也就是说,在编译器clang添加下面这个标记后,编译器会给函数方法前面都会调用_sanitizer_cov_trace_pc_guard这个函数:

Pasted Graphic 8.png

这样,我们确实在打断点看到启动阶段的所有符号。

三、如何把符号都打印出来

先打开注释的代码:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    NSLog(@"%s",__func__);
    
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
//    char PcDescr[1024];
//    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

上面的PC指的是上一个函数的地址,有了这个函数地址,就可以拿到这个函数的符号,这里需要用到dladdr函数:

#import <dlfcn.h>

dladdr(const void *, Dl_info *),这个函数可以获得一个函数的名称以及地址,第一个参数传入PC,第二个参数定义一个变量 Dl_info info传进去:

Dl_info info;
dladdr(PC, &info);

查看Dl_info,它是一个结构体:

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_sname就是所需要符号

删除其他打印,把调试代码改为如下:

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;
        NSLog(@"%d",N);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("dli_sname -- %s\n",info.dli_sname);
}

运行:

dli_sname -- main
dli_sname -- -[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate setWindow:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate scene:willConnectToSession:options:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[ViewController viewDidLoad]
dli_sname -- -[SceneDelegate sceneWillEnterForeground:]
dli_sname -- -[SceneDelegate sceneDidBecomeActive:]

得到了启动时候所需要的所有符号和启动顺序,拿到这些符号之后,把它复制粘贴到.order文件中,就可以实现之前需要的目标,就是拿到把启动所需要的符号和顺序加载在前面的page里面,就实现了二进制重排。在放进.order文件之前,需要把重复的符号删掉,并且对于函数或者block,需要在前面加个“_”,这样整个clang插桩就已经完成。

四、获取符号方式优化

但是,上面手动的方法不够灵活,应该让计算机去做这些操作,用代码完成上面的操作。 

目标:

  1. 去掉重复的符号
  2. 如果是函数就前面加上“_”
  3. 生成一个.order文件

那第一步就是要对下面的符号进行收集,然后存储起来:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    NSLog(@"%s",__func__);
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("dli_sname -- %s\n",info.dli_sname);
}

这就需要有一个全局的容器来存放,并且这里会涉及到多线程的情况,因为一个app启动不大可能只有一个线程在跑,因为函数、方法和block在哪个线程跑,这个获取符号的回调函数也在那个线程运行,所以在符号收集的时候,需要考虑线程安全问题。

所以,这边使用线程安全的原子队列,导入头文件:

#import <libkern/OSAtomic.h>

定义一个全局容器结构体:

typedef struct {
    void * pc;
    void *next;
} SYNode;

然后在回调函数里面进行操作:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
//开辟空间
    SYNode *node = malloc(sizeof(SYNode));
//赋值
    *node = (SYNode){PC,NULL};
    //结构体存入原子队列
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next ));
}

offsetof里面的next是下一个存储位置的偏移量,这样我们就把符号都放进了symbolList里面,在合适的位置把它取出来就行。