iOS优化 - 通过clang插桩收集APP启动时期的符号

362 阅读3分钟

接上一片文章

iOS优化 - Clang静态插桩实践

上一篇演示了实现静态插桩步骤和原理,这片文章基于静态插桩来实现如何拿到App启动时期的所有符号(函数);

理论性的内容不写了,直接上代码说明:

思路

关键代码

void *PC = __builtin_return_address(0); 

它的作用其实就是当前这个函数(__sanitizer_cov_trace_pc_guard)执行完毕后需要返回到哪里去,*PC指向的就是需要返回的符号地址;

//伪代码
void methodOne() {
	//插桩代码
    __sanitizer_cov_trace_pc_guard()
    ==>
    //原函数逻辑代码
    NSLog(@"%s",__FUNCTION__);
}

也就是说,拿到被clang插桩的函数的符号地址,在这里就是methodOne;

根据函数内存地址获取函数名称

  • 引入头文件 #import <dlfcn.h>#import <libkern/OSAtomic.h>,其中有一个方法:
typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符号名称 */
        void            *dli_saddr;     /* 函数起始地址 */
} Dl_info;

//这个函数能通过函数内部地址找到函数符号
int dladdr(const void *, Dl_info *);
  • 在demo中做如下修改:
- (void)buttonAction {
    //遍历出队
    while (true) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        printf("%s \n",info.dli_sname);
    }
}

//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //调用 load() 方法时 , __sanitizer_cov_trace_pc_guard 函数的参数 guard 是 0,如果要对load()进行入队,就需要要屏蔽掉这行代码!
    //if (!*guard) return;  // Duplicate the guard check.
    void *PC = __builtin_return_address(0);
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

考虑到 __sanitizer_cov_trace_pc_guard这个方法会被调用多次 , 使用锁会影响性能,这里使用苹果底层的原子队列 ( 底层实际上是个栈结构 , 利用队列结构 + 原子性来保证顺序 ) 来实现,所以上述代码引入了头文件 #import <libkern/OSAtomic.h>

还没结束,如果上述代码直接运行的话会导致死循环,原因是:

通过汇编会查看到 一个带有 while 循环的方法 , 会被静态加入多次 __sanitizer_cov_trace_pc_guard 调用 , 导致死循环.

解决方法:

修改 Other C Flags

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

仅针对func进行hook;

对比使用-fsanitize-coverage=trace-pc-guard生成的汇编代码,如图:

调用了三次__sanitizer_cov_trace_pc_guard;

再看下使用-fsanitize-coverage=func,trace-pc-guard生成的汇编代码,如图:

仅调用了一次__sanitizer_cov_trace_pc_guard;

重新运行,控制台输出如下: (ps:如果再点击一次按钮,只会输出一句-[ViewController buttonAction] ,这是因为之前的内容已经从队列里移除掉了,只有-[ViewController buttonAction] 是刚加入队列的!)

swift工程/混编工程的兼容处理

以上方式适合纯 OC 工程获取符号方式 . 搜索 Other Swift Flags , 添加两条配置即可 :

-sanitize-coverage=func

-sanitize=undefined

cocoapod工程的兼容处理

在相应的target中做相应处理即可,其它不变;

手动导入库的兼容处理

会使用主工程的配置,无需特殊处理;

后续如果需要将以上符号输出到order文件中来实现二进制重排,还需要一些细化操作,比如对输出符号的顺序取反、去重和函数写法调整,这个比较简单就不写了~

Demo

参考:

参照原作者的讲解,然后经过自己完全消化吸收后,做了一些实践操作和记录!