iOS落魄的iOS开发

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

2019-01-18  本文已影响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),从而获得整个线程的调用栈`。

函数调用栈原理

指令指针

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

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

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

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

函数出入栈过程

函数调用栈内部布局.png
从图中可以看出,函数调用时入栈顺序为:
实参N-1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1-N 。
注意:内存地址降序

函数定义

入栈过程

出栈过程

函数调用地址获取

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

线程函数地址获取小结

代码逻辑解析

流程图

image.png
task_threads(mach_task_self(), &threads, &thread_count)
//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)
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)
// 位于系统库 头文件中 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结构体将函数虚拟地址,与函数名进行关联。

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

上一篇下一篇

猜你喜欢

热点阅读