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

如何做一个方法级别启动耗时检查工具来辅助分析和监控

使用hook objc_msgSend 方式来检查启动方法的执行耗时时,我们需要实现一个称手的启动时间检查工具

首先,需要了解为什么hookobjc_msgSend 方法,就可以hook 全部Objective-C的方法

Objective-C里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由selector函数指针metadata组成的

objc_msgSend方法在运行时根据对象和方法的selector去找到对应的函数指针,然后执行。换句话说,objc_msgSendObjective-C里方法执行的必经之路,能够控制所有的Objective-C方法

objc_msgSend本身是用汇编语言写的,这样做的原因主要有两个:

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

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

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特定的部分中的指针来绑定lazynon-lazy符号,通过确认传递给rebind_symbol里每个符号更新的位置,就可以找出对应替换来重新绑定这些符号。

####fishhook的实现原理
首先,遍历dyld里的所有image, 取出image headerslide


        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;
        }
    }

复制代码

然后,获得baseindirect符号表:


   //找到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_msgSendhook了吗?

当然不够,objc_msgSend是用汇编语言实现的,所以我们还需要从汇编层多加点料

需要先实现两个方法pushCallRecordpopCallRecord,来分别记录objc_msgSend方法调用前后的时间,然后相减就能够得到方法的执行耗时。

下面针对arm64架构,编写一个可保留未知参数并跳转到c中任意函数指针的汇编代码,实现对objc_msgSend的hook。

arm643164 bit 的整数型寄存器,分别用x0x30表示,主要的实现思路是:

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

具体汇编代码如下:



复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享