iOS如何记录堆栈信息?(二)

4,055 阅读6分钟

上一次介绍了函数调用栈的原理,由此我们可以得知,每一次LR寄存器FP寄存器压入栈中,那么我们就可以拿到栈帧层层递归获取整个函数调用栈的情况

首先先要获取到_STRUCT_MCONTEXT machineContext ,其结构里面有__ss,__ss当中能够拿到LRFPSP等寄存器

// 首先初始化 
 _STRUCT_MCONTEXT machineContext;
bool fillThradStateContext(thread_t thread, _STRUCT_MCONTEXT *machineContext){
    mach_msg_type_number_t state_count = JY_THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(thread, JY_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}

拿到对应的寄存器LRPCFP

 // pc寄存器
    const uintptr_t pcRegister = machineContext.__ss.JY_INSTRUCTION_ADDRESS;
    if (pcRegister == 0) {
        return @"Fail to get pc address";
    }
    
    // LR寄存器,函数返回地址.用于递归符号化堆栈
    uintptr_t lrRegister;
#if defined(__i386__) || defined(__x86_64__)
    lrRegister =  0;
#else
    lrRegister =  machineContext.__ss.__lr;
#endif
     
    // 拿到FP栈帧指针,指向函数的起始地址
    const uintptr_t fpRegister = machineContext.__ss.JY_FRAME_POINTER;
    

紧接着开始递归拿到函数堆栈

// 初始化一个长度为StackMaxDepth的buffer
    uintptr_t backtraceBuffer[StackMaxDepth];
    int i = 0;
    // 首先把pc寄存器放进去,知道当前地址在哪
    backtraceBuffer[i++] = pcRegister;
    
    // 接着开始初始化结构体 构建栈帧
    JYStackFrame frame = {(void *)fpRegister, lrRegister};
    vm_size_t len = sizeof(frame);
​
    // 开始递归
    while (frame.fp && i < StackMaxDepth) {
        backtraceBuffer[i++] = frame.lr;
        bool flag = readFPMemory(frame.fp, &frame, len);
        if (!flag || frame.fp==0 || frame.lr==0) {
            break;
        }
    }

readFPMemory:读取fp开始,len(16)字节长度的内存。sp fp, lr... , fp占8字节,然后紧接着上面8字节是lr

bool readFPMemory(const void *fp, const void *dst, const vm_size_t len)
{
    vm_size_t bytesCopied = 0;
    kern_return_t kr = vm_read_overwrite(mach_task_self(), (vm_address_t)fp, len, (vm_address_t)dst, &bytesCopied);
    return KERN_SUCCESS == kr;
}

经过递归已经收集好函数堆栈信息了,现在就要开始拿到指令集开始恢复符号了

// 收集好所有的lr 开始恢复符号表
restoreSymbol(backtraceBuffer,i,thread).copy;

定义struct,用于记录符号信息等

typedef struct{
    uint64_t address; // 基础地址
    uint64_t offset;  // 偏移地址
    const char * symbol; // 符号
    const char * machOName; // 对应的二进制Macho名字
} JYFuncInfo;
​
​
typedef struct{
    JYFuncInfo *stacks;
    int allocLenght;
    int length;
} JYCallStackInfo;

现在开始来还原,一开始做一下初始化操作

//还原符号表
NSString * restoreSymbol(uintptr_t *backtraceBuffer, int length ,thread_t thread){
    
    JYCallStackInfo * csInfo = malloc(sizeof(JYCallStackInfo));
    if (csInfo == NULL) {
        return @"fail to malloc";
    }
    csInfo->length = 0;
    csInfo->allocLenght = length;
    csInfo->stacks =  (JYFuncInfo *)malloc(sizeof(JYFuncInfo) * csInfo ->allocLenght);
    if (csInfo->stacks == NULL) {
        return @"error";
    }
    callStackOfSymbol(backtraceBuffer, length, csInfo);
    NSMutableString *strM = [NSMutableString stringWithFormat:@"\n 🔥🔥🔥JYCallStack of thread: %u 🔥🔥🔥\n", thread];
    for (int j = 0; j < csInfo->length; j++) {
        [strM appendFormat:@"%@", formatFuncInfo(csInfo->stacks[j])];
    }
    freeMemory(csInfo);
    return strM.copy;
}

来到关键代码 callStackOfSymbol(backtraceBuffer, length, csInfo);

void callStackOfSymbol(uintptr_t *backtraceBuffer, int length ,JYCallStackInfo *csInfo){
    // 循环之前我们拿到的堆栈数据 开始恢复每一条指令
    for (int i = 0; i<length; i++) {
        // 获取当前lr地址 在程序加载的all image当中找到 image
        JYMachHeader * machHeader = getLrInMach(backtraceBuffer[i]);
        if (machHeader) {
            //在image中找到这个LR符号
            findSymbolInMach(backtraceBuffer[i],machHeader,csInfo);
        }
    }
}

首先我们backtraceBuffer[i]当中除了第一条是pc指令,其实都是lr寄存器指令的值

那么我们首先要知道,在运行时,有很多个image,我们需要获取到所有的image,因为image当中包含了ASLR,名称,头等

void getMachHeader(void){
    // 开辟空间
    machHeaderArr = (JYMachHeaderArr *)malloc(sizeof(JYMachHeaderArr));
  
    //_dyld_image_count 获取所有的image的数量
    machHeaderArr->allocLength = _dyld_image_count();
​
    // 获取第一个image的基址
//    intptr_t  base_addr = _dyld_get_image_vmaddr_slide(0);
​
    
    //image当中的
    machHeaderArr->array = (JYMachHeader *)malloc(sizeof(JYMachHeader) * machHeaderArr->allocLength);
    for (uint32_t i = 0; i < machHeaderArr->allocLength; i++) {
        JYMachHeader *machHeader = &machHeaderArr->array[i];
        
        //获取image的头
        machHeader->header = _dyld_get_image_header(i);
        
        //获取image的名称
        machHeader->name = _dyld_get_image_name(i);
        
        //获取进程中单个image加载的Slide值
        // Slide 代表默认在内存中加载的基地址
        machHeader->slide = _dyld_get_image_vmaddr_slide(i);
    }
}

拿到了所有的image存到machHeaderArr当中,我们现在就可以开始查找指令是在哪一个image当中了

// 在machO中找到header
JYMachHeader *getLrInMach(uintptr_t lr)
{
    if (!machHeaderArr) {
    // 获取所有的image镜像文件加入到machHeaderArr,machHeaderArrm里面放的都是header
        getMachHeader();
    }
    
    // 开始循环所有的image,确定当前的指令是在哪个image当中
    for (uint32_t i = 0; i < machHeaderArr->allocLength; i++) {
        // 拿到每个image的header
        JYMachHeader *machHeader = &machHeaderArr->array[i];
        
        // 开始查找lr寄存器中的指令是在哪一个image当中
        if (backtraceBufferItemInMach(lr-machHeader->slide, machHeader->header)) {
            // 找到在哪个image当中 然后返回 对应的machHeader
            return machHeader;
        }
    }
    return NULL;
}

来到backtraceBufferItemInMach这个函数,我们首先通过header拿到当前的Load Commands的地址,Load Commands的结构如下图,开始遍历Load Commands

bool backtraceBufferItemInMach(uintptr_t slideLR, const struct mach_header *header)
{
    // 向下偏移1个mach_header长度也就是 Load Commands的位置
    // cur 也就是  Load Commands的位置
    uintptr_t cur = (uintptr_t)(((struct mach_header_64*)header) + 1);
    
    // 遍历loadCommands,确认lr是否落在当前image的某个segment中.
   
    // 开始循环 ncmds: loadcommands的数量.
    for (uint32_t i = 0; i < header->ncmds; i++) {
       
        // 将Load Commands的起始位置开始赋值给command
        struct load_command *command = (struct load_command *)cur;
       
         // 判断command 类型是否为 LC_SEGMENT_64, 使用结构体segment_command_64
        if (command->cmd == LC_SEGMENT_64) {
            // 将command换成结构体 segment_command_64 的格式
            struct segment_command_64 *segmentCommand = (struct segment_command_64 *)command;
           
            // command的起始位置
            uintptr_t start = segmentCommand->vmaddr;
            
            // command 的 起始位置 + command的大小 得到  开始start 和 结束end 的位置
            uintptr_t end = segmentCommand->vmaddr + segmentCommand->vmsize;
            
            // 然后开始判断我们存在数组中的数据 是否存在于 这个区间 存在则返回
            if (slideLR >= start && slideLR <= end) {
                // 如果LR的地址落在这个模块里,则返回映像索引号
                return true;
            }
        }
        
#warning TODO
        // 如果command 类型为 LC_SEGMENT,则需要使用结构体segment_command
       
        
        // command地址是连续的,移动到下一个command的位置
        cur = cur + command->cmdsize;
    }
    return false;
}

我们找到这条指令在哪一个image当中,回到callStackOfSymbol,我们现在该去当前的这个image当中找到我们的符号findSymbolInMach(backtraceBuffer[i],machHeader,csInfo);这也是最关键的一步

首先我们需要了解一下MachO的结构,Symbol TableString Table之间的关系,以及LC_SYMTAB段__LINKEDIT段的作用,网上有很多讲解原理,可以自行了解下

__LINKEDIT段包含动态链接器使用的原始数据,如符号、字符串和重定位表项。

LC_SYMTAB 描述了字符串表和符号表在__LINKEDIT中的位置

首先我们还是通过imageheader拿到Load Commands,然后开始循环,找到LC_SYMTAB段__LINKEDIT段

我们backtraceBuffer当中lr的地址

  • seg_linkedit->vmaddr = LINKEDIT虚拟地址
  • seg_linkedit->fileoff= LINKEDIT的文件地址
  • (uintptr_t)machHeader->slide = ASLR
  • lr的偏移地址 =lr真实地址 - ASLR

拿到__LINKEDIT的基地址

segment加载进内存的基地址 = ASLR + LINKEDIT虚拟地址 - LINKEDIT的文件地址

符号表真实地址 = 符号表的虚拟地址 + symoff偏移地址 因为我们的lr真实地址 只是一条指令地址,它应该大于等于这个函数的入口地址,也就是对应符号的值,我们应该遍历所有符号表条目 找到距离lr最近的那个函数入口地址 才是最准确的,遍历所有的 Symbol Tabel获取所有的 symbol.n_value 然后与 lr的偏移地址做比较得到一个最小的值

stringTable字符串表 + symtab[best].n_un.n_strx(获取符号名在字符表中的偏移地址,best 代表符号表中的栏目第几个) ,两者的结果也就获取了符号的地址,然后读取值

得到符号名

void findSymbolInMach(uintptr_t lr, JYMachHeader * machHeader, JYCallStackInfo * csInfo){
    
    if (!machHeader) {
        return;
    }
    
    //  用于保存__LINKEDIT段的结构体 __LINKEDIT段包含动态链接器使用的原始数据,如符号、字符串和重定位表项。
    struct segment_command_64 * seg_linkedit = NULL;
    
    // 用于保存LC_SYMTAB Command的信息 里面有符号表的信息
    struct symtab_command * sym_command = NULL;
    
    // machO的header
    const struct mach_header * header = machHeader->header;
    
    // 向下偏移1个mach_header长度也就是 Load Commands的位置
    // cur 也就是  Load Commands的位置
    uintptr_t cur = (uintptr_t)(((struct mach_header_64*)header) + 1);
    
    // 遍历Load Commands,找到 LC_SYMTAB 段
    for (uint32_t i = 0; i<header->ncmds; i++) {
        
        // 将Load Commands的起始位置开始赋值给command
        struct load_command * command = (struct load_command*)cur;
      
        if (command->cmd == LC_SEGMENT_64) {
            struct segment_command_64 * segmentCommand = (struct segment_command_64 *)command;
        
            // 我们需要找到__LINKEDIT段 也就是 SEG_LINKEDIT
            if (strcmp(segmentCommand->segname, SEG_LINKEDIT) == 0) {
                seg_linkedit = segmentCommand;
            }
        
        }else if (command->cmd == LC_SYMTAB){
            /*
             LC_SYMTAB 描述了string表和symbol表在__LINKEDIT中的位置
             而symbol表描述了符号的地址信息,以及符号对应的字符串(函数名)在string表中   的位置
             */
        
            sym_command = (struct symtab_command*)command;
        }
        
        // command地址是连续的,移动到下一个command的位置
        cur = cur + command->cmdsize;
    }
    
    // 非空判断
    if (!seg_linkedit || !sym_command) {
        return;
    }
    
 
    // segment加载进内存的基地址 =  ASLR + LINKEDIT虚拟地址 - LINKEDIT的文件地址
    uintptr_t linkedit_base = (uintptr_t)machHeader->slide + seg_linkedit->vmaddr - seg_linkedit->fileoff;
    
    // 符号表真实地址 = 符号表的虚拟地址  + symoff偏移地址
    struct nlist_64 *symbolTable = (struct nlist_64 *)(linkedit_base + sym_command->symoff);
    
    // 字符串表对应的位置
    const uintptr_t stringTable = linkedit_base + sym_command->stroff;
    
    uintptr_t slideLR = lr - machHeader->slide;
     
    uint64_t offset = UINT64_MAX;
    
    int best = -1;
    
    // 遍历所有符号,找到与LR最近的那个. symtabCmd->nsyms指示了符号表的条目
    for (uint32_t i = 0; i < sym_command->nsyms; i++) {
        
        // 找到距离最近的那一个符号    lr偏移地址 -  符号的地址 = 得到两者之间的距离 distance
        uint64_t distance = slideLR - symbolTable[i].n_value;
        if (slideLR >= symbolTable[i].n_value && distance <= offset) {
            offset = distance;
            best = i;
        }
    }
                    
    
    if (best >= 0) {
        JYFuncInfo *funcInfo = &csInfo->stacks[csInfo->length++];
        funcInfo->machOName = machHeader->name;
        funcInfo->address = symbolTable[best].n_value;
        funcInfo->offset = offset;
        
        // 去字符串表中寻找对应的符号名称,
        // symtab[best].n_un.n_strx 获取符号名在字符表中的偏移地址
        funcInfo->symbol = (char *)(stringTable + symbolTable[best].n_un.n_strx);
        
        // 去掉下划线
        if (*funcInfo->symbol == '_')
        {
          // char里储存的是0~255的数,然后呢,显示出来是字符(按照Ascii表)。
          //  ++ --是对数字来运算的 所以 这里就是去掉下划线而已
            funcInfo->symbol++;
        }
        if (funcInfo->machOName == NULL) {
            funcInfo->machOName = "";
        }
    }
}
​

最后就是一些收尾工作了,将所有收集到的指令都找到函数入口并且成功恢复符号,这也是大佬所写的BSBacktraceLogger的全部原理