注意:我们使用的是
ARM64框架,所以要使用真机,而不是模拟器,也不能使用命令行工程。
栈
-
栈:是一种具有特殊的访问方式的存储空间(后进先出,Last In Out Firt, LIFO)
SP & FP 寄存器
-
SP寄存器在会保存我们栈顶的地址。
-
FP寄存器也称为x29寄存器,属于通用寄存器;但是在我们利用它保存栈底的地址。(为什么是某些时刻呢?因为当只有一个函数调用栈的时候,不需要保存栈底,只有一个栈顶就够了)
⚠️ 注意:
ARM64开始,取消了32位的LDM,STM,PUSH,POP指令,取而代之的是ldr\ldp,str\stp指令。
ARM64里面,对栈的操作是16字节对齐的!!!
-
str(store register) 指令
将数据从寄存器中读出来,存到内存中。 -
ldr(load register) 指令
将数据从内存中读出来,存到寄存器中。
另外:
ldr&str的变种ldp&stp还可以操作两个寄存器。
下面我们看一个例子:


这里我们可以通过
si命令 或者 Ctrl + 单步跳转 来进行单步执行进入
sum函数内部:
下面我们来一条一条的分析一下
sum函数的汇编代码:
| 汇编代码 | 解释 |
|---|---|
0x1003b6730 <+0>: sub sp, sp, #0x10 |
sp是栈顶指针,拉伸栈空间16个字节 |
0x1003b6734 <+4>: str w0, [sp, #0xc] |
sp往上加12个字节的位置,存放w0里面的值 |
0x1004f6258 <+8>: str w1, [sp, #0x8] |
sp往上加12个字节的位置,存放w1里面的值 |
0x1004f625c <+12>: ldr w8, [sp, #0xc] |
将sp偏移12个字节位置的值取出来,放入w8
|
0x1004f6260 <+16>: ldr w9, [sp, #0x8] |
将sp偏移12个字节位置的值取出来,放入w9
|
0x1004f6264 <+20>: add w0, w8, w9 |
w8 和 w9 里面的值相加,并赋值给w0
|
0x1004f6268 <+24>: add sp, sp, #0x10 |
sp指针向上偏移16个字节。栈平衡,因为最开始的时候拉伸了16个字节 |
0x1004f626c <+28>: ret |
返回,相当于return
|
这里可能有人会疑惑,为什么开始的时候会操作
w0&w1这两个寄存器呢?大家看main函数的混编会看到:0x1003ee288 <+20>: mov w0, #0xa 0x1003ee28c <+24>: mov w1, #0x14同时,我们在1、汇编初探里面也讲过:
通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
bl 和 ret 指令
-
bl标号
上一节我们讲过,bl可以修改pc寄存器里面内容。但是bl在这个过程中究竟做了什么呢?
i:将下一条指令的地址放入lr(x30)寄存器
ii:转到标号处执行指令
比如:
bl在执行指令,跳转到sum函数的时候:
1、首先会把下一条指令的地址0x1003ee294存放到lr(x30)寄存器里面,这也是为什么sum执行完毕之后还能回到main函数原因,因为保存了回家的路。
2、接着再跳转进sum函数。
-
ret
默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下一条指令地址。
ARM64平台的特色指令,它面向硬件做了优化处理。
x30寄存器
x30寄存器存放的是函数的返回地址。当ret指令执行时,会寻找x30寄存器保存的地址值。-
讲到这里就引入了一个问题,如果说是嵌套函数呢?一个
x30寄存器也不够用呀。下面我们来看一下嵌套函数的情况:
进入main汇编我们会发现,进入A之前,bl的下一条指令地址是0x100dda290:
接下来我们进入A:
我们来解读一下此时A里面的汇编代码:
| 汇编代码 | 解释 |
|---|---|
0x100dda264 <+0>: stp x29, x30, [sp, #-0x10]! |
sp偏移16个字节,拉伸栈空间,存储x29 & x30里面的内容。[sp, #-0x10]!相当于-=,就是将sp的地址偏移16个字节再赋值给sp。注意这种简写方式,只适用于正好占满栈空间的情况,因为栈是从栈底开始写入。 |
0x100dda268 <+4>: mov x29, sp |
将sp里面的值给x29
|
0x100dda26c <+8>: bl 0x100dda260 |
跳转至B并将下一条指令地址保存到lr(x30)
|
0x100dda270 <+12>: ldp x29, x30, [sp], #0x10 |
从栈里面取出16个字节的值,分别赋值给x29,x30(就是开始的时候,存到内存中的值),并且sp向上偏移16个字节。栈平衡 |
0x100dda274 <+16>: ret |
返回 |
看到这里我们了解到,此时A的回家的路是放在栈里面的
- 接下来我们再进入
B看一下:
- 然后我们再单步执行,返回
A:
总结:
ARM64平台,在函数嵌套调用的时候,需要将x30入栈。也就是说,函数的返回地址会保存在栈里面。
函数的参数和返回值
ARM64中,函数的参数是存放在x0 ~ x7(w0 ~ w7)这8个寄存器里面的。如果超过8个参数,就会入栈。
函数的返回值是放在x0寄存器里面的。
以上两点,我们再上面探索的过程中已经见证过了,这里就不多做赘述(注意看sum函数的汇编代码)。
我们在下一篇文章的时候,具体谈论一下参数和返回值的特殊情况
tips :
在我们日常看法OC代码的时候,函数的参数最好不要超过6个,因为OC的函数自带两个参数(id self,SEL_cmd),如果再多加6个,就会有参数要入栈。这样影响读取速率。
一些调试技巧:









