2、函数调用栈
注意:我们使用的是
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个,就会有参数要入栈。这样影响读取速率。
一些调试技巧: