一. 函数调用栈
1.1 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
image.png
- 传统栈空间存放数据方式:栈空间放入数据之后,栈顶指针挪一位
- ARM64中在代码编译时,就会开辟好栈空间,栈顶指针不会挪动
- 局部变量 参数 都是放在栈里面
- 开辟栈空间存放数据,栈顶方向是指向低地址
1.2 SP和FP寄存器
- sp寄存器在任意时刻会保存我们栈顶的地址.
- fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!
注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp,ARM64里面 对栈的操作是16字节对齐的!!
1.3 函数调用栈
常见的函数调用开辟和恢复栈空间
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
// 注意 add sp, sp, #0x40; 栈平衡恢复的值不需要销毁,等待下一次开辟栈空间覆盖
// 我们不能直接拿到sp寄存器的值,必须先拉伸栈空间之后,才能拿到sp寄存器的值
1.4 关于内存读写指令
注意:读/写 数据是都是往高地址读/写,下面两个指令专门操作栈空间
- str(store register)指令
将数据从寄存器中读出来,存到内存栈空间中. - ldr(load register)指令
将数据从内存栈空间中读出来,存到寄存器中
ldr 和 str 的变种stp和ldp,可以操作2个寄存器。
str ldr可以操作64位,stp ldp 可以操作128位
1.5 堆栈操作练习
现在我们回到汇编(一)中的demo,修改汇编代码如下
// asm.s文件内容
.text
.global _B
_B:
// 使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换.
sub sp, sp, #0x20 ;拉伸栈空间32个字节
// 这里要操作两个寄存器,所以使用stp
stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0,寄存器里面的值进行交换
add sp, sp, #0x20 ;栈平衡恢复栈空间32个字节
ret
// ViewController.m 中调用
@implementation ViewController
// 汇编B函数声明
int B();
- (void)viewDidLoad {
[super viewDidLoad];
B();
// Do any additional setup after loading the view.
}
@end
我们来分析下上面汇编这样写 stp x0, x1, [sp] 会不会有问题?其中sp寄存器中存放的是内存地址
- 执行sub sp, sp, #0x20之前sp寄存器指向地址
image.png
- 执行sub sp, sp, #0x20之后sp寄存器指向地址
image.png
- 执行stp x0, x1, [sp] 意味着sp寄存器往上挪一位,指向0x16b28f948
image.png
上面写法stp x0, x1, [sp]没有问题
sub sp, sp, #0x20 ;拉伸栈空间32个字节,开辟的内存空间如下图
image.png
stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1,存放地址如下图
image.png
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来放入x1 和 x0,寄存器里面的值进行交换,sp寄存器与内存数据都没有变化,变化的只是x0 x1 寄存器
运行工程打断点执行到汇编代码
执行完sub sp, sp, #0x20 ;拉伸栈空间32个字节
image.png
执行完stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
image.png
执行完ldp x1, x0, [sp, #0x10] ;
image.png
image.png
执行完add sp, sp, #0x20 ;栈平衡恢复栈空间32个字节
image.png
这个时候a b的值还在内存地址中,等待下一轮栈空间拉伸之后写入数据,覆盖掉之前的值
1.6 问题探讨
请问死循环,程序是否会崩溃?
不会开辟内存空间的时候,死循环不会崩溃,如果汇编代码写成下面这样,会发生堆栈溢出崩溃
.text
.global _A,_B
_B:
sub sp, sp, #0x20 ;拉伸栈空间32个字节
// 这里要操作两个寄存器,所以使用stp
stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0,寄存器里面的值进行交换
// bl _0 死循环一直调用_B,此时会一直拉伸栈空间,堆栈溢出就会导致崩溃
bl _0
add sp, sp, #0x20 ;栈平衡恢复栈空间32个字节
ret
如果死循环不断开辟空间,当堆空间与栈空间发生碰撞时,堆栈溢出
.png
二. bl和ret指令
bl指令
- l表示将下一条指令的地址放入lr(x30)寄存器
- b表示跳转到标号处执行指令
image.png
举例 当_B执行完成之后,要跳转到ldp指令,如下图
image.png
image.png
当执行到bl指令时,就会把下一条指令地址存入lr中,lr中存放回家的路
ret指令
- 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!
- ARM64平台的特色指令,它面向硬件做了优化处理的
x30寄存器
- x30寄存器存放的是函数的返回地址.当ret指令执行时会寻找x30寄存器保存的地址值!
- 注意:在函数嵌套调用的时候.需要将x30入栈!
接下来我们来进行练习
// asm.s文件内容
.text
.global _A,_B
_A:
mov x0,#0xaaaa
bl _B
mov x0,#0xaaaa
ret
_B:
mov x0,#0xbbbb
ret
// ViewController.m 中调用
@implementation ViewController
// 汇编函数声明
int B();
int A();
- (void)viewDidLoad {
[super viewDidLoad];
printf("A");
A();
printf("B");
// Do any additional setup after loading the view.
}
@end
运行程序,准备执行A函数
image.png
lr中保存的是0x0000000100d01f2c,与上图对应
image.png
继续执行跳入B函数,可以发现lr的内存地址为0x100d01ec8
image.png
继续执行ret返回到A函数中,再次执行A函数中ret发现死循环(此时的死循环没有拉伸栈空间,所以不会崩溃)对应了上节课遗留的问题!!! 原因是lr的内存地址一直为0x100d01ec8,执行完ret就会回到0x100d01ec8的位置,产生死循环。
lr寄存器与pc寄存器的区别?
pc寄存器指的是我们接下来要执行的内存地址,ret寄存器指的是让CPU将lr作为接下来执行的地址,此时会把lr的地址赋值给pc寄存器,如下图
image.png
接下来我们探讨怎么从函数A返回viewDidLoad?
// ViewController.m 中调用,我们修改代码如下,查看系统是怎么保存lr寄存器的
@implementation ViewController
void d() {
}
void c() {
d();
return;
}
- (void)viewDidLoad {
[super viewDidLoad];
c();
// Do any additional setup after loading the view.
}
@end
image.png
-> 0x100289ef4 <+0>: stp x29, x30, [sp, #-0x10]! 从图中可以看出系统是这样保护lr寄存器的,!表示将[sp, #-0x10]算出来的结果赋值给sp,如下图
赋值前
赋值后
0x100289f00 <+12>: ldp x29, x30, [sp], #0x10,是从sp读取x29 x30,读完之后sp 再加上#0x10 还原回去保持栈平衡。通过上面我们可以发现当我们进行函数嵌套时,是把x30存入栈的形式来保存回家的路
.text
.global _A,_B
_A:
sub sp,sp,#0x10
// 保护lr寄存器,这里不能把lr寄存器的地址存入其他寄存器,因为函数嵌套过深,担心其他寄存器的值会被修改
str x30,[sp]
mov x0,#0xaaaa
bl _B
mov x0,#0xaaaa
ldr x30,[sp]
add sp,sp,#0x10
ret
_B:
mov x0,#0xbbbb
ret
// 上面_A也可以这样写
_A:
str x30,[sp,#-0x10]!
mov x0,#0xaaaa
bl _B
mov x0,#0xaaaa
ldr x30,[sp],#0x10
ret
// viewDidLoad中打断点,会发现viewDidLoad之前有很多函数嵌套调用
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
* frame #0: 0x00000001027b9eec demo`-[ViewController viewDidLoad](self=0x0000000102d08530, _cmd="viewDidLoad") at ViewController.m:21:5
frame #1: 0x0000000199522adc UIKitCore`-[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 104
...
运行demo,此时lr内存地址如下
image.png
继续执行,如下图所示
image.png
image.png
image.png
从上图可以看出,lr成功从栈里取出,sp也恢复栈平衡,成功回到viewDidLoad
如果我们修改上面汇编sub sp,sp,#0x8 会发现最后取数据时崩溃ldr x30,[sp]。为什么存数据没问题,取数据崩溃?原因是ARM64里面 对栈的操作是16字节对齐的!!
三. 带参数返回值的函数
带参数返回值的函数
- ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈.
- 一般为了提高效率,参数最好不要超过6个,因为中间有两个隐形参数 self 和 selector
- 如果参数过多,可以使用数组或结构体来提高效率
- 函数的返回值是放在X0 寄存器里面的.
- 如果返回值大于8个字节,就会利用内存传递返回值
函数的局部变量
- 函数的局部变量放在栈里面!
3.1 先查看系统汇编实现
// ViewController.m中函数调用
@implementation ViewController
int sum(int a, int b) {
return a + b;
}
- (void)viewDidLoad {
[super viewDidLoad];
sum(10, 20);
}
@end
image.png
// sum函数的汇编代码如下
demo`sum:
-> 0x100281ee4 <+0>: sub sp, sp, #0x10 ; =0x10
0x100281ee8 <+4>: str w0, [sp, #0xc] //写入内存
0x100281eec <+8>: str w1, [sp, #0x8]
0x100281ef0 <+12>: ldr w8, [sp, #0xc] //从内存中取出
0x100281ef4 <+16>: ldr w9, [sp, #0x8]
0x100281ef8 <+20>: add w0, w8, w9
0x100281efc <+24>: add sp, sp, #0x10 ; =0x10
0x100281f00 <+28>: ret
3.2 实现简写版汇编
// ViewController.m中函数调用
@implementation ViewController
int suma(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
suma(10, 20);
printf("%d",suma(10,20));
}
@end
// asm.s文件内容
.text
.global _suma
_suma:
add x0,x0,x1
ret
// 参数放在寄存器中,返回值放在x0寄存器中
// 运行成功打印30











网友评论