美文网首页iOS落魄的iOS开发
iOS堆栈信息解析(函数地址与符号关联)

iOS堆栈信息解析(函数地址与符号关联)

作者: 龙猫六六 | 来源:发表于2019-01-18 16:34 被阅读77次

    任务Mach-Task

    描述:一个机器无关的thread的执行环境抽象
    作用:task可以理解为一个进程,包含它的线程列表
    结构体:
    task_threads
    task_threads将traget_task任务下的所有线程保存在act_list数组中,数组个数为act_listCnt

    kern_return_t task_threads
    (
      task_t traget_task,
      thread_act_array_t *act_list,                     //线程指针列表
      mach_msg_type_number_t *act_listCnt  //线程个数
    )
    

    thread_info线程信息

    kern_return_t thread_info
    (
      thread_act_t target_act,
      thread_flavor_t flavor,
      thread_info_t thread_info_out,
      mach_msg_type_number_t *thread_info_outCnt
    );
    

    如何获取线程的堆栈数据
    1.所有线程:调用内核API函数task_threads获取指定task线程列表,即act_list
    2.指定线程:调用API函数thread_info获得对应线程信息thread_info
    3.线程信息:调用thread_get_state获得指定线程上下问信息_STRUCT_MCONTEXT。thread_get_stateAPI两个参数随着cpu架构不同而改变。_STRUCT_MCONTEXT结构存储当前线程栈顶指针(sp)和最顶部的栈帧指针(frame pointer),从而获得整个线程的调用栈`。

    函数调用栈原理

    指令指针

    • 指令指针IP:指令寄存器存储,指向处理器下条等待执行的指令地址(代码内的偏移量),每次执行完 IP会增加
    • 堆栈栈顶指针SP:堆栈指令寄存器存储,系统栈的栈顶地址
    • 栈帧指针FP:栈帧基址指令寄存器存储,每个栈帧都有一个对应的栈帧基地址,局部变量和函数参数都可以通过FP确定,因为它们到FP的距离不会受到压栈和出栈操作影响。

    为了访问函数局部变量,必须能定位每个变量。局部变量相对于堆栈指针SP的位置在进入函数时就已确定,理论上变量可用SP加偏移量来引用,但SP会在函数执行期随变量的压栈和出栈而变动。尽管某些情况下编译器能跟踪栈中的变量操作以修正偏移量,但要引入可观的管理开销。而且在有些机器上(如Intel处理器),用SP加偏移量来访问一个变量需要多条指令才能实现,由此设计了栈帧指针FPFP两侧分别记录函数参数,及局部变量。

    函数调用栈内部布局
    栈帧:函数(运行中且未完成)占用的一块独立的连续内存区域。
    函数调用通常是嵌套的,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

    编译器利用栈帧,使得函数参数和函数中局部变量的分配与释放对程序员透明。编译器将控制权移交函数本身之前,插入特定代码将函数参数压入栈帧中,并分配足够的内存空间用于存放函数中的局部变量。使用栈帧的一个好处是使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。

    栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。

    函数出入栈过程

    • BP栈帧指针地址:间隔被调用函数(局部变量内存空间)和调用函数(被调函数参数,调用函数地址,指令指针)
    • BP栈帧指针值:上一个栈帧的地址值,便于被调函数释放后,回到调用函数
    • BP栈帧入栈时机:函数被调用,申请内存空间来存储前一个栈帧的地址值
    函数调用栈内部布局.png
    从图中可以看出,函数调用时入栈顺序为:
    实参N-1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1-N 。
    注意:内存地址降序

    函数定义

    • caller(主调函数,紫色)
    • callee(被调函数,蓝色)

    入栈过程

    • 1.caller未调用callee,内存分布如下:
      EBP:caller EBP
      ESP:caller的LocalVariables

    • 2.caller调用callee
      callee函数的参数入栈(由caller提供)
      caller的函数地址(vm_add), EIP入栈(代码偏移量offset)。备注:代码位置=vm_add+offset

    • 3.callee栈帧指针入栈
      申请栈帧指针空间
      存储caller的栈帧指针地址

    • 4.申请callee局部变量空间
      为局部变量申请足够的内存空间
      Local Variable#1,Local Variable#2,Local Variable#3...Local Variable#n
      EBP:callee的EBP
      ESP:Local Variable#n

    出栈过程

    • 1.callee调用完毕
      callee局部变量空间释放
      EBP:callee ebp -> caller ebb
      ESP:caller ebp

    • 2.caller函数执行复原
      代码执行复原:ip+return address = 代码位置
      callee函数空间释放:Argumne #1,Argumne #2,...,Argumne #1n
      EBP:caller ebb
      ESP:caller Load Variables

    函数调用地址获取

    获取thread
    API函数task_thread获取线程数组地址线程个数
    API函数task_thread声明

    kern_return_t task_threads
    (
      task_t traget_task,
      thread_act_array_t *act_list,   //线程指针列表
      mach_msg_type_number_t *act_listCnt  //线程个数
    )
    

    使用代码

    thread_act_array_t threads;
    mach_msg_type_number_t thread_count=0;
    task_threads(mach_task_self(),  &thrads, &thread_count);
    

    thread的内存上下文
    API函数thread_get_state获取内存上下文,上下文信息存储在_struct_mcontext结构体内

    kern_return_t thread_get_state
    (
        thread_act_t target_act,  //thread
        thread_state_flavor_t flavor,
        thread_state_t old_state, 
        mach_msg_type_number_t *old_stateCnt
    );
    

    备注:target_act和old_stateCnt配套使用,与cpu类型相关

    使用代码

    bool fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT * machineContext) {
        mach_msg_type_number_t state_count = LSL_THREAD_STATE_COUNT;
        kern_return_t kr = thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
        return (kr == KERN_SUCCESS);
    }
    

    thread_get_state传入thread,_STRUCT_MCONTEXT->__ss(寄存器指针结构体),以及cpu相关常量(target_act,old_stateCnt),来实现_STRUCT_MCONTEXT赋值

    堆栈指针获取
    _STRUCT_MCONTEXT结构体获取堆栈指针
    如x86_64为_STRUCT_MCONTEXT->__ss结构体如下

    #define DETAG_INSTRUCTION_ADDRESS(A) (A)
    #define LSL_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT //thread_get_state函数参数
    #define LSL_THREAD_STATE x86_THREAD_STATE64 //thread_get_state函数参数
    #define LSL_FRAME_POINTER __rbp
    #define LSL_STACK_POINTER __rsp
    #define LSL_INSTRUCTION_ADDRESS __rip
    

    指令指针

    _STRUCT_MCONTEXT->__ss.LSL_INSTRUCTION_ADDRESS //rip 指令指针
    

    栈顶指针

    _STRUCT_MCONTEXT->__ss.LSL_STACK_POINTER  //bsp 栈顶指针
    

    栈帧指针

    _STRUCT_MCONTEXT->__ss.LSL_FRAME_POINTER  //rbp 栈帧指针
    

    栈帧结构体
    栈帧结构体StackFrameEntry

    typedef struct StackFrameEntry{
        const struct StackFrameEntry *const previous;  //前一个栈帧地址
        const uintptr_t return_address;  //栈帧的函数返回地址
    } StackFrameEntry;
    

    首个栈帧结构体赋值
    API函数vm_read_overwrite

    kern_return_t vm_read_overwrite
    (
        vm_map_t target_task,  //task任务
        vm_address_t address,  //栈帧指针FP
        vm_size_t size,  //结构体大小 sizeof(StackFrameEntry)
        vm_address_t data,  //结构体指针StackFrameEntry
        vm_size_t *outsize  //赋值大小
    );
    

    使用代码

    
    //参数src:栈帧指针
    //参数dst:StackFrameEntry实例指针
    //参数numBytes:StackFrameEntry结构体大小
    kern_return_t lsl_mach_copyMem(const void * src, const void * dst, const size_t numBytes) {
        vm_size_t bytesCopied = 0;
    //   调用api函数,根据栈帧指针获取该栈帧对应的函数地址
        return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
    }
    

    函数地址
    参考上一步,完成首个栈帧结构体赋值后
    1.通过栈帧结构体StackFrameEntry->previous,遍历所有栈帧
    2.API函数vm_read_overwrite对栈帧结构体赋值,获取当前栈帧函数
    伪代码

    //循环遍历,停止条件MAX_FRAME_NUMBER栈帧个数
        for (; idx < MAX_FRAME_NUMBER; idx++) {
     栈帧函数赋值
            backtraceBuffer[idx] = frame.return_address;
            
            if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS ||
                frame.previous == NULL ||
    //        根据当前的栈帧的previous,获取前一个栈帧地址
                lsl_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
                break;
            }
    

    线程函数地址获取小结

    • 1.找到目标thread,方法:API函数task_threads
    • 2.获得thread的内存上下文_STRUCT_CONTEXT,方法:API函数thread_get_state
    • 3.获取指针栈帧结构体_STRUCT_CONTEXT._ss,解析得到对应指令指针_STRUCT_CONTEXT._ss.ip;首次个栈帧指针_STRUCT_CONTEXT._ss.bp;栈顶指针_STRUCT_CONTEXT._ss.sp
      1. 首个栈帧结构体赋值,方法:API函数vm_read_overwrite(_STRUCT_CONTEXT._ss.bp...),完成首个栈帧结构体赋值StackFrameEntry
      1. 遍历StackFrameEntry获取所有栈帧及对应的函数地址

    代码逻辑解析

    流程图

    image.png
    • Setp1:
      调用API函数task_threads,获取线程数组栈帧threads,线程个数thread_count
    task_threads(mach_task_self(), &threads, &thread_count)
    
    • Setp2:
      调用API函数thread_get_state,实例化结构体STRUCT_MCONTEXT,STRUCT_MCONTEXT->__ss包含栈帧指针fp,指令指针ip,栈顶指针sp
    //thread:线程
    //LSL_THREAD_STATE:cpu相关的定量
    //machineContext->__ss:设备上下文,__ss结构体存储了`fp`,ip,sp
    //state_count:cpu相关的定量
    thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count)
    
    • Setp3:
      调用API函数vm_read_overwrite,实例化StackFrameEntry结构体,StackFrameEntry存储首个栈帧的函数地址,以及前一个栈帧地址从而通过遍历堆栈所有函数地址的获取
    typedef struct StackFrameEntry{
        //    前一个栈帧地址
        const struct StackFrameEntry * const previous;
        //    函数地址
        const uintptr_t return_address;
    } StackFrameEntry;
    
    //mach_task_self:task对象
    //src:fp栈帧指针
    //numBytes:sizeof(StackFrameEntry)
    //dst:StackFrameEntry指针
    //bytesCopied://cpye字节大小
    vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied)
    
    • Setp4:
      遍历StackFrameEntry(遍历条件StackFrameEntry.previous),来获取堆栈所有栈帧地址,及函数地址(add)并存储在函数地址数组backTrackBuffer

    • Setp5:
      获得函数的实现地址,由于函数地址无法进行阅读,需要通过符号表(nlist)来解析为函数名(Setp6-Setp15操作目标),从而进行程序定位。

    • Setp6:
      调用API函数_dyld_image_count(void) ,获取images文件总数,即mach-o文件总数,Setp6-Setp9遍历获取mach-o target index(目标mach-o镜像文件)。

    • Setp7:
      调用API函数_dyld_get_image_header(imageIndex)获取mach-o文件的header对象,header对象存储load command个数及大小;
      调用API函数_dyld_get_image_vmadd_slide(imageIndex)的mach-o文件的随机内存地址偏移量

    • Setp8:
      补充
      函数地址:add,函数真实的实现地址
      函数虚拟地址:vm_add
      ALSR:slide函数虚拟地址加载到进程内存的随机偏移量,每个mach-o的slide各不相同
      关系:vm_add + slide = add
      已知参数:add,slide因此通过关系换算得到vm_add

    • Setp9:
      image index:函数对应的mach-o镜像文件image索引index
      遍历:遍历mach-o下所有loadCommand(LC_SEGMENT),循环条件header->ncmds(load command个数)。
      目标:函数地址对应的mach-o镜像文件image。
      查询条件:vm_add=[image(index).segment(i).vmadd, image(index).segment(i).vmadd+image(index).segment(i).vmsize],其中index=image index,i=cmd index

    • Setp10:
      调用API函数_dyld_get_image_vmaddr_slide(index),获取目标image的slide用来换算基址。不同的mach-o的slide不同

    • Setp11:
      获得函数对应的mach-o的镜像image(index)文件后,计算程序链接基址,从而获取符号表地址symbolTab_Add,字符串表地址strTab_Add。
      base_add = segmet(LINKEDIR).vmadd - segment(LINKEDIT).fileoff + slide
      函数对应的镜像文件image(index),遍历loadcommadn,获得cmd.segname=LINKEDIT的segment,提取vmadd(虚拟地址),fileoff(文件偏移量)

    • Setp12:
      获得符号表地址
      symbolTab_add:符号表地址,一块连续的地址来存储mach-o所有的函数符号,存储结构为nlis
      base_add:程序链接时基址,通过LINKEDIT计算得到
      symoff:符号表偏移地址,存储在LC_SYMTAB的cmd中,symoff为相对基址的偏移量
      关系:symbolTab_add = base_add + symoff

    • Setp13:
      获得字符串表地址
      strTab_add:符号表地址,一块连续的地址来存储mach-o所有的字符串指针base_add:程序链接时基址,通过LINKEDIT计算得到stroff:符号表偏移地址,存储在LC_SYMTAB的cmd中,stroff为相对基址的偏移量
      关系:strTab_add = base_add + stroff

    • Setp14:
      符号表结构体nlist

    // 位于系统库 头文件中 struct nlist {
      union {
         uint32_t n_strx;  //符号名在字符串表中的偏移量
      } n_un;
      uint8_t n_type;
      uint8_t n_sect;
      int16_t n_desc; 
      uint32_t n_value; //符号在内存中的地址,类似于函数虚拟地址指针   
    };
    

    符号表以nlist的结构体连续存储mach-o文件下所有函数符号,nlist结构体将函数虚拟地址,与函数名进行关联。

    • Setp15:
      符号结构体nlist关联了函数虚拟地址和函数名(n_vaule函数虚拟地址,n_um_strx字符串表偏移量),目前已知函数地址,因此可以遍历所有的nlist获得对应的n_um_strx。
      函数虚拟地址vm_add: vm_add = add - slide
      符号表注册函数虚拟地址n_value:nlist(index).n_value
      index遍历条件:vm_add >= n_value && min(vm_add - n_value),满足上述条件的符号index即为函数对应的nlist(index)

    • Setp16:
      获得函数对应的符号表索引后,得到函数名起始地址nlist(inde).n_um.n_strx + strTab_add

    至此完成函数地址与函数名的关联~

    相关文章

      网友评论

        本文标题:iOS堆栈信息解析(函数地址与符号关联)

        本文链接:https://www.haomeiwen.com/subject/rsibdqtx.html