美文网首页
【iOS内功】ARM黑魔法—栈桢的入栈和出栈

【iOS内功】ARM黑魔法—栈桢的入栈和出栈

作者: iOS开发面试总结 | 来源:发表于2021-04-10 14:59 被阅读0次

前言:最近应该有很多小伙伴去跳槽面试的吧,相信各位有的已经顺利收到offer了,而有些则是碰壁了,那么我在这里给大家准备了相关面试资料,还有相关算法资料。想了解的可找我拿

你关注 我送礼:感谢各位的观看,别忘了点个赞,同时我在这里还给各位准备了你们专属资料,关注我,获得私信进裙了解,或者直接进群有管理员主动找你,回复[7]之后,你就能拿到各自想要的资料。别忘了去领取啊

栈桢之谜

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

这是一个很有趣的问题,也是一个重要的知识点,它是排查疑难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();
}

调试汇编代码:

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    

这里有一个iOS交流圈:891 488 181 可以来了解,分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习!

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       

函数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个字节。
思考:fp_A和lrA存储时哪个在前面,哪个在后面,为什么?

第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个字节的空间。
思考:如果函数A有10几个入参,入参类型除了int,还有其他的类型,这个时候栈桢的空间会是多少呢?

第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的指令

总结

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

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

相关文章

  • 【iOS内功】ARM黑魔法—栈桢的入栈和出栈

    栈桢之谜 调用一个子函数,在内存上会入一个新的栈桢。子函数执行完了,当前栈桢会出栈。在运行时,栈桢的出栈和入栈的逻...

  • 【iOS内功】ARM黑魔法—栈桢的入栈和出栈

    前言:最近应该有很多小伙伴去跳槽面试的吧,相信各位有的已经顺利收到offer了,而有些则是碰壁了,那么我在这里给大...

  • 递归累加数组

    入栈 5入栈 4入栈 3入栈 2入栈 1出栈 [1 0]出栈 [2 1 0]出栈 [3 2 1 0]出栈 [4 3...

  • 汇编学习-入栈和出栈

    栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈...

  • 栈-N946-验证栈序列

    题目 概述:给定一个入栈序列和出栈序列,判断如果以入栈序列的顺序入栈,所给定的出栈序列的顺序是否是合理的 输入:入...

  • 栈的简单Java实现

    栈栈的特点是先进后出,出栈、入栈都是在栈顶操作。

  • ARM栈结构

    ARM 栈类型 根据栈生长方向,ARM的栈可分为递增堆栈和递减堆栈。 递增堆栈:栈向高地址生长 递减堆栈:栈向低地...

  • 栈和队列算法设计题(二)

    题目 假设以I和O分别表示入栈和出栈操作。栈的初态和终态均为空,入栈和出栈的操作顺序可表示为仅由I和O组成的序列,...

  • 链栈的操作

    链栈的定义 链栈的操作 初始化 判断栈空 入栈 出栈

  • 剑指offer 面试题7:用两个栈实现队列

    题目:用两个栈实现一个队列 解法:有两个栈A、B,入队时往A栈入,出栈时,如果B栈为空,则把A栈依次出栈入B栈,然...

网友评论

      本文标题:【iOS内功】ARM黑魔法—栈桢的入栈和出栈

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