上一篇中我们讨论了Block的来源,它是OC中对闭包的实现。Block的设计初衷及其特性,主要3个特性:
- 可嵌套定义,可在函数及Block内定义Block;
- Block内可访问外层变量,这里我们上篇介绍了有
截取变量
及__block变量
。 - 方法传递,包括了后面会介绍的Block的堆拷贝。
那根据我们的“三问精神”,这篇开始我们就来深究一下Block本身及这些特性是怎样设计并实现的。
这一篇中,我们将为深入Block源码做好各种准备,它将为我们后续对Block的研究或是学习其它技术提供知识储备与帮助。
1 深入函数栈
我们说Block特性的时候,说过Block内可访问其外层的变量,这个外层的变量,主要有外层函数的局部变量,当然也有在外层定义的分配在堆上的对象。堆对象我们在介绍《面向对象》和《OC的内存管理》时介绍过了,这里我们补充下函数中的局部变量。
局部变量
,顾名思义,只在局部起作用(一般就是函数的作用域),它其实存放在函数栈
当中。
1.1 函数栈stack与栈帧frame
1.1.1 函数栈
我们先来回顾下程序在内存中的结构:
对内存结构的详细分析,请查看《这篇》
函数栈
,又称栈区(stack)
,在内存中从高地址往低分配,与堆区域相对。
|
|
1.1.2 栈帧
我们知道函数调用是发生在栈上的,每个函数相关的信息(局部变量、调用记录等)都存储在一个栈帧中。每执行一次函数调用,就会成生一个其相关的栈帧,然后将之压入函数栈。而当函数执行结束,则将此函数对应的栈帧出栈并释放掉。我们使用的在函数中定义的局部变量就放在栈帧中,每次函数调用完随栈帧一起释放掉,这样就保证了函数调用的快速高效。
比如下面这个程序示例:
|
|
程序执行时栈区中栈帧变化:
|
|
1.2 深入栈帧
接下来,我们深入探索一下栈帧的变化。我们先简单回顾下ARM中的寄存器:
1.2.1 寄存器基础
因为现在iPhone cpu是都是使用ARM64位架构,所以这里就简单回顾下ARM64中的寄存器架构。
- ARM64寄存器
ARM64 有34个寄存器,包括31个通用寄存器、SP、PC、CPSR。
寄存器 | 描述 |
---|---|
x0-x30 | 通用寄存器,如果有需要可以当做32bit使用 |
FP(x29) | 保存栈帧地址(栈底指针) |
LR(x30) | 程序链接寄存器,保存子程序结束后需要执行的下一条指令 |
SP | 栈指针,使用 SP/WSP来进行对SP寄存器的访问 |
PC | 程序计数器,总是指向即将要执行的下一条指令,软件自身是不能直接改写PC寄存器的 |
CPSR | 状态寄存器 |
x0-x7: 用于子程序调用时的参数传递,X0还用于返回值传递
x0-x30 是31个通用整形寄存器。每个寄存器可以存取一个64位大小的数。 当使用 r0 - r30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位.
其中我们关注的主要就是几个特殊的寄存器而已。
- 和程序执行有关的2个:
- PC 程序计数器,指向下一条要执行的指令;
- LR 程序连接寄存器,指向函数执行结束的要返回的指令;
- 和栈帧有关的2个:
- SP 栈顶指针,指向栈区的栈顶;
- FP 栈帧指针,指向当前栈帧的栈底。
画了一张图方便理解:
|
|
1.2.2 要用到的LLDB基础
接下来,我们进入实战,用一个简单的函数调用例子来看看真实场景下栈帧是怎样工作的。作为准备工作,我们先看看要使用的LLDB中的几个常用的栈帧调试命令:
先罗列下等会我们要用到的调试命令
|
|
1.2.3 深入栈帧实战
我们实验用的示例程序如下,调用顺序为:method1 -> method2 -> method3
:
|
|
此程序的调用栈帧应该是这样:
|
|
我们下面从method1入口处开始分析:
- method1入口
我们在method(1)
调用处断点,输入dis
查看:
|
|
bl
是汇编的子程序跳转指令,它会将把下一条指令的地址存储到LR寄存器中(程序返回点),并跳转到给定的地址。因此,0x100a796fc
就是method1
方法的入口地址(后面看汇编代码可以看出)。
|
|
记下寄存器信息,接着点“继续运行”,断点进入method1内部。
- method1
在method1内部断点,我们先来查看下其汇编源码:
|
|
我们继续使用LLDB来看看实际情况是否如我们分析的一样。
我们先查看下当前栈区:
|
|
我们看到,执行进入method1
之后,frame0变成了method1
。我们再看看寄存器状态:
|
|
我们看到,LR变成了method1的入口-[XXController onButtonPress]
方法,也就是说method1执行完后将返回-[XXController onButtonPress]
方法,这是符合我们所想的。
我们再根据 sp = 0x000000016f388f10
,查看下内存中栈区的信息(因为栈是从高地址到低地址的,所以我们从栈顶打印就可以看到想要的栈帧信息):
|
|
从对栈帧内存究竟的分析,和反汇编出来的代码是一一对应的:
保存现场部分:
我们从FP
的指向可知,FP+4
和FP+8
正好对应汇编第一部分中的保存现场部分,将上一个栈帧的FP
与LR
入栈!局部变量部分:
从栈区结合反汇编代码可见,入参与局部变量使用FP+位移
或SP+位移
来取(看哪个近来提高寻址速度),两者都位于栈帧内存区域中。然后函数的传参是用寄存器w0,w1…来传递(从反汇编代码中看得)。函数返回部分:
函数返回时,先把FP
、LR
恢复,再直接把SP下移,该栈帧信息就给释放了,可见函数栈操作的效率是比较高的。
- method2
method2与method1都是非叶子函数(非叶子函数就是函数内还调用了自定义函数),所以情况是相同的,我们就不分析反汇编了,直接看走进去后的栈帧信息。
|
|
- method3
接着让断点走进method3
:
|
|
再使用LLDB查看一下内存与寄存器信息:
|
|
这里可见,叶子函数没有设FP
,这应该是一个优化,也就是当调用的函数为叶子函数时,不需要使用FP栈帧底指针,可以省去FP的保存与恢复,提高一些效率。
- 函数返回method2
我们在method2的return处也打了一个断点,看看返回到method2时的栈区情况:
|
|
由此可见,method3的栈帧已经回收了
再看看寄存器和栈区信息
|
|
后面method1的返回也是累同,我这里就不多作累叙了。朋友们可以自已实验一下,体会其中乐趣。
1.3 小结
至此,我们知道了函数栈在程序运行过程中是怎样工作的,寄存器与栈区操作配合,完成了函数调用、参数传递及局部变量的存储。我们知道了参数存在什么地方,函数怎样保存入参、怎样保存局部变量。
当然,我的实验为了简单,用的是int型基本变量,感兴趣的朋友可以验证下分配对象的话,对象的引用是怎样存储的,就作为本节的小作业吧~
2 小结
本篇为我们深入Block源码做好知识储备。
首先我们了解函数栈的实现,其实也是加深了对函数的理解,知道了在程序运行过程中函数的调用过程是怎样的,传参是怎样的,局部变量是怎样存储的。栈的操作思想及数据结构在系统的设计中是非常常见且有用的,其设计逻辑简单、操作效率高效,在很多场景上都有运用,比如我们讲AutoReleasePool的设计与实现,也有用到,大家可以细细体会。
对于局部变量的存储我们也有了深刻的认识,在栈上分配的局部变量是直接存放在函数栈中的,准确来说是在栈帧中。结合我们《OC的内存管理》中介绍的,内存分配在堆上的对象。我们后面会介绍分配在栈究竟的Block,分配在堆内存中的Block以及将Block从栈上拷贝到堆上,故本文可为后面的叙述打下一个基础。
3 引用
- 《iOS-iOS与OS X多线程和内存管理》 我学习本系列的主要参考书,写得非常好,真正的深入浅出。
- 《谈Objective-C block的实现 - 唐巧》 唐巧一篇早期的文章,写得不错的。
- 源码:libclosure-73,objc4-709
- 《iOS逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章