栈桢之谜

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

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

int x = 7+18;

int y = a + b + c;

func2();

return y;

}

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

func1(5,7,9);

}

复制代码

调试汇编代码:

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

main函数汇编代码

OCSimpleTest`main:

0x104a5a008 : sub sp, sp, #0x20 ; =0x20

0x104a5a00c : stp x29, x30, [sp, #0x10]

0x104a5a010 : add x29, sp, #0x10 ; =0x10

0x104a5a014 : stur w0, [x29, #-0x4]

0x104a5a018 : str x1, [sp]

0x104a5a01c : mov w0, #0x5

0x104a5a020 : mov w1, #0x7

0x104a5a024 : mov w2, #0x9

-> 0x104a5a028 : bl 0x104a59fd4 ; func1 at main.m:60

0x104a5a02c : mov w8, #0x0

0x104a5a030 : mov x0, x8

0x104a5a034 : ldp x29, x30, [sp, #0x10]

0x104a5a038 : add sp, sp, #0x20 ; =0x20

0x104a5a03c : ret

复制代码

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 : sub sp, sp, #0x30 ; =0x30

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

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

0x10428dfc4 : stur w0, [x29, #-0x4]

0x10428dfc8 : stur w1, [x29, #-0x8]

0x10428dfcc : stur w2, [x29, #-0xc]

0x10428dfd0 : mov w8, #0x19

-> 0x10428dfd4 : str w8, [sp, #0x10]

0x10428dfd8 : ldur w8, [x29, #-0x4]

0x10428dfdc : ldur w9, [x29, #-0x8]

0x10428dfe0 : add w8, w8, w9

0x10428dfe4 : ldur w9, [x29, #-0xc]

0x10428dfe8 : add w8, w8, w9

0x10428dfec : str w8, [sp, #0xc]

0x10428dff0 : bl 0x10428dfb4 ; func2 at main.m:58:1

0x10428dff4 : ldr w0, [sp, #0xc]

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

0x10428dffc : add sp, sp, #0x30 ; =0x30

0x10428e000 : 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入栈过程

将传递给函数A的参数,存储到w0开始的寄存器中

保存main函数栈底指针fp和返回地址lr。

对fp和sp指针进行偏移,开辟函数A的栈桢空间

函数A执行完出栈过程

从内存中取出返回值,储存到w0寄存器里

从内存中取出main函数的fp和lr

执行lr的指令

总结

本文介绍了函数入栈和出栈的底层实现,具体使用到哪些的特殊寄存器,如何通过简单的arm指令操作,一步一步完成调用栈的管理。理解这个过程,可以欣赏到Arm设计的精妙之处,也能让我们排查疑难问题时可以俯瞰全局。

为了方便读者理解,前面有一篇文章还介绍了栈的基础概念,栈在内存中的布局,调用栈的内存布局。

【iOS内功】深入解析Crash调用栈的内存布局

参考

文章汇总

iOS内功系列

架构系列

思维系列

arm64入栈出栈_【iOS内功】ARM黑魔法—栈桢的入栈和出栈相关推荐

  1. 高速的二舍八入三七作五_京承高速收费“二舍八入 三七作五”

    (3)望和桥至北七家里程16.74公里,盘算费额8.37元,按"二舍八入"原则,应收10元. 市民马*回声:京承高速公路的出京偏向收费*正,对小车而言,黄港出口和后沙峪出口收费都是 ...

  2. 栈的输出_程序设计做题笔记:计算表达式(一):栈

    题目要求输入一个四则运算表达式(保证合法,不含空格),输出这个表达式的值. 解决这个问题有两种主要的思路,一种是用栈解决,较为符合人在计算四则运算表达式时的行为,从左向右算过去:另一种思路是递归,把原 ...

  3. python win10 捕获 弹出窗口_[python爬虫] Selenium高级篇之窗口移动、弹出对话框自登录...

    在我们使用Selenium Python制作自动爬虫和网页自动测试的时候,通常会遇到弹出新的窗体或对话框的时候,此时你捕获的窗体已经被打断,从而干扰你的爬虫. 那怎么解决这个问题呢? 本篇文章主要记录 ...

  4. 用python画出叶子_有没有一种方法可以在叶子上画出许多标记?

    我正在尝试使用Folium从pandas数据框中读取地理信息. 我的密码是:import folium from folium import plugins import pandas as pd . ...

  5. 【数据结构入门】栈(Stack)的实现(定义、销毁、入栈、出栈等) | 图解数据结构,超详细哦~

    文章目录 (1)前言 1)栈的概念 2)进栈出栈的形式 3)栈的存储结构 (2)栈的实现(顺序栈) 1)栈的定义 2)栈的初始化 3)栈的销毁 4)入栈 5)出栈 6)检测栈是否为空 7)获取栈中有效 ...

  6. python 栈实现 加减乘除_数据结构与算法(六):基于栈实现简单的四则运算

    栈,是一种操作受限的线性表,其操作规则为:后进者先出,先进者后出. 目录 题目需求: 给定一个字符串,形如"1+6*5/3-8",根据其内容,计算结果,此例结果为3. 解题思路: ...

  7. 初始化栈的代码_数据结构中的栈,你知道多少?

    由于栈比较简单,也很容易理解,学过的人都知道一句话就可以描述栈的特性:后进先出.所以这篇文章主要是写如何使用代码来描述栈,当然也是让大家很容易理解的语言.还是先给出这篇文章的大致脉络. 首先,对栈有一 ...

  8. java 取栈顶元素_《Java实战之内存模型》详解篇

    内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行 JVM内存布局规定了Java在运行过程中内存申请.分配.管理的策略,保证了JVM的高效稳定运行 不同的JV ...

  9. java 栈的变量_深入Java核心:JVM中的栈和局部变量

    操作数栈和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组.但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的.可把操作数栈理解为存储计算时,临时数据的存储区域.下面我们通过 ...

最新文章

  1. Qt: 实现浮点slider和浮点spinbox
  2. 64位Fedora运行32位C++程序中int精度溢出处理
  3. java上下文即ServletContext
  4. CF468B Two Sets
  5. 申请https协议总结
  6. collapse用法
  7. Astar2007初赛第一场的题目2
  8. 在Ubuntu 20.04部署SONIC testbed(topo 0)
  9. 在openEuler上搭建LFS
  10. mac linux 键盘布局,Macbook Pro 推出中文键盘布局
  11. 程序员多次下载的浏览器竟然是它!
  12. JAVA集合中常见的List和Map用法
  13. [强烈推荐]ring0下文件解锁强制删除工具
  14. 切割钢条问题(rod cutting problem)
  15. 计算机 手机原理是什么,什么是手机投屏,手机投屏到电脑上的原理
  16. 【STM32】继电器的使用
  17. 按键状态机—实现连发
  18. 物联网终端设备的工作原理和功能讲解
  19. 微软中国公布2010年度经典MSN签名Top10
  20. 寒假集训D1.22.12.28

热门文章

  1. 雷军卸任小米电子董事长!常程等人也退出该公司 网友:为造车放弃家业?
  2. 万象双环设计+原色双影像极致体验 华为P50标准版9月29日正式开售
  3. 2021年Q2全球智能手机销量小米升至第二,苹果降至第三
  4. 苹果iOS 15正式发布!一大波新功能上线,这几个被刷爆了
  5. CES 2021落下帷幕 未来3年展会时间已公布
  6. 腾讯微博正式关闭:目前无法登陆 用户可申请个人内容备份
  7. 支付宝要给全国人民发100亿,7月1日起!
  8. 华为正式宣布全场景AI计算框架MindSpore开源 降低AI开发门槛
  9. realme曝光全球首张6400万像素样张 大家品一品?
  10. 如何才能招到马云这样的人才?海尔张瑞敏这样说...