如何记录堆栈信息?(一)
在App卡顿以及Crash的时候,或者想要在某些特定时候记录一下堆栈信息,用于帮助开发者更好的解决问题。今天主要来分析一下BSBacktraceLogger
的工作原理
记录堆栈原理大致都是:
- 回溯栈帧
- 获得函数调用地址,
- 解析
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()
的位置处打上断点
假设当前调用栈为下图:
0x104f793a8 <+0>: sub sp, sp, #0x20 ; =0x20
sp向低地址方向移动0x20(32)
个字节,如下图
0x104f793ac <+4>: stp x29, x30, [sp, #0x10]
将x29
、x30
寄存器的值存储到当前栈顶SP + 0x10
的位置,分别保存的是c函数
的栈底和a函数
返回后的下一条指令
0x104f793b0 <+8>: add x29, sp, #0x10 ; =0x10
当前栈顶SP + 0x10
存入x29(FP)
寄存器中,相当于FP
指向了SP + 0X10
,此时FP
指向了函数a
调用栈的栈底,其中存储的是调用方c
函数的栈底
0x104f793b4 <+12>: mov w8, #0x4
该条指令是一个简单的赋值操作,w8
即x8
寄存器的低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
继续下移
0x104f79398 <+4>: mov w8, #0x3
该条指令是一个简单的赋值操作,w8
即x8
寄存器的低8位
存储常数3
-> 0x104f7939c <+8>: str w8, [sp, #0xc]
将w8
的值存储在sp + 0xc
的位置,
对比a函数
当中的赋值操作,两者存储的位置的不同
0x104f793b8 <+16>: stur w8, [x29, #-0x4]
将
w8
的值存储在x29(FP)
寄存器往下偏移4个字节
的位置
还可以发现在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
的位置读取数据,存入x29
和x30
寄存器中,从上图可以看出,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来确认调用方函数的符号。
有两种特殊情况是获取不到调用堆栈的
- 尾调用优化
- 内联函数