美文网首页
2、函数调用栈

2、函数调用栈

作者: Jax_YD | 来源:发表于2021-03-23 14:30 被阅读0次

注意:我们使用的是ARM64框架,所以要使用真机,而不是模拟器,也不能使用命令行工程。

  • 栈:是一种具有特殊的访问方式的存储空间(后进先出,Last In Out Firt, LIFO)



SP & FP 寄存器
  • SP寄存器在\color{red}{任意时刻}会保存我们栈顶的地址。
  • FP寄存器也称为x29寄存器,属于通用寄存器;但是在\color{red}{某些时刻}我们利用它保存栈底的地址。(为什么是某些时刻呢?因为当只有一个函数调用栈的时候,不需要保存栈底,只有一个栈顶就够了)

⚠️ 注意:ARM64开始,取消了32位的 LDMSTMPUSHPOP指令,取而代之的是ldr\ldpstr\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 w8w9 里面的值相加,并赋值给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个字节的值,分别赋值给x29x30(就是开始的时候,存到内存中的值),并且sp向上偏移16个字节。栈平衡
0x100dda274 <+16>: ret 返回

看到这里我们了解到,此时A的回家的路是放在栈里面的

  • 接下来我们再进入B看一下:
  • 然后我们再单步执行,返回A:

总结:ARM64平台,在函数嵌套调用的时候,需要将x30入栈。也就是说,函数的返回地址会保存在栈里面。


函数的参数和返回值

ARM64中,函数的参数是存放在x0 ~ x7(w0 ~ w7)这8个寄存器里面的。如果超过8个参数,就会入栈。
函数的返回值是放在x0寄存器里面的。
以上两点,我们再上面探索的过程中已经见证过了,这里就不多做赘述(注意看sum函数的汇编代码)。
我们在下一篇文章的时候,具体谈论一下参数和返回值的特殊情况

tips :
在我们日常看法OC代码的时候,函数的参数最好不要超过6个,因为OC的函数自带两个参数(id selfSEL_cmd),如果再多加6个,就会有参数要入栈。这样影响读取速率。


一些调试技巧:


相关文章

网友评论

      本文标题:2、函数调用栈

      本文链接:https://www.haomeiwen.com/subject/ekoxhltx.html