当前位置:   article > 正文

【iOS内功】ARM黑魔法—栈桢的入栈和出栈_register fp(x29);

register fp(x29);

栈桢之谜

调用一个子函数,在内存上会入一个新的栈桢。子函数执行完了,当前栈桢会出栈。在运行时,栈桢的出栈和入栈的逻辑是怎么实现的呢?

这是一个很有趣的问题,也是一个重要的知识点,它是排查疑难Crash的必备技能。

ARM64特殊寄存器

栈桢的入栈和出栈依赖于3个特殊寄存器,它们是fp、lr、sp,在ARM汇编里对应的是X29、X30、x31

特殊寄存器作用
LR (X30)link register 链接寄存器,保存返回上一层调用函数的地址
FP (X29)Frame point 指向栈底,保存栈桢的地址
SP (x31)Stack point 指向栈顶, 可以用来寻址
PC指向当前执行的代码的地址,我们无法访问PC寄存器
CPSR状态寄存器。不同于编程语言里面的if else.在汇编中就需要根据状态寄存器中的一些状态来控制分支的执行。

案例分析

下面基于一个Demo来分析

void func2(int c) {
}

void func1() {
    int c = 7+18;
    func2(c);
}

int main(int argc, char * argv[]) {
    func1();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

调试汇编代码:

XCode设置Debug->Debug Workflow->Always Show Disassembly,然后真机调试运行Demo,就可以查看到每一个方法的ARM64汇编指令。

main函数汇编代码

OCSimpleTest`main:
    0x104a5a008 <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x104a5a00c <+4>:  stp    x29, x30, [sp, #0x10]
    0x104a5a010 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x104a5a014 <+12>: stur   w0, [x29, #-0x4]
    0x104a5a018 <+16>: str    x1, [sp]
    0x104a5a01c <+20>: mov    w0, #0x5
    0x104a5a020 <+24>: mov    w1, #0x7
    0x104a5a024 <+28>: mov    w2, #0x9
->  0x104a5a028 <+32>: bl     0x104a59fd4               ; func1 at main.m:60
    0x104a5a02c <+36>: mov    w8, #0x0
    0x104a5a030 <+40>: mov    x0, x8
    0x104a5a034 <+44>: ldp    x29, x30, [sp, #0x10]
    0x104a5a038 <+48>: add    sp, sp, #0x20             ; =0x20 
    0x104a5a03c <+52>: ret    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

main函数指令解析

第5行到第7行

mov指令是给寄存器赋值,main函数调用func1时会传递3个参数,因此跳转func1前,要先将3个参数存储到寄存器w0,w1,w2.(w寄存器只占32位,也就是4个字节)

第八行

bl 0x102a41fd8 ; func1 at main.m:60

bl是跳转指令,从main函数跳转到下一个函数func1

函数A汇编代码

OCSimpleTest`func1:
    0x10428dfb8 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x10428dfbc <+4>:  stp    x29, x30, [sp, #0x20]
    0x10428dfc0 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x10428dfc4 <+12>: stur   w0, [x29, #-0x4]
    0x10428dfc8 <+16>: stur   w1, [x29, #-0x8]
    0x10428dfcc <+20>: stur   w2, [x29, #-0xc]
    0x10428dfd0 <+24>: mov    w8, #0x19
->  0x10428dfd4 <+28>: str    w8, [sp, #0x10]
    0x10428dfd8 <+32>: ldur   w8, [x29, #-0x4]
    0x10428dfdc <+36>: ldur   w9, [x29, #-0x8]
    0x10428dfe0 <+40>: add    w8, w8, w9
    0x10428dfe4 <+44>: ldur   w9, [x29, #-0xc]
    0x10428dfe8 <+48>: add    w8, w8, w9
    0x10428dfec <+52>: str    w8, [sp, #0xc]
    0x10428dff0 <+56>: bl     0x10428dfb4               ; func2 at main.m:58:1
    0x10428dff4 <+60>: ldr    w0, [sp, #0xc]
    0x10428dff8 <+64>: ldp    x29, x30, [sp, #0x20]
    0x10428dffc <+68>: add    sp, sp, #0x30             ; =0x30 
    0x10428e000 <+72>: ret       

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

函数A指令解析

第1行

sub sp, sp, #0x30 ; =0x30

sub是减法指令。 SP寄存器的值向低地址偏移48个字节(0x30)。这时候SP已经指向新栈桢的顶部。

第2行

stp x29, x30, [sp, #0x20]

stp是存值指令,存2个值 存储上一个栈桢fp寄存器(x29)和lr寄存器(x30)的值,存储的位置是sp寄存器地址向高地址偏移32个字节(0x20)。

这里存储上一个栈桢fp和lr的值是一个重要的设计,下一个函数执行完,读取这两个值就可以回到原来的逻辑。

偏移的方向和大小(知识点)
因为栈是从高地址向低地址生长,所以入栈时地址偏移都是负向的。ARM64里寄存器是64位,也就是8个字节,这里要存储fp和lr两个寄存器,所以偏移量是16个字节。
  • 1
  • 2
思考:fp_A和lrA存储时哪个在前面,哪个在后面,为什么?
  • 1

第3行

add x29, sp, #0x20 ; =0x20

add是加法指令。 设置fp(x29)寄存器,将其指向sp寄存器向高地址偏移32个字节的位置(0x20)。

此时函数A的栈桢已经布局完成,fp_A指向栈底,sp_A指向栈顶,占了16个字节。上一个栈桢的fp和lr的指针存储在栈桢A之前,也占了16个字节。

思考:为什么栈桢A的空间只有32个字节?

fp到sp之间的内存,主要用来存储寄存器带过来的入参、函数内的局部变量。

函数A有3个入参,每个入参占4个字节。2个局部变量,每个4字节,总共20字节。内存有字节对齐,所以总共申请了32个字节的空间。
  • 1
  • 2
  • 3
  • 4
  • 5
思考:如果函数A有10几个入参,入参类型除了int,还有其他的类型,这个时候栈桢的空间会是多少呢?
  • 1

第16行

ldr w0, [sp, #0xc]

ldr是取值指令。 将sp向高地址偏移12个字节(0xc)的值读出来,存储到w0寄存器。sp+0xc存的是“a + b + c”的结果,是函数A要返回的结果y。

第17行

ldp x29, x30, [sp, #0x20]

ldp是取值指令,取2个值 将sp向高地址偏移32个字节的两个值,取出来存储到fp寄存器(x29)和lr寄存器(x30)。

这里和第二行命令是一一对应的,取回main函数的fp和lr

第18行

ret

函数A栈桢出栈,执行lr寄存器指向的指令地址,也就是main函数跳转到fun_A的下一行命令。

小结

main函数调用函数A入栈过程

  1. 将传递给函数A的参数,存储到w0开始的寄存器中
  2. 保存main函数栈底指针fp和返回地址lr。
  3. 对fp和sp指针进行偏移,开辟函数A的栈桢空间

函数A执行完出栈过程

  1. 从内存中取出返回值,储存到w0寄存器里
  2. 从内存中取出main函数的fp和lr
  3. 执行lr的指令

总结

这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

本文主要介绍调用栈的内存布局,已经ARM汇编如果使用指令进行出栈和入栈。为了方便读者理解,前面还介绍了栈的基础概念,栈在内存中的布局。

作者:Blacktea
链接:https://juejin.cn/post/6897975519048892423

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/531555
推荐阅读
相关标签
  

闽ICP备14008679号