iOS开发过程中难免会遇到卡顿等性能问题或者死锁之类的问题,此时如果有调用堆栈将对解决问题很有帮助。那么在应用中如何来实时获取函数的调用堆栈呢?本文参考了网上的一些博文,讲述了使用mach thread的方式来获取调用栈的步骤,其中会同步讲述到栈帧的基本概念,并且通过对一个demo的汇编代码的讲解来方便理解获取调用链的原理。
一、栈帧等几个概念先抛出一个栈帧的概念,解释下什么是栈帧。
应用中新创建的每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间,那么问题就来了,函数运行过程中会有非常多的入栈出栈的过程,当函数返回backtrace的时候怎样能精确定位到返回地址呢?还有子函数所保存的一些寄存器的内容?这样就有了栈帧的概念,即每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈。
栈帧下面再抛出几个概念:
寄存器中的fp,sp,lr,pc。
寄存器是和CPU联系非常紧密的一小块内存,经常用于存储一些正在使用的数据。对于32位架构armv7指令集的ARM处理器有16个寄存器,从r0到r15,每一个都是32位比特。调用约定指定他们其中的一些寄存器有特殊的用途,例如:
r0-r3:用于存放传递给函数的参数;
r4-r11:用于存放函数的本地参数;
r11:通常用作桢指针fp(frame pointer寄存器),栈帧基址寄存器,指向当前函数栈帧的栈底,它提供了一种追溯程序的方式,来反向跟踪调用的函数。
r12:是内部程序调用暂时寄存器。这个寄存器很特别是因为可以通过函数调用来改变它;
r13:栈指针sp(stack pointer)。在计算机科学内栈是非常重要的术语。寄存器存放了一个指向栈顶的指针。看这里了解更多关于栈的信息;
r14:是链接寄存器lr(link register)。它保存了当目前函数返回时下一个函数的地址;
r15:是程序计数器pc(program counter)。它存放了当前执行指令的地址。在每个指令执行完成后会自动增加;
不同指令集的寄存器数量可能会不同,pc、lr、sp、fp也可能使用其中不同的寄存器。后面我们先忽略r11等寄存器编号,直接用fp,sp,lr来讲述
如下图所示,不管是较早的帧,还是调用者的帧,还是当前帧,它们的结构是完全一样的,因为每个帧都是基于一个函数,帧伴随着函数的生命周期一起产生、发展和消亡。在这个过程中用到了上面说的寄存器,fp帧指针,它总是指向当前帧的底部;sp栈指针,它总是指向当前帧的顶部。这两个寄存器用来定位当前帧中的所有空间。编译器需要根据指令集的规则小心翼翼地调整这两个寄存器的值,一旦出错,参数传递、函数返回都可能出现问题。
其实这里这几个寄存器会满足一定规则,比如:
fp指向的是当面栈帧的底部,该地址存的值是调用当前栈帧的上一个栈帧的fp的地址。
lr总是在上一个栈帧(也就是调用当前栈帧的栈帧)的顶部,而栈帧之间是连续存储的,所以lr也就是当前栈帧底部的上一个地址,以此类推就可以推出所有函数的调用顺序。这里注意,栈底在高地址,栈向下增长
而由此我们可以进一步想到,通过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