需要注意,由于是在mac上编译出可执行程序,指令集已经是x86-64,所以上文的fp、sp、lr、pc名称和使用的寄存器发生了变化,但含义基本一致,对应关系如下:
fp----rbp
sp----rsp
pc----rip
接下来我们看下具体的汇编代码,可以看到在main函数中在经过预处理和参数初始化后,通过call _func来调用了func函数,这里call _func其实等价于两个汇编命令:
Pushl %rip //保存下一条指令(第41行的代码地址)的地址,用于函数返回继续执行 Jmp _func //跳转到函数foo于是,当main函数调用了func函数后,会将下一行地址push进栈,至此,main函数的栈帧已经结束,然后跳转到func的代码处开始继续执行。可以看出,rip指向的函数下一条地址,即上文中所说的lr已经入栈,在栈帧的顶部。
而从func的代码可以看到,首先使用push rbp将帧指针保存起来,而由于刚跳转到func函数,此时rbp其实是上一个栈帧的帧指针,即它的值其实还是上一个栈帧的底部地址,所以此步骤其实是将上一个帧底部地址保存了下来。
下一句汇编语句mov rbp, rsp将栈顶部地址rsp更新给了rbp,于是此时rbp的值就成了栈的顶部地址,也是当前栈帧的开始,即fp。而栈顶部又正好是刚刚push进去的存储上一个帧指针地址的地址,所以rbp指向的时当前栈帧的底部,但其中保存的值是上一个栈帧底部的地址。
至此,也就解释了为什么fp指向的地址存储的内容是上一个栈帧的fp的地址,也解释了为什么fp向前一个地址就正好是lr。
另外一个比较重要的东西就是出入栈的顺序,在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。
三、获取调用栈步骤其实上面的几个fp、lr、sp在mach内核提供的api中都有定义,我们可以使用对应的api拿到对应的值。如下便是64位和32位的定义
_STRUCT_ARM_THREAD_STATE64 { __uint64_t __x[29]; /* General purpose registers x0-x28 */ __uint64_t __fp; /* Frame pointer x29 */ __uint64_t __lr; /* Link register x30 */ __uint64_t __sp; /* Stack pointer x31 */ __uint64_t __pc; /* Program counter */ __uint32_t __cpsr; /* Current program status register */ __uint32_t __pad; /* Same size for 32-bit or 64-bit clients */ }; _STRUCT_ARM_THREAD_STATE { __uint32_t r[13]; /* General purpose register r0-r12 */ __uint32_t sp; /* Stack pointer r13 */ __uint32_t lr; /* Link register r14 */ __uint32_t pc; /* Program counter r15 */ __uint32_t cpsr; /* Current program status register */ };于是,我们只要拿到对应的fp和lr,然后递归去查找母函数的地址,最后将其符号化,即可还原出调用栈。
总结归纳了下,获取调用栈需要下面几步:
1、挂起线程 thread_suspend(main_thread); 2、获取当前线程状态上下文thread_get_state _STRUCT_MCONTEXT ctx; #if defined(__x86_64__) mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count); #elif defined(__arm64__) _STRUCT_MCONTEXT ctx; mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count); #endif 3、获取当前帧的帧指针fp #if defined(__x86_64__) uint64_t pc = ctx.__ss.__rip; uint64_t sp = ctx.__ss.__rsp; uint64_t fp = ctx.__ss.__rbp; #elif defined(__arm64__) uint64_t pc = ctx.__ss.__pc; uint64_t sp = ctx.__ss.__sp; uint64_t fp = ctx.__ss.__fp; #endif 4、递归遍历fp和lr,依次记录lr的地址 while(fp) { pc = *(fp + 1); fp = *fp; }这一步我们其实就是使用上面的方法来依次迭代出调用链上的函数地址,代码如下
void* t_fp[2]; vm_size_t len = sizeof(record); vm_read_overwrite(mach_task_self(), (vm_address_t)(fp),len, (vm_address_t)t_fp, &len); do { pc = (long)t_fp[1] // lr总是在fp的上一个地址 // 依次记录pc的值,这里先只是打印出来 printf(pc) vm_read_overwrite(mach_task_self(),(vm_address_t)m_cursor.fp[0], len, (vm_address_t)m_cursor.fp,&len); } while (fp);