一、栈的基础概念
栈是一种数据结构,只能在”一端”进行”插入”和”删除”的线性表。其中能进行插入和删除的这一端就是“栈顶”,另外一端称为“栈底”。
栈的模型
程序员说的栈经常默认指的是内存中的栈,但栈是一个基础的数据结构,不局限在计算机,其他领域都可以见到栈的应用。
比如码头对货柜箱的存储,货车来了,一个一个箱子叠起来就是在入栈,船来了,一个个箱子从上往下搬走,就是在出栈,地面就是栈底,最高的位置就只栈顶。
二、栈在内存如何布局
上面是常规的栈的模型,栈顶在上面,栈底在下面。但是在内存中,栈的布局刚好“相反”,栈顶在下面,栈底在下面。
内存有N个GB大小,以字节为单位进行划分,为了方便寻址,按顺序给每一个字节编号,这个编号我们称为内存地址。地址数值小的我们称为低地址0x000000001,数值大的们称为高地址0xc00000000。
而栈在内存中是从高地址到低地址的,一般用图来描述内存地址布局,高地址会在上面,低地址会在下面,所以从图中看栈底在上面,栈顶在下面。
注:栈为什么在内存中是从高地址到低地址生长(思考题)
栈的内存布局模型
三、调用栈
什么是调用栈?
一个线程里的函数调用的序列,使用栈的数据结构存储,这就是调用栈。 首现要理解线程的概念。线程是操作系统能够调度的最小的执行体,它是一个单一顺序的控制流,CPU在同一时间只会处理一个线程的任务。
CPU支持多线程处理,每次切换线程要先切换线程的上下文。上下文包括寄存器的数据,堆栈中存储的数据。包括局部变量、当前执行的代码地址、中间变量等。
CPU要执行的线程任务,可以理解为按顺序调用的方法树,方法里面会嵌套子方法,子方法就是树的子节点。
iOS运行时,如果出现Crash,可以通过 “[NSThread callStackSymbols]”抓取当前线程的调用栈。
Thread 17 Crashed:
0 libGPUSupportMercury.dylib 0x000000022d873fe4 _gpus_ReturnNotPermittedKillClient
1 AGXGLDriver 0x0000000231f21ed8 0x0000000231efd000 + 151256
2 libGPUSupportMercury.dylib 0x000000022d874fac _gpusSubmitDataBuffers
这个调用栈是方法的回溯,序号0是发生异常的最后一个方法,序号1是调用序号0的方法,依次类推往回溯源。有了回溯的线程调用栈,我们就可以排查Crash的源头在哪里。
线程调用栈在内存中如何布局
上面的线程调用栈,是由高级语言(OC、C、C++、swift)的函数调用符号(Symbol)组成。在高级语言层面分析,我们可以定位到80%Crash的原因。
而20%的疑难杂症需要进入到汇编层面来分析,因此我们要掌握CPU在执行一个线程任务时,调用栈在内存是如何布局的。
我们不需要考虑多线程,多线程切换会伴随上线文的切换,同一时间只需要考虑一个线程调用栈。
注:iOS程序只需要掌握ARM64汇编
注:ARM64汇编(知识点)
OC函数使用LLVM编译成ARM汇编语言后,是多个汇编指令。Arm汇编以函数为单位来布局指令,调用另外一个函数使用跳转指令bl,函数执行完要使用ret指令返回上一个函数。
CPU在执行指令时,会将一个函数映射会一个栈桢(stack frame),栈桢相当于一个嵌套在当前线程调用栈中的子栈,FP寄存器指向栈桢的栈底,SP寄存器指向栈桢的栈顶。
栈桢布局模型
栈桢(Stack Frame)
Stack Frame是一个按照方法调用顺序, 从栈的高地址向低地址依次存放的一组数据, 用于存放上一次方法调用的关键信息.它是一个函数所使用的stack的一部分,所有函数的stack frame串起来就组成了一个完整的栈。
参考
ARM64 Function Calling Conventions
Procedure Call Standard for the ARM 64-bit Architecture (AArch64)