iOS如何记录堆栈信息?(一)

4,544 阅读4分钟

如何记录堆栈信息?(一)

在App卡顿以及Crash的时候,或者想要在某些特定时候记录一下堆栈信息,用于帮助开发者更好的解决问题。今天主要来分析一下BSBacktraceLogger 的工作原理

记录堆栈原理大致都是:

  1. 回溯栈帧
  2. 获得函数调用地址,
  3. 解析MachO文件获取函数名

要想理清楚回溯栈帧 ,就需要了解一下arm64上函数的调用栈工作原理

函数调用栈

首先简要介绍一下arm64知识

寄存器描述
x0-x30通用寄存器,当做32位时为W0-W30
x29(FP)当前函数栈帧的栈底位置
x30(LR)指向当前函数执行完后要执行的下一条指令
SP当前函数栈帧的栈顶,移动SP可以改变当前函数的栈帧大小
PC程序计数器,总是指向即将要执行的下一跳指令
CPSR状态寄存器
// c函数调用a函数
void b (){
    int b = 3;
}
​
void a (){
    int a = 4;
    b();
}

我们在b()的位置处打上断点

假设当前调用栈为下图:

img


0x104f793a8 <+0>: sub sp, sp, #0x20 ; =0x20

sp向低地址方向移动0x20(32)个字节,如下图

img


0x104f793ac <+4>: stp x29, x30, [sp, #0x10]

x29x30寄存器的值存储到当前栈顶SP + 0x10的位置,分别保存的是c函数的栈底和a函数返回后的下一条指令


0x104f793b0 <+8>: add x29, sp, #0x10 ; =0x10

当前栈顶SP + 0x10存入x29(FP)寄存器中,相当于FP指向了SP + 0X10,此时FP指向了函数a调用栈的栈底,其中存储的是调用方c函数的栈底

img


0x104f793b4 <+12>: mov w8, #0x4

该条指令是一个简单的赋值操作,w8x8寄存器的低8位存储常数4


0x104f793b8 <+16>: stur w8, [x29, #-0x4]

w8的值存储在x29(FP)寄存器往下偏移4个字节的位置


-> 0x104f793bc <+20>: bl 0x104f79394 ; c at ViewController.m:67

bl是带返回的跳转指令,返回的地址保存到x30(LR)寄存器中

跳转到0x104f79394来到函数b


0x104f79394 <+0>: sub sp, sp, #0x10 ; =0x10

sp继续下移

img


0x104f79398 <+4>: mov w8, #0x3

该条指令是一个简单的赋值操作,w8x8寄存器的低8位存储常数3


-> 0x104f7939c <+8>: str w8, [sp, #0xc]

w8的值存储在sp + 0xc的位置,

对比a函数当中的赋值操作,两者存储的位置的不同

0x104f793b8 <+16>: stur w8, [x29, #-0x4]

w8的值存储在x29(FP)寄存器往下偏移4个字节的位置

img

还可以发现在b函数的汇编代码中,没看到保存a函数的FP(x29)和LR(x30)的指令,猜测是因为b函数已经处于整个调用链的最后,它没有调用其他的函数,因此不需要专门记录了,只需要在执行完毕之后返回到LR的指令就可以了


0x104f793a0 <+12>: add sp, sp, #0x10 ; =0x10

sp往上移动0x10,和b函数的第一条指令对应,一个是入栈,一个是出栈,此时b函数已经完全调用完了,ret回到x30(LR)的位置,当前调用栈如图所示:

回到a函数继续执行下面

0x104f793c0 <+24>: ldp x29, x30, [sp, #0x10]

sp + 0x10的位置读取数据,存入x29x30寄存器中,从上图可以看出,sp + 0x10的位置确实存储a函数的调用方x29和x30数据,其实也就是还原现场


0x104f793c4 <+28>: add sp, sp, #0x20 ; =0x20

SP往上偏移0x20,此时a函数的调用堆栈结束,ret返回到LR(x30)的位置。


外部一个c函数调用a函数是一个入栈出栈的过程,调用开始的时候入栈,同时需要保存c函数的FP和LR,在a函数的的FP和FP+8的位置,即当前函数a的FP位置保存的就是调用方的FP位置,a函数调用结束时返回到LR的位置继续执行下一条指令,而这条指令属于c函数,因为我们可以通过FP来建立整个调用链的关系,通过LR来确认调用方函数的符号。

有两种特殊情况是获取不到调用堆栈的

  1. 尾调用优化
  2. 内联函数