一弹指六十刹那, 一刹那九百生灭 -- 仁王经
组件
计算机是一种数据处理设备, 它由 CPU 和内存以及外部设备组成 CPU 负责数据处理, 内存负责存储, 外部设备负责数据的输入和输出, 它们之间通过总线连接在一起 CPU 内部主要由控制器运算器和寄存器组成控制器负责指令的读取和调度, 运算器负责指令的运算执行, 寄存器负责数据的存储, 它们之间通过 CPU 内的总线连接在一起每个外部设备 (例如: 显示器硬盘键盘鼠标网卡等等) 则是由外设控制器 I/O 端口和输入输出硬件组成外设控制器负责设备的控制和操作, I/O 端口负责数据的临时存储, 输入输出硬件则负责具体的输入输出, 它们间也通过外部设备内的总线连接在一起
上面的计算机系统结构图中我们可以看出硬件系统的这种组件化的设计思路总是贯彻到各个环节在这套设计思想 (冯. 诺依曼体系架构) 里面, 总是有一部分负责控制一部分负责执行一部分则负责存储, 它之间进行交互以及接口通信则总是通过总线来完成这种设计思路一样的可以应用在我们的软件设计体系里面: 组件和组件之间通信通过事件的方式来进行解耦处理, 而一个组件内部同样也需要明确好各个部分的职责(一部分负责调度控制一部分负责执行实现一部分负责数据存储)
缓存
一个完整的 CPU 系统里面有控制部件运算部件还有寄存器部件其中寄存器部件的作用就是进行数据的临时存储既然有内存作为数据存储的场所, 那么为什么还要有寄存器呢? 答案就是速度和成本我们知道 CPU 的运算速度是非常快的, 如果把运算的数据都放到内存里面的话那将大大降低整个系统的性能解决的办法是在 CPU 内部开辟一小块临时存储区域, 并在进行运算时先将数据从内存复制到这一小块临时存储区域中, 运算时就在这一小快临时存储区域内进行我们称这一小块临时存储区域为寄存器因为寄存器和运算器以及控制器是非常紧密的联系在一起的, 它们的频率一致, 所以运算时就不会因为数据的来回传输以及各设备之间的频率差异导致系统性能的整体下降你可能又会问为什么不把整个内存都集成进 CPU 中去呢? 答案其实还是成本问题! 因为 CPU 速度很快, 相应的寄存器也需要存取很快, 二者速度上要匹配, 所以这些寄存器的制作难度大, 选材精, 而且是集成到芯片内部, 所价格高而内存的成本则相对低廉, 而且从工艺上来说, 我们不可能在 CPU 内部集成大量的存储单元 运算的问题通过寄存器解决了, 但是还存在一个问题: 我们知道程序在运行时是要将所有可执行的二进制指令代码都装载到内存里面去, CPU 每执行一条指令前都需要从内存中将指令读取到 CPU 内并执行如果按这样每次都从内存读取一条指令来依次执行的话, 那还是存在着 CPU 和内存之间的处理瓶颈问题, 从而造成整体性能的下降这个问题怎么解决呢? 答案就是高速缓存其实在 CPU 内部不仅有为解决运算问题而设计的寄存器, 还集成了一个部分高速缓存存储区域高度缓存的制造成本要比寄存器低, 但是比内存的制造成本高, 容量要比寄存器大, 但是比内存的容量小很多虽然没有寄存器和运算器之间的距离那么紧密, 但是要比内存到运算器之间的距离要近很多一般情况下 CPU 内的高速缓存可能只有几 KB 或者几十 KB 那么大正是通过高速缓存的引入, 当程序在运行时, 就可以预先将部分在内存中要执行的指令代码以及数据复制到高速缓存中去, 而 CPU 则不再每次都从内存中读取指令而是直接从高速缓存依次读取指令来执行, 从而加快了整体的速度当然要预读取哪块内存区域的指令和数据到缓存上以及怎么去读取这些工作都交给操作系统去调度完成, 这里面的算法和逻辑也非常的复杂, 大家可以通过学习操作系统相关的课程去了解, 这里就不再展开了可以看出高速缓存的作用解决了不同速度设备之间的数据传递问题在实际中 CPU 内部可能不止设有一级高速缓存, 有可能会配备两级到三级的高速缓存, 越高级的高速缓存速度越快, 容量越低, 而越低级的高度缓存则速度越慢, 但是容量越大比如 iPhoneX 上的搭载的 arm 处理器 A11 里面除了固有的 37 个通用寄存器外, L1 级缓存的容量是 64KB, L2 级缓存的容量达到了 8M(这么大的二级缓存, 都有可能在你的程序代码少时可以一次性将代码读到缓存中去运行), 没有配备三级缓存
我们知道在软件设计上有一个所谓的空间换时间的概念, 就是当两个对象之间进行交互时因为二者处理速度并不一致时, 我们就需要引入缓存来解决读写不一致的问题比如文件读写或者 socket 通信时, 因为 IO 设备的处理速度很慢, 所以在进行文件读写以及 socket 通信时总是要将读出或者写入的部分数据先保存到一个缓存中, 然后再统一的执行读出和写入操作 可以看出无论是在硬件层面上还是在软件层面上, 当两个组件之间因为速度问题不能进行同步交互时, 就可以借助缓存技术来弥补这种不平衡的状况
指令中的寄存器
CPU 执行的每条指令都由操作码和操作数组成, 简单理解就是要对谁 (操作数) 做什么 (操作码) 在 CPU 内部要运算的数据总是放在寄存器中, 而实际的数据则有可能是放在内存或者是 IO 端口中因此我们的程序其实大部分时间就是做了如下三件事情:
把内存或者 I/O 端口的数据读取到寄存器中
将寄存器中的数据进行运算(运算只能在寄存器中进行)
将寄存器的内容回写到内存或者 I/O 端口中
这三件事情都是跟寄存器有关, 寄存器就是数据存储的中转站, 非常的关键, 因此在 CPU 所提供的指令中, 如果操作数有两个时至少要有一个是寄存器
; 下面部分是 arm64 指令示例:
mov x0, #0x100 ; 将常数 0x100 赋值给寄存器 x0
mov x1, x0 ; 将寄存器 x0 的值赋值给寄存器 x1
ldr x3, [sp, #0x8] ; 将栈顶加 0x8 处的内存值赋值给 x3 寄存器
add x0, x1, x2 ;x0 = x1 + x2 可以看出运算的指令必须放在寄存器中
- sub x0, x1, x2 ;r0 = x1 - x2
- str x1, [sp, #0x08] ; 将寄存器 x1 中的值保存到栈顶加 0x8 处的内存处
; 下面部分是 x64 指令示例(AT&T 汇编):
mov $0x100, %rax ; 将常数 0x100 赋值给寄存器 rax
mov %rax, %rbx ; 将寄存器 rax 的值赋值给 rbx 寄存器
movq 8(%rax), %rbx ; 将寄存器 rax 中的值 + 8 并将所指向内存中的数据赋值给 rbx 寄存器
所以不要将机器语言或者汇编语言当成是很复杂或者难以理解的语言, 如果你仔细观察一段汇编语言代码时, 你就会发现几乎大部分代码都是做的上面的三件事情我们在高级语言里面看到的只是变量, 但是在低级语言里面看到的就是内存地址和寄存器, 你可以将内存地址和寄存器也理解为定义的变量, 带着这样的思路去阅读汇编代码时你就会发现其实汇编语言也不是那么的困难在高级语言中我们可以根据自身的需要定义出很多有特殊意义的变量, 但是低级语言中因为寄存器就那么几个, 它必须要被复用和重复使用, 因此汇编语言中就会出现大量的将寄存器的内容保存到内存中的指令代码以及从内存中读取到寄存器中的指令代码这些代码中有很多都有共性, 只要在你实践中多去阅读, 然后适应一下就很快能够很高兴的去看汇编代码了, 熟能生巧吗
寄存器的分类
寄存器是 CPU 中的数据临时存储单元, 不同的 CPU 体系结构中的寄存器的数量是不一致的比如: arm64 体系下的 CPU 就提供了 37 个 64 位的通用的寄存器, 而 x64 体系下的 CPU 就提供了 16 个 64 位的通用寄存器在说分类之前要说一下寄存器的长度问题有时候我们看汇编代码时会发现代码中出现了 x0, w0(arm64); 或者 rax, eax, ax, al(x64) 它们之间有什么关系吗? 寄存器是存储单元, 意味着它具备一定的容量, 也就是每个寄存器能保存的最大的数值是多少, 也就是寄存器的位数不同 CPU 架构下的寄存器的位数有差别, 这个跟 CPU 的字长有关系一般情况下 64 位字长的 CPU 提供的寄存器的容量是 64 个 bit 位, 而 32 位字长的 CPU 提供的寄存器的容量是 32 个 bit 位比如 arm64 体系下的 CPU 提供的 37 个通用寄存器的容量都是 8 个字节的, 所以每个寄存器能保存的数值范围就是(0 到 2^64 次方)
对于 x64 系的 CPU 来说, 如果寄存器以 r 开头则表明的是一个 64 位的寄存器, 如果以 e 开头则表明是一个 32 位的寄存器, 同时系统还提供了 16 位的寄存器以及 8 位的寄存器 32 位的寄存器是 64 位寄存器的低 32 位部分并不是独立存在的, 16 位寄存器则是 32 位寄存器的低 16 位部分并不是独立存在的, 8 位寄存器则是 16 位寄存器的低 8 位部分并不是独立存在的
对于 arm64 系的 CPU 来说, 如果寄存器以 x 开头则表明的是一个 64 位的寄存器, 如果以 w 开头则表明是一个 32 位的寄存器, 在系统中没有提供 16 位和 8 位的寄存器供访问和使用其中 32 位的寄存器是 64 位寄存器的低 32 位部分并不是独立存在的
不管寄存器的长度如何, 它们有些用来存放将要执行的指令地址, 有些用来存储要运算的数据, 有些用来存储计算的结果状态, 有些用来保存内存的基地址信息, 有些用来保存要运算的浮点数因此 CPU 中的寄存器可以按照作用进行如下分类:
1. 数据地址寄存器
数据地址寄存器通常用来做数据计算的临时存储做累加计数地址保存等功能定义这些寄存器的作用主要是用于在 CPU 指令中保存操作数, 在 CPU 中当做一些常规变量来使用所以我们的代码里面看到的以及用到的最多的寄存器就是这些寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RAX,RBX,RCX,RDX,RDI,RSI, R8-R15 |
x64 | 32 | EAX,EBX,ECX,EDX,EDI,ESI, R8D-R15D |
x64 | 16 | AX,BX,CX,DX,DI,SI, R8W-R15W |
x64 | 8 | AL,BL,CL,DL,DIL,SIL, R8L-R15L |
arm64 | 64 | X0-X30, XZR |
arm64 | 32 | W0-W30, WZR |
如果你仔细观察一些汇编代码中的寄存器的使用, 其实你会发现一些特点:
在 x64 体系中 RAX 以及 arm64 体系中的 X0 一般都用来保存函数的返回值
在函数调用时的参数传递在 x64 体系中分别保存在 RDI,RSI,RDX,RCX,R8,R9...; 而在 arm64 体系中则分别保存在 X0,X1,X2,.... 中
arm64 体系中的 XZR,WZR 表示为一个特殊的寄存器, 就是用来表示 0
arm64 体系中的 X8 一般用来表示全局变量或者常量的偏移地址而 X16,X17 则有特殊的用途一般用来保存间接调用时的函数地址
arm64 中的 X29 寄存器特殊用于保存函数栈的基址寄存器(X29 也叫 FP), 所以一般不能用于其他用途
2.Intel 架构 CPU 的段寄存器
早期的 16 位实模式程序中的内存访问都是基于物理地址的, 而且还把整个程序拆分为数据段代码段栈段扩展段四个区域, 每个内存区段内的地址编码都是相对于这个段的偏移来设置的, 因此为了定位和区分这些内存区段, CPU 分别设置了 CS,DS,SS,ES 四个寄存器来保存这些段的基地址后来随着 CPU 和操作系统的发展, 应用程序不再直接访问物理内存地址了, 而是访问由操作系统提供的虚拟内存地址, 同时也不再把整个内存空间划分为数据段和代码段了, 而是提供一个从 0 开始的平坦连续的内存空间了, 同时将程序所能访问的内存区域和操作系统内核所能访问的内存区域进行了隔离, 我们称这样的程序为保护模式下运行的程序因此这时候里面的 CS,DS,SS,ES 寄存器的作用将不再用于保存内存区域的基地址了, 同时还增加了 FS,GS 两个寄存器, 这 6 个寄存器的作用变为了保存操作系统进入用户态还是核心态以及进行用户态和核心态之间进行切换上下文数据的功能了也就是在保护模式下运行的程序我们将不需要也没有权利去访问这些段寄存器了如果你想了解更加具体的内容请搜索: 全局描述符表与局部描述符表 相关的知识在 arm 体系的 CPU 中则没有专门提供这些所谓的段寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 16 | CS,DS,SS,ES,FS,GS |
这里面需要澄清的是我们的程序内存区域虽然从物理上不再划分为代码段数据段栈段几个独立的内存空间但是在平坦内存模式下我们依然保留了代码段数据段栈段的划分, 每个段的基地址都是从 0 开始, 只是各种类型的数据存放到了不同的内存空间中去了, 也就是说程序分段的机制由硬件划分转化为了软件划分了
3. 栈寄存器
栈的概念, 在学习数据结构的时候就已经有了解, 栈是一块具有后进先出功能的存储区域, 在进行操作时我们总是只能将数据压入栈顶, 或者将数据从栈顶弹出来
从上面可以看出要维护一个栈区域就必须要提供 2 个寄存器, 一个寄存器用来保存栈的基地址也就是栈的底部, 而一个寄存器则用来保存栈的偏移也就是栈的顶部在一般的系统中, 我们都将栈的基地址设置在内存的高位, 而将栈顶地址设置在内存的低位因此每当有进栈操作时则将栈顶地址进行递减, 而当有出栈操作时则将栈顶地址递增栈的这种特性, 使得他非常适合于保存函数中定义的局部变量, 以及函数内调用函数的情况 (具体栈和函数的关系我会在后续的文章中详细介绍) 在 x64 体系的 CPU 中, 提供了一个专门的 RBP 寄存用来保存栈的基地址, 同时提供一个专门的 RSP 寄存器来保存栈的栈顶地址; 而 arm64 体系的 CPU 中则没有设置专门的栈基址寄存器而是一般用 X29 寄存器来保存栈的基地址(至少在 iOS 的 64 位系统里面是如此的), 但是设置一个 SP 寄存器来保存栈的栈顶地址
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RBP 为栈基址寄存器,RSP 为栈顶寄存器 |
x64 | 32 | EBP 为栈基址寄存器,ESP 为栈顶寄存器 |
x64 | 16 | BP 为栈基址寄存器,SP 为栈顶寄存器 |
arm64 | 64 | X29 为栈基址寄存器,SP 为栈顶寄存器 |
arm64 | 32 | W29 为栈基址寄存器,WSP 为栈顶寄存器 |
4. 浮点和向量寄存器
因为浮点数的存储以及其运算的特殊性, 所以 CPU 中专门提供 FPU 以及相应的浮点数寄存器来处理浮点数, 除了一些浮点数状态和控制寄存器 (比如四舍五入的处理方式等) 外主要就是一些保存浮点数的寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 128 | XMM0 - XMM15 |
arm64 | 64 | D0 - D31 |
arm64 | 32 | S0 - S31 |
现在的 CPU 除了支持标量运算外, 还支持向量运算向量运算在图形处理相关的领域用得非常的多为了支持向量计算系统了也提供了众多的向量寄存器, 以及 SSE 和 SIMD 指令集:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 128 | XMM0 - XMM15, YMM0-YMM15, STMM0-STMM7 |
arm64 | 128 | V0-V31 |
5. 状态寄存器
状态寄存器用来保存指令运行结果的一些信息, 比如相加的结果是否溢出结果是否为 0 以及是否是负数等 CPU 的某些指令会根据运行的结果来设置状态寄存器的状态位, 而某些指令则是根据这些状态寄存器中的值来进行处理比如一些条件跳转指令或者比较指令等等我们在高级语言里面的条件判断最终在转化为机器指令时, 机器指令就是根据状态寄存器里面的特殊位置来进行跳转的 ** 在 x64 体系的 CPU 中提供了一个 64 位的 RFLAGS 寄存器来作为状态寄存器; arm64 体系的 CPU 则提供了一个 32 位的 CPSR 寄存器来作为状态寄存器 ** 状态寄存器的内容由 CPU 内部进行置位, 我们的程序中不能将某个数值赋值给状态寄存器
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RFLAGS |
arm64 | 32 | CPSR |
6. 指令寄存器(程序计数器)
我们知道程序代码是保存在内存中的, 那 CPU 又是如何知道要执行哪一条保存在内存中的指令呢? 这就是通过指令寄存器来完成的因为内存中的指令总是按线性序列保存的, CPU 只是按照编制好的程序来执行指令因此 CPU 内提供一个指令寄存器来记录 CPU 下一条将要执行的指令的内存地址, 这样每次执行完毕一条指令后, CPU 就根据指令寄存器中所记录的地址到内存中去读取指令并执行, 同时又将下一条指令的内存地址保存到指令寄存器中, 就这样就重复不断的处理来完成整个程序的执行
但是这里面有两问题:
前面不是说 CPU 内有高速缓存吗? 怎么又说每次都去访问内存呢? 而且保存还是内存的地址呢 这是没有问题的, 指令寄存器中保存的确实是下一条指令在内存中的地址, 但是操作系统除了将部分内存区域中的指令保存到高速缓存外还会建立一个内存地址到高速缓存地址之间的映射关系数据结构因此即使是指令寄存器中保存的是内存地址, 但是在指令真实执行时 CPU 就会根据指令寄存器中的内存地址以及内部建立的内存和高速缓存的映射关系来转化为指令在高速缓存中的地址来读取指令并执行当然如果发现指令并不在高速缓存中时, CPU 就会触发一个中断并告诉操作系统, 操作系统再根据特定的策略从内存中再次读取一块新的内存数据到高速缓存中, 并覆盖掉原先保存在高速缓存中的内容, 然后 CPU 再次读取高速缓存中的指令后继续执行
如果说指令寄存器每次都是保存的顺序执行指令的话那么怎么去实现跳转逻辑呢? 答案是跳转指令和函数调用指令的存在我们的用户态中的代码不能去人为的改变指令寄存器的值, 也就是不能对指令寄存器进行赋值, 因此默认情况下指令寄存器总是由 CPU 内部设置为下一条指令的地址, 但是跳转指令和函数调用指令例外, 这两条指令的主要作用就是用来改变指令寄存器的内容, 正是因为跳转功能才使得我们的程序可以不只按顺序去执行而是具有条件执行和循环执行代码的能力
在 x64 体系的 CPU 中提供了一个 64 位的指令寄存器 RIP, 而在 arm64 体系的 CPU 中则提供了一个 64 位的 PC 寄存器需要再次强调的是指令寄存器保存的是下一条将要执行的指令的内存地址, 而不是当前正在执行的指令的内存地址
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RIP |
x64 | 32 | EIP |
arm64 | 64 | PC, LR |
这里再看一下 arm64 体系下的 PC 和 LR 寄存器, 我们先看下面一张图:
从上面的图中我们可以看出 PC 寄存器和 LR 寄存器所表示的意义: PC 寄存器保存的是下一条将要执行的指令的内存地址, 而不是当前正在执行的指令的内存地址 LR 寄存器则保存着最后一次函数调用指令的下一条指令的内存地址那么 LR 寄存器有什么作用吗? 答案就是为了做函数调用栈跟踪, 我们的程序在崩溃时能够将函数调用栈打印出来就是借助了 LR 寄存器来实现的具体的实现原理我会在后面的文章里面详细介绍
7. 其他寄存器
上面列出的都是我们在编程时会用到的寄存器, 其实 CPU 内部还有很多专门用于控制的寄存器以及用于调试的寄存器, 这些寄存器一般都提供给操作系统使用或者用于 CPU 内部调试使用这里就不再进行介绍了, 感兴趣的同学可以去下载一本 x64 或者 arm 手册进行学习和了解
寄存器的编码
这里面需要澄清的是上述中的寄存器名称只是汇编语言里面对寄存器的一个别称或者有意义的命名, 我们知道机器指令是二进制数据, 一条机器指令里面无论是操作码还是操作数都是二进制编码的, 二进制数据太过晦涩难以理解, 所以才有了汇编语言的诞生, 汇编语言是一种机器指令的助记语言, 他只不过是以人类更容易理解的自然语言的方式来描述一条机器指令而已所以虽然上面的寄存器看到的是一个个字母, 但是在机器语言里面, 则是通过给寄存器编号来表示某个寄存器的还记得在我的介绍指令集的文章里面, 你有看到过里面的虚拟 CPU 里面的寄存器的定义吗:
- // 定义寄存器编号
- typedef enum : int {
- Reg0,
- Reg1,
- Reg2,
- Reg3
- } RegNum;
上面的枚举你可以看到我们在代码里面用 Reg0, Reg1... 来表示虚拟的寄存器编号, 但是实际的寄存器编号则分别为 0,1... 真实中的 CPU 的寄存器也是如此编号的, 我们来看下面一段代码, 以及其中的机器指令:
- mov x0, #0x0 ;0xD2800000
- mov x1, #0x0 ;0xD2800001
- mov x2, #0x0 ;0xD2800002
mov 指令的二进制结构如下:
可见上面的二进制机器指令中关于寄存器部分的字段 Rd 分别从 0 到 2 而出现了差异, 从而说明了寄存器读写的编码规则寄存器编码的机制和内存地址编码是同样的原理和机制, CPU 访问内存数据时总是要指定内存数据所在的地址, 同样 CPU 访问某个寄存器时一样的要通过寄存器编码来完成, 这些东西统统都体现在指令里面
寄存器的查看
上面分别介绍了两种不同 CPU 上的寄存器, 那么我们如何来查看和设置寄存器的内容呢? 在 XCODE 中可以很方便的在代码执行到断点时查看当前线程中的所有寄存器中内容 (请选择最左下角处的 all 表示显示所有变量) 我们可以通过下面两张图来查看所有的寄存的信息
上面两图中的左下角列出了执行到某个断点时所有寄存器的当前值, 你可以看到其中的通用寄存器 (General Purpose Registers) 浮点寄存器 (Floating Point Registers) 异常状态寄存器 (Exception State Registers) 中的数据通用寄存器中的每个寄存器默认都是一个 64 位长度的存储单元查看左下角的寄存器值唯一的缺点是你无法看出寄存器中的保存的数据的真实类型, 而只能干巴巴的看到 16 进制的数值其实你可以将寄存器理解一个个特殊定义的变量, 既然可以在 lldb 中通过 expr 或者 p 命令来显示某个变量的更加详细的信息, 那么也一样的可以显示某个寄存器当前保存的数据的详细信息通过看上面图片的右下角你可以看出,** 要想打印显示某个寄存器的内容, 我们在使用 expr 或者 po 时 只需要在显示的寄存器的前面增加一个 $ 即可 ** 比如下面的例子中我们分别显示模拟器下的 rdi, rsi 以及真机下的 x0 和 x1 寄存器中的内容:
- // 模拟器下
- expr -o -- $rdi
- expr (char*)$rsi
- // 真机下
- expr -o -- $x0
- expr (char*)$x1
- expr $r12 = 100; // 和变量一样你也可以手动改变寄存器的值
当你在某个 OC 方法内部断点并打印这两个寄存器的值时, 大多数情况下你会发现 rdi/x0 总是指向一个 OC 的 self 对象, 而 rsi/x1 则是这个方法的方法名没有错, 这是系统的一个规定: 在任何一个 OC 方法调用前都会将寄存器 rdi/x0 的值设置为调用方法的对象, 而将寄存器 rsi/x1 设置为方法的签名也就是方法的 SEL(具体的原因我会在后面的文章中详细说明原因)很可惜的是上面的这套读取和设置寄存器的语法在 swift 中就失效了, 当你要在 swift 中读取和写入寄存器的内容时你应该采用: register read 寄存器
register write 寄存器 值 的方式来读取和写入某个寄存器的值了, 比如下面的例子(lldb 中):
- register read x0 // 读取 x0 寄存器的值, 这里不再需要附加 $ 符号了
- register read // 读取所有寄存器的值
- register write x10 100 // 将寄存器的 x10 的值设置为 100
arm64 体系的 CPU 中虽然定义 X29,X30 两个寄存器, 但是你在 XCODE 上是看不到这两个寄存器的, 但是你能看到 FP 和 LR 寄存器, 其实 X29 就是 FP, X30 就是 LR
寄存器的复用
1. 线程切换时的寄存器复用
我们的代码并不是只在单线程中执行, 而是可能在多个线程中执行那么这里你就可能会产生一个疑问? 既然进程中有多个线程在并行执行, 而 CPU 中的寄存器又只有那么一套, 如果不加处理岂不会产生数据错乱的场景? 答案是否定的我们知道线程是一个进程中的执行单元, 每个线程的调度执行其实都是通过操作系统来完成也就是说哪个线程占有 CPU 执行以及执行多久都是由操作系统控制的具体的实现是每创建一个线程时都会为这线程创建一个数据结构来保存这个线程的信息, 我们称这个数据结构为线程上下文, 每个线程的上下文中有一部分数据是用来保存当前所有寄存器的副本每当操作系统暂停一个线程时, 就会将 CPU 中的所有寄存器的当前内容都保存到线程上下文数据结构中而操作系统要让另外一个线程执行时则将要执行的线程的上下文中保存的所有寄存器的内容再写回到 CPU 中, 并将要运行的线程中上次保存暂停的指令也赋值给 CPU 的指令寄存器, 并让新线程再次执行可以看出操作系统正是通过这种机制保证了即使是多线程运行时也不会导致寄存器的内容发生错乱的问题因为每当线程切换时操作系统都帮它们将数据处理好了下面的部分线程上下文结构正是指定了所有寄存器信息的部分:
- // 这个结构是 linux 在 arm32CPU 上的线程上下文结构, 代码来自于: http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h
- // 这里并没有保存所有的寄存器, 是因为 ABI 中定义 linux 在 arm 上运行时所使用的寄存器并不是全体寄存器, 所以只需要保存规定的寄存器的内容即可这里并不是所有的 CPU 所保存的内容都是一致的, 保存的内容会根据 CPU 架构的差异而不同
- // 因为 iOS 的内核并未开源所以无法得到 iOS 定义的线程上下文结构
- // 线程切换时要保存的 CPU 寄存器,
- struct cpu_context_save {
- __u32 r4;
- __u32 r5;
- __u32 r6;
- __u32 r7;
- __u32 r8;
- __u32 r9;
- __u32 sl;
- __u32 fp;
- __u32 sp;
- __u32 pc;
- __u32 extra[2]; /* Xscale 'acc' register, etc */
- };
- // 线程上下文结构
- struct thread_info {
- unsigned long flags; /* low level flags */
- int preempt_count; /* 0 => preemptable, <0 => bug */
- mm_segment_t addr_limit; /* address limit */
- struct task_struct *task; /* main task structure */
- __u32 cpu; /* cpu */
- __u32 cpu_domain; /* cpu domain */
- struct cpu_context_save cpu_context; /* cpu context */
- __u32 syscall; /* syscall number */
- __u8 used_cp[16]; /* thread used copro */
- unsigned long tp_value[2]; /* TLS registers */
- #ifdef CONFIG_CRUNCH
- struct crunch_state crunchstate;
- #endif
- union fp_state fpstate __attribute__((aligned(8))); /* 浮点寄存器 */
- union vfp_state vfpstate; /* 向量浮点寄存器 */
- #ifdef CONFIG_ARM_THUMBEE
- unsigned long thumbee_state; /* ThumbEE Handler Base register */
- #endif
- };
2. 函数调用时的寄存器复用
寄存器数据被切换的问题也同样会出现在函数的调用上, 举个例子来说: 假设我们正在调用 foo1 函数, 在 foo1 中我们的代码指令会用到 x0,x1,x2 等寄存器进行数据运算和存储假设我们在 foo1 中的某处调用 foo2 函数, 这时候因为 foo2 函数内部的代码指令也可能会用到 x0,x1,x2 等寄存器那么问题就来了, 因为 foo2 内部的执行会改变 x0,x1,x2 寄存器的内容, 那么当 foo2 函数返回并再次执行 foo1 下面的代码时, 就有可能 x0,x1,x2 等寄存器的内容被改动而跟原先的值不一致了, 从而导致数据错乱问题的发生那么这又是如何解决的呢? 解决的方法就是由编译器在编译出机器指令时按一定的规则进行编译(这是一种 ABI 规则, 什么是 ABI 后续我会详细介绍) 我们知道在高级语言中定义的变量无论是局部还是全局变量或者是堆内存分配的变量都是在内存中存储的编译为机器指令后, 对内存数据进行处理时则总是要将内存中的数据转移到寄存器中进行, 然后再将处理的结果写回到内存中去, 这种场景会发生在每次进行变量访问的情形中我们来看如下的高级语言代码:
- void foo2()
- {
- int a = 20;
- a = a + 2;
- int b = 30;
- b = b * 3;
- int c = a + b;
- }
- void foo1()
- {
- int a = 10;
- int b = 20;
- int c = 30;
- a += 10;
- b += 10;
- c += 10;
- foo2();
- c = a + b;
- }
虽然我们在 foo1 和 foo2 里面都定义了 a,b,c 三个变量, 但是因为这三个变量分别保存在 foo1 和 foo2 的不同栈内存区, 他们都是局部变量因此两个函数之间的变量是不会受到影响的但是如果是机器指令则不一样了, 因为运算时总是要将内存数据移动到寄存器中去, 但是寄存器只有一份因此解决的方法就是高级语言里面的每一行代码在编译为机器指令时总是先将数据从内存读取到寄存器中, 处理完毕后立即写回到内存中去, 中间并不将数据进行任何在寄存器上的缓存
从上面的代码对应关系可以看出, 每次高级语言的赋值处理总是先读取再计算然后再写回三步, 因此当调用 foo2 函数前, 所有寄存器其实都是处于空闲的或者可以被任意修改的状态而调用完毕函数后要访问变量时又再次从内存读取到寄存器, 运算完毕后再立即写回到内存中正是这种每次访问数据时都从内存读取到寄存器, 处理后立即再写会内存的机制就足以保证了即使在函数调用函数时也不会出现数据混乱的问题发生
上面是对寄存器复用的两种不同的策略: 空间换时间和时间换空间 在软件设计中当存在有某个共享资源被多个系统竞争或者使用时我们就可以考虑采用上面的两种不同方案来解决我们的问题
敬请期待下一篇:[深入 iOS 系统底层之机器指令介绍]
目录
深入 iOS 系统底层之汇编语言
深入 iOS 系统底层之指令集介绍
深入 iOS 系统底层之 XCODE 对汇编的支持介绍
深入 iOS 系统底层之 CPU 寄存器介绍
深入 iOS 系统底层之机器指令介绍
深入 iOS 系统底层之赋值指令介绍
深入 iOS 系统底层之函数调用介绍
深入 iOS 系统底层之其他常用指令介绍
深入 iOS 系统底层之函数栈介绍
深入 iOS 系统底层之函数栈 (二) 介绍
深入 iOS 系统底层之不定参数函数实现原理介绍
深入 iOS 系统底层之在高级语言中嵌入汇编语言介绍
深入 iOS 系统底层之常见的汇编代码片段介绍
深入 iOS 系统底层之 OC 中的各种属性以及修饰的实现介绍
深入 iOS 系统底层之 ABI 介绍
深入 iOS 系统底层之编译链接过程介绍
深入 iOS 系统底层之可执行文件结构介绍
深入 iOS 系统底层之 MACH-O 文件格式介绍
深入 iOS 系统底层之映像文件操作 API 介绍
深入 iOS 系统底层之知名 load command 结构介绍
深入 iOS 系统底层之程序加载过程介绍
深入 iOS 系统底层之静态库介绍
深入 iOS 系统底层之动态库介绍
深入 iOS 系统底层之 framework 介绍
深入 iOS 系统底层之基地址介绍
深入 iOS 系统底层之模块内函数调用介绍
深入 iOS 系统底层之模块间函数调用介绍
深入 iOS 系统底层之机器指令动态构造介绍
深入 iOS 系统底层之 crash 问题解决方法
深入 iOS 系统底层之无上下文 crash 解决方法
深入 iOS 系统底层之常用工具和命令的实现原理介绍
深入 iOS 系统底层之真实的 OC 类内存结构介绍
欢迎大家访问我的 github 地址和简书地址
来源: https://juejin.im/post/5a786c555188257a6854b18c