本文由云+社区发表html
iOS开发过程当中不免会遇到卡顿等性能问题或者死锁之类的问题,此时若是有调用堆栈将对解决问题颇有帮助。那么在应用中如何来实时获取函数的调用堆栈呢?本文参考了网上的一些博文,讲述了使用mach thread的方式来获取调用栈的步骤,其中会同步讲述到栈帧的基本概念,而且经过对一个demo的汇编代码的讲解来方便理解获取调用链的原理。api
先抛出一个栈帧的概念,解释下什么是栈帧。架构
应用中新建立的每一个线程都有专用的栈空间,栈能够在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间,那么问题就来了,函数运行过程当中会有很是多的入栈出栈的过程,当函数返回backtrace的时候怎样能精肯定位到返回地址呢?还有子函数所保存的一些寄存器的内容?这样就有了栈帧的概念,即每一个函数所使用的栈空间是一个栈帧,全部的栈帧就组成了这个线程完整的栈。ide
下面再抛出几个概念:函数
寄存器中的fp,sp,lr,pc。工具
寄存器是和CPU联系很是紧密的一小块内存,常常用于存储一些正在使用的数据。对于32位架构armv7指令集的ARM处理器有16个寄存器,从r0到r15,每个都是32位比特。调用约定指定他们其中的一些寄存器有特殊的用途,例如:性能
不一样指令集的寄存器数量可能会不一样,pc、lr、sp、fp也可能使用其中不一样的寄存器。后面咱们先忽略r11等寄存器编号,直接用fp,sp,lr来说述学习
以下图所示,不论是较早的帧,仍是调用者的帧,仍是当前帧,它们的结构是彻底同样的,由于每一个帧都是基于一个函数,帧伴随着函数的生命周期一块儿产生、发展和消亡。在这个过程当中用到了上面说的寄存器,fp帧指针,它老是指向当前帧的底部;sp栈指针,它老是指向当前帧的顶部。这两个寄存器用来定位当前帧中的全部空间。编译器须要根据指令集的规则当心翼翼地调整这两个寄存器的值,一旦出错,参数传递、函数返回均可能出现问题。ui
其实这里这几个寄存器会知足必定规则,好比:spa
而由此咱们能够进一步想到,经过sp和fp所指出的栈帧能够恢复出母函数的栈帧,不断递归恢复便恢复除了调用堆栈。向下面代码同样,每次递归pc存储的*(fp + 1)其实就是返回的地址,它在调用者的函数内,利用这个地址咱们能够经过符号表还原出对应的方法名称。
while(fp) {
pc = *(fp + 1);
fp = *fp;
}
复制代码
若是你非要问为何会这样,咱们能够从汇编角度看下函数是怎么调用的,从而更深入理解为何fp老是存储了上一个栈帧的fp的地址,而fp向前一个地址为何老是lr?
写以下一个demo程序,因为我是在mac上作实验,因此直接使用clang来编译出可执行程序,而后再用hopper工具反汇编查看汇编代码,固然也可直接使用clang的
-S
参数指定生产汇编代码。
demo源码
#import <Foundation/Foundation.h>
int func(int a);
int main (void)
{
int a = 1;
func(a);
return 0;
}
int func (int a)
{
int b = 2;
return a + b;
}
复制代码
汇编语言
; ================ B E G I N N I N G O F P R O C E D U R E ================
; Variables:
; var_4: -4
; var_8: -8
; var_C: -12
_main:
0000000100000f70 push rbp
0000000100000f71 mov rbp, rsp
0000000100000f74 sub rsp, 0x10
0000000100000f78 mov dword [rbp+var_4], 0x0
0000000100000f7f mov dword [rbp+var_8], 0x1
0000000100000f86 mov edi, dword [rbp+var_8] ; argument #1 for method _func
0000000100000f89 call _func
0000000100000f8e xor edi, edi
0000000100000f90 mov dword [rbp+var_C], eax
0000000100000f93 mov eax, edi
0000000100000f95 add rsp, 0x10
0000000100000f99 pop rbp
0000000100000f9a ret
; endp
0000000100000f9b nop dword [rax+rax]
; ================ B E G I N N I N G O F P R O C E D U R E ================
; Variables:
; var_4: -4
; var_8: -8
_func:
0000000100000fa0 push rbp ; CODE XREF=_main+25
0000000100000fa1 mov rbp, rsp
0000000100000fa4 mov dword [rbp+var_4], edi
0000000100000fa7 mov dword [rbp+var_8], 0x2
0000000100000fae mov edi, dword [rbp+var_4]
0000000100000fb1 add edi, dword [rbp+var_8]
0000000100000fb4 mov eax, edi
0000000100000fb6 pop rbp
0000000100000fb7 ret
复制代码
须要注意,因为是在mac上编译出可执行程序,指令集已是x86-64,因此上文的fp、sp、lr、pc名称和使用的寄存器发生了变化,但含义基本一致,对应关系以下:
接下来咱们看下具体的汇编代码,能够看到在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,而后递归去查找母函数的地址,最后将其符号化,便可还原出调用栈。
总结概括了下,获取调用栈须要下面几步:
thread_suspend(main_thread);
复制代码
_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
复制代码
#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
复制代码
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);
复制代码
上面代码便会从下到上依次打印出调用栈函数中的地址,这个地址老是在函数调用地方的下一个地址,咱们就须要拿这个地址还原出对应的符号名称。
thread_resume(main_thread);
复制代码
这一步主要是将已经得到的调用链上的地址分别解析出对应的符号。主要是参考了运行时获取函数调用栈 的方法,其中用到的dyld连接mach-o文件的基础知识,后续会专门针对这里总结一篇文章。
enumerateSegment(header, [&](struct load_command *command) {
if (command->cmd == LC_SYMTAB) {
struct symtab_command *symCmd = (struct symtab_command *)command;
uint64_t baseaddr = 0;
enumerateSegment(header, [&](struct load_command *command) {
if (command->cmd == LC_SEGMENT_64) {
struct segment_command_64 *segCmd = (struct segment_command_64 *)command;
if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) {
baseaddr = segCmd->vmaddr - segCmd->fileoff;
return true;
}
}
return false;
});
if (baseaddr == 0) return false;
nlist_64 *nlist = (nlist_64 *)(baseaddr + slide + symCmd->symoff);
uint64_t strTable = baseaddr + slide + symCmd->stroff;
uint64_t offset = UINT64_MAX;
int best = -1;
for (int k = 0; k < symCmd->nsyms; k++) {
nlist_64 &sym = nlist[k];
uint64_t d = pcSlide - sym.n_value;
if (offset >= d) {
offset = d;
best = k;
}
}
if (best >= 0) {
nlist_64 &sym = nlist[best];
std::cout << "SYMBOL: " << (char *)(strTable + sym.n_un.n_strx) << std::endl;
}
return true;
}
return false;
});
复制代码
此文已由做者受权腾讯云+社区在各渠道发布
获取更多新鲜技术干货,能够关注咱们腾讯云技术社区-云加社区官方号及知乎机构号