iPhone设备主要使用ARM架构的CPU,iPhone5S之后的CPU架构都是ARM64,因此主要学习ARM64架构的寄存器和与之对应的指令集
1、栈
-
1.1 了解栈
栈是一种具有特殊的访问方式的存储空间,后进先出(Last In First Out,LIFO)
-
1.2 汇编中对栈的操作
栈主要是栈顶地址与栈底地址,ARM64使用 sp寄存器 保存栈顶的地址,使用 fp寄存器 保存栈底地址(注意:fp寄存器也称为x29寄存器, 属于通用寄存器, 某些时刻我们利用它保存栈底的地址)
App整体内存图
- 栈的操作
- 栈的拉伸(开辟栈空间)
- 栈的平衡(回收栈空间)
- ARM64中栈的操作主要是对sp寄存器的操作, 对
sub sp, #0x10
可以拉伸栈,add sp, #0x10
可以平衡栈
注意: 对sp寄存器进行操作,必须是16字节对齐的!!也就是 add与sub 数是16的倍数,否则报错
- 栈内存的操作(ARM64下的指令)
- 栈内存的写入:
- str:将一个寄存器的数据写入栈内存中
- stp:将两个寄存器的数据写入栈内存中
- 栈内存的读取:
- ldr:从栈内存中读取数据,放入到某个寄存器中
- ldp: 从占内存中读取数据,分别放入到两个寄存器中
- 栈内存的读写是从 sp指向地址开始向高内存地址的读写
- 栈内存的写入:
举例
_A: sub sp, #0x20; mov x26, 0xffffffff mov x27, 0xa0a0a0a0 stp x26, x27, [sp]; ldp x27, x26, [sp]; add sp, #0x20; ret复制代码
分析
第一行代码:对栈进行拉伸,用于存储数据
第四行代码:将寄存器的数据写入到内存中,写入位置可以看下图的分析(写入的时候,x26数据写入到sp~sp+0x10的位置--> 向高地址写入)
第五行代码:从栈内存中读取数据,放入到寄存器中,但是放的时候和写的时候2个寄存器的位置是相反的,因此,寄存器的数据会被交换(读取的时候,先读取sp~sp+0x10的位置到x27 --> 高地址读取)
分析图
xcode内存调试图
- 栈的操作
2、函数
-
2.1 函数的调用与返回
-
bl指令,用于跳转,会执行下面2步
- 保存当前bl指令的下一条指令的地址到lr(x30)寄存器,用于ret返回
- 将pc寄存器的值更改为需要跳转的指令地址,实现程序控制
-
ret指令
- 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!
举例
void funcB(void) { return;}void funcA(void) { funcB(); return;}int main(int argc, char * argv[]) { funcA();}复制代码
Xcode汇编代码
主要分析 funcA 函数的调用与返回
main中
第7行 bl 0x1000d07b0, 跳转到funcA函数入口地址,执行funcA函数,同时会将下条指令的地址0x1000d07dc保存到lr寄存器,函数返回后,需要执行这条指令
第9行 [sp, #0x10]只是在sp的地址上进行偏移,不会改变sp的值funcA中
第2行 先开辟funcA函数的栈,[sp, #-0x10]! 感叹号表示先运算修改寄存器的值,然后在使用寄存器的值, []中括号表示这是一个内存地址,也要对内存地址进行读写操作, 整个指令的意思是,将x29(32位ARM下用于保存fp栈底地址) x30(lr 栈顶地址)写入到funcA函数栈内存中。why?bl跳转到funcA中后,lr寄存器保存了funcA返回后需要执行的下一条指令的地址,直接点就是函数返回后需要执行的下一条语句的位置。为什么要写入函数栈??首先lr寄存器的值会经常修改(bl执行后就会修改), 其次函数栈才能保证当前数据不会被别人给覆盖掉
第5行 先使用sp的值,然后在修改(sp偏移0x10, 平衡函数栈)。整个指令的意思就是,从函数栈中取出保存的fp lr的值,并还原回去,之后在进行函数栈内存的回收 第6行 ret指令从lr中获取到函数返回后要执行的下一条指令的地址,退出函数经过分析,我们可以总结:函数的调用的时候,会开辟栈空间,并将函数的lr寄存器值存储起来,通俗点说 保护回家的路
-
-
2.2 函数的参数、局部变量与返回值
- 参数的传递:(例如main调用sum函数)
- 在函数调用的时候,CPU 通常 会将传给被调用函数的参数放在x0-x7 这8个寄存器中, 被调用的函数从寄存器中读取
- 如果超出8个,放入函数栈中(哪个函数的栈??main中,然后sum从栈中读取出来)
- 如果一个参数在寄存器中放不下,这个暂时不知道如何处理的。。
- 函数局部变量:哪个函数内部的局部变量就放在哪个函数的栈中
- 函数的返回值:当被调用函数要返回前,会将返回值放在x0寄存器中(调试的时候,可以使用lldb命令register write x0 0x1111 改变x0寄存器值,看看函数返回值是否改变)
举例
int sum(int aa, int bb, int cc, int dd, int ee, int ff, int gg, int hh, int ii) { int temp = 20; return aa+bb+cc+dd+ff+gg+hh+ii+temp;}int main(int argc, char * argv[]) { sum(1, 2, 3, 4, 5, 6, 7, 8, 9); return 0;}复制代码
Xcode汇编代码(请使用真机)
main函数 main函数汇编代码分析
2~3行:拉伸栈,写入栈 保护x29, x30(lr)寄存器,用于函数返回 5~12行 16~17行:先用x2~x9保存前8个参数,然后转移到x0~x7
13行 x10 存储第9个参数 18行 将x10写入sp栈,也就是当前函数的栈 21行 将x0写入栈,也就是将sum函数的返回值先保存到main函数栈中sum函数 sum函数续 sum函数汇编代码分析
2行:拉伸栈, 为什么没有像main一样做lr的保护??因为sum是一个叶子函数,也就是sum不再调用其他函数了,lr不会被更改
3行、27行:从main函数栈内存中读取第9个参数。因为进来的时候sp更改了,所以取的时候要加回去 5~12行:将x0~x7参数写入栈 4行、13行: 生成局部变量,并入栈 14~26行:读取x0~x7参数并计算和,和放在x9中 27~28行:读取第9个参数并计算 29~30行:读取局部变量temp并计算 最后的计算结果存储在w0中,也就是x0栈内存分析图
注意:sp 的拉伸是16的倍数,虽然有浪费
- 参数的传递:(例如main调用sum函数)
-
2.3 函数的总结
-
函数的调用
- bl 指令,跳转到函数入口地址处执行函数
- bl 跳转的同时,还会将下一条指令的地址保存到lr(x30)寄存器中,用于函数返回
-
函数栈
- 进入函数后要做的第一件事,就是开辟函数的栈空间,sp向低地址偏移(一般
sub sp, sp, #0x20
类似的汇编代码) - 函数栈主要用于保存 lr寄存器、函数参数、函数局部变量(static局部变量不是保存在函数栈中)
- 栈内存的读写:str/stp ldr/ldp,读写是基于 sp 向高地址进行
- 进入函数后要做的第一件事,就是开辟函数的栈空间,sp向低地址偏移(一般
-
函数参数传递
- 少于等于8个参数:x0~x7寄存器中
- 超过8个:存储在上一个函数的栈中,使用时从上个函数栈内存中读取
-
函数局部变量
- 局部变量存储在栈内存中
- static修饰的局部变量存储在app的全局数据区
-
函数返回
- 首先,平衡函数栈
add sp, sp,#0x20
- 如果不是叶子函数,lr寄存器的值不是当前函数的返回地址,需要从栈中读取保存好的lr寄存器的值,还原回来
- ret返回(根据当前lr寄存器存储的指令地址,放入pc寄存器,cpu跳转执行)
- 首先,平衡函数栈
-
补充1:递归函数的调用和函数A调用函数B同样理解,不会因为函数名相同而有什么特殊
补充2:多线程环境下,切换线程时,操作系统切换之前会对寄存器数据进行保护,而函数的栈内存却不会,因此多线程资源抢夺的时候需要我们自己对函数栈内存进行保护(锁或者其他)。