App启动速度监控-方法级别启动耗时检查工具

作者: 稀土掘金  更新时间:2021-05-07 10:27:25  原文链接


如何做一个方法级别启动耗时检查工具来辅助分析和监控 使用hook objc_msgSend 方式来检查启动方法的执行耗时时,我们需要实现一个称手的启动时间检查工具 首先,需要了解为什么hook 了objc_msgSend 方法,就可以hook 全部Objective-C的方法 Objective-C里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由selector、函数指针和metadata组成的 objc_msgSend方法在运行时根据对象和方法的selector去找到对应的函数指针,然后执行。换句话说,objc_msgSend是Objective-C里方法执行的必经之路,能够控制所有的Objective-C方法 objc_msgSend本身是用汇编语言写的,这样做的原因主要有两个:

objc_msgSend的调用频次最高,在它上面进行的性能优化能够提升整个App生命周期的性能。而汇编语言在性能优化上属于原子级优化,能够吧优化做到极致 其他语言难以实现未知参数跳转到任意函数指针的功能

苹果开源了objective-c的运行时代码,可以在苹果开源网站找到objc_msgSend的源码

上图列出的是所有架构的实现,包括x86_64等。objc_msgSend是iOS方法执行最核心的部门 objc_msgSend方法执行的逻辑是:先获取对象对应类的信息,再获取方法的缓存,根据方法的selector查找函数指针,经过异常错误处理后,最后跳转到对应函数的实现 hook objc_msgSend方法 Facebook开源了一个库,可以在iOS上运行的Mach-O二进制文件中动态的重新绑定符号,这个库叫fishhook : GitHub地址 fishhook实现的大致思路是,通过重新绑定符合,可以实现对c方法的hook。dyld是通过更新Mach-O二进制的_DATA segment特定的部分中的指针来绑定lazy和non-lazy符号,通过确认传递给rebind_symbol里每个符号更新的位置,就可以找出对应替换来重新绑定这些符号。 ####fishhook的实现原理 首先,遍历dyld里的所有image, 取出image header和slide

if (!_rebindings_head->next) {
        _dyld_register_func_for_add_image(_rebind_symbols_for_image);
    }else {
        uint32_t c = _dyld_image_count();
        //遍历所有image
        for (uint32_t i = 0; i < c; i++) {
            //读取 image header 和 slider
            _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
        }
    }

复制代码 接下来,找到符号表相关的command,包括linkedit segment command、symtab command 和dysymtab command

segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command * symtab_cmd = NULL;
struct dysymtab_command *dysymtab_cmd = NULL;
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
        if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
            //linkedit segment command
            linkedit_segment = cur_seg_cmd;
        }
    }else if (cur_seg_cmd->cmd == LC_SYMTAB){
        //symtab command
        symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    }else if (cur_seg_cmd->cmd == LC_DYSYMTAB){
        //dysymtab command
        dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
}

复制代码 然后,获得base和indirect符号表:

//找到base符号表地址 uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); //找到indirect符号表 uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

复制代码 最后,有了符号表和传入的方法替换数组,就可以进行符号表访问指针地址的替换了:

uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)(uintptr_t)slide + section->addr);
for (uint i = 0 ; i < section->size/sizeof(void *); i++) {
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
        continue;
    }
    
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    char *symbol_name = strtab + strtab_offset;
    if (strnlen(symbol_name,2) < 2) {
        continue;
    }
    
    struct rebindings_entry *cur = rebindings;
    while (cur) {
        for (uint j = 0; j < cur->rebindings_nel; j++) {
            if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i].replaced!= cur->rebindings[j].replacement) {
                    *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                }
                
                //符号表访问指针地址的替换
                indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                goto symbol_loop;
            }
        }
        cur = cur->next;
    }
symbol_loop:;
    
}

复制代码 以上,就是fishhook的实现原理了,fishhook是对底层的操作,其中查找符号表的过程和堆栈符号化实现原理基本类似,了解其中原理对于理解可执行文件Mach-O内部结构会有很大的帮助。 接下来,我们再看一个问题:只靠fishhook 就能够搞定objc_msgSend的hook了吗? 当然不够,objc_msgSend是用汇编语言实现的,所以我们还需要从汇编层多加点料 需要先实现两个方法pushCallRecord和popCallRecord,来分别记录objc_msgSend方法调用前后的时间,然后相减就能够得到方法的执行耗时。 下面针对arm64架构,编写一个可保留未知参数并跳转到c中任意函数指针的汇编代码,实现对objc_msgSend的hook。 arm64有 31 个 64 bit 的整数型寄存器,分别用x0到x30表示,主要的实现思路是:

入栈参数,参数寄存器是x0~x07。对应objc_msgSend方法来说,x0第一个参数是传入对象,x1第二个参数是选择器_cmd, syscall的number会放到x8里。 交换寄存器中保存的参数,将用于返回的寄存器lr中的数据移到想x1里 使用 bl label 语法调用pushCallRecord函数 执行原始的objc_msgSend,保存返回值 使用bl label 语法调用popCallRecord函数

具体汇编代码如下:

复制代码