iOS 内存分区及栈区
栈区是存放临时变量、记录函数调用的区域。
函数因为有了栈,所以才会具有递归的特点。
向低地址增长的特性
iOS 内存的栈空间是向低地址生长的,在图示中靠下的位置是栈顶,靠上方的位置是栈底:
字节序、端序、尾序
计算机硬件有两种储存数据的方式,分别为大端(big endian)和小端(little endian),当然还有混合的,在这里我们先不研究。
举例来说,数值0x2211使用两个字节储存:高位字节是0x22,低位字节是0x11。
- 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
- 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存(小端就是大端的逆序)。
再举个例子,这里有 0x100 0x101 0x102 0x103 这么一段数据,在大端跟小端的存储情况如下图:
啊。。为什么要多此一举呢?都用大端这种人能看懂的不好吗,为什么会有小端字节序呢?
其实道理很简单,计算机电路先处理低位字节,效率比较高,计算都是从低位开始的。
所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。
因此,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
iOS是小端的操作系统
寄存器
ARM64 有34个寄存器,包括31个通用寄存器、SP、PC、CPSR:
- x0-x30 64bit 通用寄存器,如果有需要可以当做32bit使用:W0-W30。其中x0 - x7:这 8 个寄存器主要用来存储传递参数 。如果参数超过 8 个,则会通过栈来传递 ;x0 也用来存放上文方法的返回值。
- FP(x29) 64bit 保存栈帧地址(栈底指针),指向当前方法栈的底部。
- LR(x30) 64bit 通常称x30为程序链接寄存器,因为这个寄存器会记录着当前方法的调用方地址 ,即当前方法调用完成时应该返回的位置。例如我们遇到 Crash 要获取方法堆栈,其本质就是不断的向上递归每一个 x30 寄存器的记录状态(也就是栈上 X30 寄存器的内容) 来找到上层调用方。
- SP 64bit 保存栈指针,使用 SP/WSP来进行对SP寄存器的访问。指向当前方法栈的顶部。
- PC 64bit 程序计数器,俗称PC指针,总是指向即将要执行的下一条指令,在arm64中,软件是不能改写PC寄存器的。
- CPSR 64bit 状态寄存器
ARM64 有 31 个通用寄存器,每个寄存器可以存取一个 64 位的数据。
我们可以通过 X0 - X30 来对这些寄存器进行寻址。
对应 X0 - X30,W0 - W30 对应的就是相同单元数的低 32 位。
W0 - W30 当进行写入操作时,会将高 32 位清零。
栈空间开辟和释放
大角度指导,如果我们不断的递归调用一个方法,就会造成 Stack Overflow 。
由于FP 始终指向当前方法的栈底,SP 始终指向当前方法的栈顶。
如果我要在不断的调用一个方法,那么SP指针就一直在上升(大家记住,iOS里上下是反的):
对应的,当我们需要释放一个区域,其实也是 SP 指针下沉的过程(上下是反的):
由于 SP 是始终指向栈顶(低地址)的,每次进行 Push 压栈的时候,其顶部指针就会向低地址偏移,而当进行弹栈 Pop 操作时,指针向高地址偏移。
在方法栈中,我们的方法就相当于一个又一个栈内元素,与之对应的就是 SP 指针的上移和下沉。
这样我们也就可以解释的通什么是Stack Overflow了,方法压栈操作执行时,在汇编层执行的是类似于 sub sp, sp, #0xN 这种操作,这个指令决定了 SP 是往低地址逐渐逼近。
所以当 SP 移动到栈区的最低位置(接近于堆区),则称之为Stack Overflow 。
函数的栈区结构
假设我们在执行main方法,此时在栈上会分配一个区域,即从 FP(main 方法的栈底)到 SP (main 方法的栈顶):
函数调用时栈区表现
基于寄存器的方法栈回溯示意:
关于 stp 指令的解释
stp 指令是 str 的变种指令,p 可以理解成 pair 的意思,可以同时操作两个寄存器。举一个例子:
stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置
这里我们需要注意的一个点,在后面对于 objc_msgSend 方法 Hook 的汇编代码中,会有这么一条指令:
stp q6, q7, [sp, #-32]!
这与上面所说的 stp 命令不太一样。我们注意到差异是在 sp 偏移寻址的标记后多了一个 ! 。
则这条命令可以等效于:
sub sp, sp, #32
stp q6, q7, [sp]
也就是说,在执行完上文的 stp 指令后,还会使 sp 寄存器也产生偏移。
如此就可以达到持续入栈的效果,类似于 ARM32 中的 push 指令。