栈
栈:是一种具有特殊的访问方式的存储空间,具有后进先出的特性(
Last In Out Firt
,LIFO
)
SP和FP寄存器
sp
寄存器:在任意时刻会保存栈顶的地址(栈的开口方向)fp
寄存器:也称为x29
寄存器,属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址(有局部变量且嵌套调用的时候)注意:
ARM64
开始,取消32位
的LDM
、STM
、PUSH
(入栈)、POP
(出栈)指令。 取而代之的是ldr\ldp
、str\stp
32位
模式下,空栈时栈顶和栈底指向同一个地方。当数据入栈,栈顶指针跟随向上移动。数据出栈,栈顶指针跟随向下移动但是在
ARM64
中,栈的开口方向是向下的,由高地址到低地址。对栈的操作是16字节
对齐存储数据前先拉伸栈,也就是开辟栈空间,然后再往里面存储数据。使用完毕后恢复栈平衡,里面存储的数据不需要回收,因为下次开辟栈空间后,新数据会直接覆盖
栈空间的拉伸,在代码编译过程中,由编译器决定,将局部变量、参数等放入栈区
死循环和死递归的区别
- 死循环:如果死循环内没有开辟任何空间,不会造成程序崩溃
- 死递归:死递归将不断开辟栈空间,最终因为堆栈溢出导致程序崩溃
堆栈溢出(
Stack Overflow
)
- 栈的开口方向是向下的,而堆区是向上的。当栈区与堆区边界碰撞,就会造成堆栈溢出
函数调用栈
常见的函数调用开辟和恢复的栈空间
sub sp, sp, #0x40 ; 拉伸0x40(64字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ; 栈平衡
ret
- 使用
sub
指令,减一个地址,相当于开辟栈空间- 使用
add
指令,加一个地址,相当于恢复栈平衡- 写数据时,必须先拉伸栈,有了栈空间后,才能存放
关于内存读写指令
读/写数据是都是往高地址读/写
str
(store register
)指令
- 将数据从寄存器中读出来,存到内存中
ldr
(load register
)指令
- 将数据从内存中读出来,存到寄存器中
ldr
和str
的变种指令,ldp
和stp
,可以操作2
个寄存器案例:
开辟
32字节
作为这段程序的栈空间。利用栈将x0
、x1
寄存器中的值进行交换搭建
Demo
项目
创建
asm.s
文件,写入以下代码:``` .text .global _A
_A: sub sp, sp, #0x20 mov x0, #0xa0 mov x1, #0xb0 stp x0, x1, [sp,#0x10] ldp x1, x0, [sp,#0x10] add sp, sp, #0x20 ret
> - `sub sp, sp, #0x20`:开辟栈空间,`sp`拉伸`32字节`栈空间
> - `mov x0, #0xa0`:将`#0xa0`写入`x0`寄存器
> - `mov x1, #0xb0`:将`#0xb0`写入`x1`寄存器
> - `stp x0, x1, [sp,#0x10]`:将`x0`、`x1`寄存器的值,写入到`sp`向上偏移`16字节`的内存地址中
> - `ldp x1, x0, [sp,#0x10]`:读取`sp`向上偏移`16字节`后内存中的值,写入到`x1`、`x0`寄存器,相当于交换
> - `add sp, sp, #0x20`:将拉伸后的`sp`加`32字节`,恢复栈平衡
> - `ret`:返回
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
int A(void);
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; A(); }
@end
> 真机运行项目,使用断点单步调试,来到`A`函数<br />

> 单步调试,向下执行`1`步。开辟栈空间,拉伸`32字节`<br />

> - 拉伸后,`sp`指向地址`0x000000016d250fd0`
> 向下执行`2`步,将`#0xa0`写入`x0`寄存器,将`#0xb0`写入`x1`寄存器<br />

> 向下执行`1`步,将`x0`、`x1`寄存器的值,写入到`sp`向上偏移`16字节`的内存地址中<br />

> 单步调试,向下执行`1`步。读取`sp`向上偏移`16字节`后内存中的值,写入到`x1`、`x0`寄存器,相当于交换<br />

> - `x0`、`x1`寄存器的值交换,但内存的数据并没有发生任何变化
> - 内存充当临时变量的作用
> 向下执行`1`步,将拉伸后的`sp`加`32字节`,恢复栈平衡<br />

> - 恢复栈平衡,`sp`指向地址`0x000000016d250ff0`
> - 内存中的数据依然存在,它们并不需要被回收。当下一轮栈空间开辟后,新数据会将其覆盖
#####bl和ret指令
> #####bl
> - `bl 地址`
> - 将下一条指令的地址放入`lr`(`x30`)寄存器
> - 转到标号处执行指令
> #####ret
> - 默认使用`lr`(`x30`)寄存器的值,通过底层指令提示`CPU`此处作为下条指令地址
> `ARM64`平台的特色指令,它面向硬件做了优化处理
> #####x30寄存器
> - `x30`寄存器存放的是函数的返回地址,当`ret`指令执行时刻,会寻找`x30`寄存器保存的地址值
> 在函数嵌套调用的时候,需要将`x30`入栈
> 案例1:
> 演示`lr`寄存器,在函数嵌套调用时的作用
> 延用上述`Demo`案例
> 打开`asm.s`文件,写入以下代码:
>
.text .global _A,_B
_A: sub sp, sp, #0x20 bl _B add sp, sp, #0x20 ret
_B: ret
> - `A`函数:拉伸栈空间
> - 跳转`B`函数
> - 恢复栈平衡
> - `B`函数:直接返回
> 真机运行项目,来到`viewDidLoad`方法<br />

> - 即将执行的指令是`bl 0x104bee9bc`,也就是跳转到`A`函数
> - `bl`指令的特性,一旦执行,先将下一条指令地址`0x104bee644`放入`lr`寄存器,然后进行跳转
> 单步调试,向下执行`1`步。跳转到`A`函数<br />

> - 打印`lr`寄存器的值,已经赋值为`0x104bee644`
> 向下执行`1`步,开辟栈空间<br />

> - 即将执行的又是`bl`指令
> - 一旦执行,将下一条指令地址`0x104bee9c4`放入`lr`寄存器,然后进行跳转`B`函数
> 向下执行`1`步,跳转到`B`函数<br />

> - 打印`lr`寄存器的值,已经赋值为`0x104bee9c4`
> - 即将执行`B`函数中的`ret`指令
> - `ret`指令的特性,使用`lr`寄存器的值作为下条指令地址。即:跳转至`0x104bee9c4`
> 向下执行`1`步,顺利回到了`A`函数的`0x104bee9c4`指令地址<br />

> 向下执行`1`步,恢复栈平衡<br />

> - 即将执行的是`A`函数的`ret`指令
> - 按照正常逻辑,应该跳转回`viewDidLoad`方法的`0x104bee644`指令地址
> - 但是,打印`lr`寄存器的值,保存的依然是`A`函数的`0x104bee9c4`指令地址
> 向下执行`1`步,产生死循环,又回到`A`函数的`0x104bee9c4`指令地址<br />

> 此时取消断点,让程序继续运行。程序会在`add sp, sp, #0x20`和`ret`两句指令上,循环往复的执行,从而形成一个死循环
> 上述问题的产生,牵扯到`lr`寄存器的现场保护
> `lr`寄存器保存的相当于`回家的路`。函数嵌套调用过程中,在`bl`到另一个函数前,必须保护好当前的`lr`寄存器,否则函数返回势必出现问题
> 如果当前函数为叶子函数,里面没有其他函数的调用,例如案例中的`B`函数,则无需保护
> 当`bl`指令执行,`lr`寄存器会被替换,如何对它进行现场保护?
> 能否将`lr`寄存器的值,存储在其他寄存器中?
> - 肯定是`不行`的。因为寄存器的数量有限,在后续的函数嵌套调用中,有可能被任意函数覆盖掉,这种做法是不安全的
> 正确的作法是什么?我们参考`llvm`编译器生成的汇编代码,看看它是如何对`lr`寄存器现场保护的
> 案例2:
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void D(void){ }
void C(void){ D(); }
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; C(); }
@end
> 真机运行项目,使用断点单步调试,来到`C`函数<br />

> - `stp x29, x30, [sp, #-0x10]!`:该指令完成两个功能,先将`sp`减`16字节`,开辟栈空间,等同于`sub sp, sp, #0x10`指令。然后将`x29`、`x30`寄存器写入到内存
> - `ldp x29, x30, [sp], #0x10`:该指令同样完成两个功能,先读取内存中的值,写入`x29`、`x30`寄存器。然后将`sp`加`16字节`,恢复栈平衡,等同于`add sp, sp, #0x10`指令
> 单步调试,向下执行`1`步。开辟栈空间,同时`x29`、`x30`寄存器的值写入内存<br />

> - 当前`lr`寄存器的值为`0x000000010081263c`
> 向下执行`2`步,进入`B`函数,同时`lr`寄存器的值被覆盖<br />

> - 当前`lr`寄存器的值被覆盖为`0x00000001008125ec`
> 向下执行`2`步,回到`A`函数。先读取内存中的值,写入`x29`、`x30`寄存器,然后恢复栈平衡<br />

> - 当前`lr`寄存器的值恢复为`0x000000010081263c`
> 向下执行`1`步,成功`ret`到`viewDidLoad`方法的`0x10081263c`指令地址<br />

> 由此可见,编译器在开辟栈空间后,先将`x29`、`x30`寄存器保存在当前函数栈中。然后在`ret`指令前,读取内存中的值,写入`x29`、`x30`寄存器。最后恢复栈平衡,`ret`回到正确的指令地址
> 将数据入栈保护的行为,统称:`现场保护`
> 案例3:
> 按上述`llvm`编译器的作法,重新修改`案例1`的汇编代码
> 打开`asm.s`文件,写入以下代码:
>
.text .global _A,_B
_A: sub sp, sp, #0x10 stp x29, x30, [sp] bl _B ldp x29, x30, [sp] add sp, sp, #0x10 ret
_B: ret
> 真机运行项目,来到`viewDidLoad`方法<br />

> - 下一条指令地址`0x10211a63c`
> 单步调试,向下执行`3`步。跳转到`A`函数,开辟栈空间,将`x29`、`x30`寄存器写入到内存<br />

> - 下一条指令地址`0x10211a9c0`
> 向下执行`1`步,跳转到`B`函数<br />

> - `lr`寄存器被覆盖为`0x10211a9c0`
> 向下执行`2`步,回到`A`函数。读取内存中的值,写入`x29`、`x30`寄存器<br />

> 向下执行`2`步,恢复栈平衡,成功`ret`到`viewDidLoad`方法的`0x10211a63c`指令地址<br />

> 上述案例中,将简写指令拆解,以便理解:
> `stp x29, x30, [sp, #-0x10]!`:
> - `sub sp, sp, #0x10`
> - `stp x29, x30, [sp]`
> `ldp x29, x30, [sp], #0x10`:
> - `ldp x29, x30, [sp]`
> - `add sp, sp, #0x10`
> 案例4:
> 如果只有一个`x30`寄存器需要现场保护,开辟`16字节`栈空间过于浪费,只开辟`8字节`可以吗?
> 打开`asm.s`文件,将栈空间的开辟和恢复都改为`8字节`<br />

> - 当前`sp`寄存器的地址为`0x000000016d54cff0`
> 单步调试,向下执行`1`步。开辟栈空间,将`x30`寄存器入栈保护<br />

> - `sp`寄存器的地址为`0x000000016d54cfe8`
> 向下执行`2`步,先跳转到`B`函数,又返回到`A`函数<br />

> 截止到`此时此刻`,一切都是`正常`的
> 向下执行`1`步,从栈中取值写入`x30`寄存器,恢复栈平衡<br />

> - 异常出现了。在`ARM64`中,对栈的操作是`16字节`对齐。所以栈空间的开辟,必须为`16字节`的倍数
> 此处还有一个细节,上述代码使用简写指令,`x30`寄存器成功写入内存。但如果将指令拆解,在写入时就会报出异常<br />

#####函数的参数和返回值
> - 在`ARM64`中,函数的参数是存放在`x0`至`x7`这`8`个寄存器里面。如果超过`8`个参数,就会入栈
> - 函数的返回值是放在`x0`寄存器里面
> 案例1:
> 查看编译器是如何传递参数并计算返回的
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
int sum(int a, int b){ return a + b; }
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; sum(10, 20); }
@end
> 真机运行项目,来到`viewDidLoad`方法<br />

> - 传递给`sun`函数的`10`、`20`两个参数,分别写入`w0`、`w1`两个寄存器
> 单步调试,向下执行`4`步。跳转到`sun`函数,开辟`16字节`栈空间,将`w0`、`w1`寄存器入栈保护<br />

> - 相当于函数内存储了两个局部变量
> 向下执行`2`步,从内存中取值,写入`w8`、`w9`寄存器<br />

> 向下执行`1`步,将`w8`、`w9`两个寄存器的值相加,赋值给`w0`寄存器<br />

> - 由于`CPU`无法直接对内存中的数据进行计算操作,故此先将内存数据写入寄存器,然后进行相加,赋值给`x0`寄存器
> - `x0`寄存器之前存储的是参数,此刻被结果覆盖为`0x000000000000001e`
> 案例2:
> 使用汇编代码实现`sum`函数
> 打开`asm.s`文件,写入以下代码:
>
.text .global _sum
_sum: add x0, x0, x1 ret
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
int sum(int a, int b);
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; printf(“sum:%d”,sum(10, 20)); }
@end
> 真机运行项目,来到`viewDidLoad`方法<br />

> - 传递给`sun`函数的`10`、`20`两个参数,分别写入`w0`、`w1`两个寄存器
> 单步调试,向下执行`2`步。跳转到`sun`函数,直接将`x0`、`x1`寄存器进行相加,将结果赋值给`x0`<br />

> 打印结果:`sum:30`<br />

> 案例3:
> 当函数超过`8`个参数,编译器是如何传递的?
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
@implementation ViewController
int test(int a, int b, int c, int d, int e, int f, int g, int h, int i){ return a + b + c + d + e + f + g + h + i; }
- (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,test(1, 2, 3, 4, 5, 6, 7, 8, 9)); }
@end
> - 注释`[super viewDidLoad]`方法,减少生成的汇编代码,避免干扰
> 真机运行项目,来到`viewDidLoad`方法<br />

> - 对`bl`指令前的汇编代码进行分析
> - `sub sp, sp, #0x30`:开辟`48字节`栈空间
> - `stp x29, x30, [sp, #0x20]`:现场保护,将`x29`、`x30`在`sp + #0x20`位置入栈
> - `add x29, sp, #0x20`:将`sp + #0x20`位置设为栈底
> - `stur x0, [x29, #-0x8]`:将`x0`在`fp - #0x8`位置入栈。`stur`指令:把寄存器的值(`32位`)写进内存
> - `str x1, [sp, #0x10]`:将`x1`在`sp + #0x10`位置入栈
> - `mov w0, #0x1`~`mov w7, #0x8`:将前`8`个参数,分别写入到`w0`~`w7`
> - `mov x8, sp`:将`sp`所在位置,写入到`x8`
> - `mov w9, #0x9`:将第`9`个参数,写入到`w9`
> - `str w9, [x8]`:将`w9`入栈到`x8`所存储的`sp`位置
> `bl`指令执行前`View Memory`中的内存数据<br />

> `bl`指令执行前的栈图<br />

> 继续执行代码,跳转到`test`函数<br />

> - 对`add sp, sp, #0x30`指令前的汇编代码进行分析
> - `sub sp, sp, #0x30`:开辟`48字节`栈空间
> - `ldr w8, [sp, #0x30]`:将`sp + #0x30`位置的值写入`w8`。`sp + #0x30`是`viewDidLoad`方法调用栈的位置,这里写入的是`w9`的值,也就是第`9`个参数的值
> - `str w0, [sp, #0x2c]`~`str w8, [sp, #0xc]`:现场保护,将`w0`至`w8`的`9`个寄存器分别入栈
> - `ldr w8, [sp, #0x2c]`:将`sp + #0x2c`位置的值写入`w8`,也就是第`1`个参数的值
> - `ldr w9, [sp, #0x28]`:将`sp + #0x28`位置的值写入`w9`,也就是第`2`个参数的值
> - `add w8, w8, w9`:`w8`、`w9`相加,将结果赋值给`w8`
> - 依次从内存中将后续`6`个参数值写入`w9`,然后和`w8`相加,将结果赋值给`w8`
> - `add w0, w8, w9`:最后一个参数的相加,`w8`、`w9`相加,将结果赋值给`w0`
> `add sp, sp, #0x30`指令执行前`View Memory`中的内存数据<br />

> `add sp, sp, #0x30`指令执行前的栈图<br />

> 打印结果:`sum:45`<br />

> 上述案例中,函数参数超过`8`个,超出的参数不再使用寄存器传递,而是直接入栈。当下一个函数使用时,从上一个函数调用栈中读取
> 从栈中读取数据效率并不高,所以在开发中,应避免函数超过`8`个参数
> - `C`函数:最好不要超过`8`个参数
> - `OC`方法:最好不要超过`6`个参数。因为`objc_msgSend(id self, SEL _cmd, ...)`自身还有两个隐含参数
> 案例4:
> 上述案例,使用`Release`模式运行,编译器会如何优化?
> 选择`Release`模式运行<br />

> 真机运行项目,来到`viewDidLoad`方法<br />

> - 在`viewDidLoad`方法调用栈中,经过编译器优化只剩下少量代码,甚至连`test`函数的调用也被优化掉了。编译器直接使用`mov w8, #0x2d`指令,将计算结果`45`写入`x8`寄存器
> 案例5:
> 使用汇编代码实现带参数的函数嵌套调用
> 打开`asm.s`文件,写入以下代码:
>
.text .global _funcA,_sum
_funcA: stp x29, x30, [sp, #-0x10]! bl _sum ldp x29, x30, [sp], #0x10 ret
_sum: add x0, x0, x1 ret
> - `_funcA`函数嵌套调用`_sum`函数,需要现场保护
> - `_sum`函数直接使用`add x0, x0, x1`指令进行相加,结果写入`x0`
> 更简单的实现方式,将`bl`替换为`b`指令
>
.text .global _funcA,_sum
_funcA: b _sum
_sum: add x0, x0, x1 ret
> - `b`指令:用于不返回的跳转,仅跳转到标号处,不改变`lr`寄存器的值
> - `b`指令常用于破解的地方,可以绕过代码执行
> 案例6:
> 返回值一般是`8字节`指针。如果返回一个结构体,大小超过`8字节`,编译器会如何处理?
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
@implementation ViewController
struct str { int a; int b; int c; int d; int f; int g; };
struct str getStr(int a,int b,int c,int d,int f,int g){ struct str str1; str1.a = a; str1.b = b; str1.c = d; str1.d = d; str1.f = f; str1.g = g; return str1; }
- (void)viewDidLoad { // [super viewDidLoad]; struct str str2 = getStr(1, 2, 3, 4, 5, 6); }
@end
> 真机运行项目,来到`viewDidLoad`方法<br />

> - `sub sp, sp, #0x40`:开辟`64字节`栈空间
> - `stp x29, x30, [sp, #0x30]`:现场保护,将`x29`、`x30`在`sp + #0x30`位置入栈
> - `add x29, sp, #0x30`:将`sp + #0x30`位置设为栈底
> - `stur x0, [x29, #-0x8]`:将`x0`在`fp - #0x8`位置入栈
> - `stur x1, [x29, #-0x10]`:将`x1`在`fp - #0x10`位置入栈
> - `add x8, sp, #0x8`:将`x8`指向`sp + #0x8`位置
> `bl`指令执行前的栈图<br />

> 继续执行代码,跳转到`getStr`函数<br />

> - `sub sp, sp, #0x20`:开辟`32字节`栈空间
> - `str w0, [sp, #0x1c]`~`str w5, [sp, #0x8]`:现场保护,将`w0`至`w5`的`6`个寄存器分别入栈
> - `ldr w9, [sp, #0x1c]`:将`sp + #0x1c`位置的值写入`w9`,也就是第`1`个参数的值
> - `str w9, [x8]`:将`w9`入栈到`x8`所存储的位置,`x8`存储的是`viewDidLoad`方法调用栈的位置
> - 后续逻辑同上,使用`w9`依次获取参数值,写入到`viewDidLoad`方法调用栈中
> - 最后`add sp, sp, #0x20`恢复栈平衡并`ret`
> `add sp, sp, #0x20`指令执行前的栈图<br />

> 上述案例,当返回值超过`8字节`,则不再使用`x0`寄存器作为返回值,而是将返回的数据写入到上一个函数调用栈中
> 所以在开发中,应避免返回的数据超过`8字节`。如果函数需要返回结构体,可以将返回类型定义为结构体指针,将大小控制在`8字节`,这样可以使用`x0`寄存器传递,效率会更高
>
struct str getStr(int a,int b,int c,int d,int f,int g){ struct str str1 = malloc(24); str1 -> a = a; str1 -> b = b; str1 -> c = d; str1 -> d = d; str1 -> f = f; str1 -> g = g; return str1; }
#####函数的局部变量
> 函数的局部变量放在栈里面
> 案例1:
> 编译器如何存储函数的局部变量?
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
@implementation ViewController
int funcB(int a, int b){ int c = 6; return a + b + c; }
- (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,funcB(10, 20)); }
@end
> 真机运行项目,来到`funcB`方法<br />

> - `mov w8, #0x6`:`#0x6`的值为`6`,相当于局部变量`c`,将其写入`x8`寄存器
> - `str w8, [sp, #0x4]`:将`w8`寄存器入栈
> - 相加时,先从栈中取值,写入`w9`寄存器,然后运算
> - 恢复栈平衡后,函数调用栈中的局部变量就不存在了
> 案例2:
> 编译器如何处理函数嵌套调用时的局部变量、参数和返回值?
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
@implementation ViewController
int funcB(int a, int b){ int c = 6; int d = sumD(a, b, c); int e = sumD(a, b, c); return d + e; }
int sumD(int a, int b, int c){ int d = a + b + c; return d; }
- (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,funcB(10, 20)); }
@end ```
真机运行项目,来到
funcB
方法
sub sp, sp, #0x30
:开辟48字节
栈空间stp x29, x30, [sp, #0x20]
:现场保护,将x29
、x30
入栈add x29, sp, #0x20
:设置栈底stur w0, [x29, #-0x4]
:将w0
入栈,即:第1
个参数stur w1, [x29, #-0x8]
:将w1
入栈,即:第2
个参数mov w8, #0x6
:将#0x6
写入w8
,即:局部变量c
stur w8, [x29, #-0xc]
:将w8
入栈,即:局部变量c
ldur w0, [x29, #-0x4]
~ldur w2, [x29, #-0xc]
:从栈中读取两个参数和局部变量c
的值,分别写入w0
~w2
bl 0x102c925c4
:调用sumD
函数str w0, [sp, #0x10]
:sumD
函数使用w0
作为返回值,将其入栈,即:局部变量d
ldur w0, [x29, #-0x4]
~ldur w2, [x29, #-0xc]
:再次从栈中读取两个参数和局部变量c
的值,分别写入w0
~w2
bl 0x102c925c4
:再次调用sumD
函数str w0, [sp, #0xc]
:sumD
函数依然使用w0
作为返回值,将其入栈,即:局部变量e
ldr w8, [sp, #0x10]
:从栈中读取局部变量d
的值,写入w8
ldr w9, [sp, #0xc]
:从栈中读取局部变量e
的值,写入w9
add w0, w8, w9
:将w8
、w9
相加,结果赋值给w0
ldp x29, x30, [sp, #0x20]
:恢复x29
、x30
的值add sp, sp, #0x30
:恢复栈平衡ret
:返回
add sp, sp, #0x30
指令执行前的栈图
总结
栈
- 栈:存储空间,具有后进先出的访问方式
sp
寄存器:在任意时刻会保存我们栈顶的地址fp
寄存器:也称x29
寄存器,属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址- 在
ARM64
中,栈是递减栈,由高地址向低地址延伸。对栈的操作是16字节
对齐的栈的读写指令
- 读:
ldr
(load register
)指令- 写:
str
(store register
)指令ldr
和str
的变种指令,ldp
和stp
,可以操作2
个寄存器简写指令:
stp x0, x1, [sp, #-0x10]!
- 数据入栈,一般从栈底开始存入。所以使用简写的条件是,不需要额外的栈空间,存入的数据刚好放满
- 简写的执行顺序,一定是先开辟空间,再入栈
- 等同于:
sub sp, sp, #0x10
:拉伸16字节
栈空间
stp x0, x1, [sp]
:在sp
所在位置存放x0
和x1
bl
指令
- 跳转指令:
bl 地址
。表示程序执行到标号处。将下一条指令的地址保存到lr
寄存器b
:代表跳转l
:代表lr
(x30
)寄存器
ret
指令
- 类似函数中的
return
- 让
CPU
执行lr
寄存器所指向的指令- 当函数嵌套调用时,需要现场保护。
lr
寄存器入栈函数嵌套调用
- 会将
x29
、x30
寄存器入栈保护函数的参数
- 在
ARM64
中,参数是放在x0
至x7
的8
个寄存器中- 如果是浮点数,使用浮点寄存器
- 如果超过
8
个参数就会用栈传递函数的返回值
- 默认情况下,函数的返回值放在
x0
寄存器- 如果返回值大于
8字节
,就会利用内存,写入上一个调用栈的内部,用x8
寄存器作为参照函数的局部变量
- 使用栈保存局部变量