gitbook/操作系统实战45讲/docs/410396.md
2022-09-03 22:05:03 +08:00

300 lines
23 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 46 | AArch64体系ARM最新编程架构模型剖析
你好我是LMOS。
在今天Andriod+ARM已经成了移动领域的霸主这与当年的Windows+Intel何其相似。之前我们已经在Intel的x86 CPU上实现了Cosmos今天我会给你讲讲ARM的AArch64体系结构带你扩展一下视野。
首先我们来看看什么是AArch64体系然后分析一下AArch64体系有什么特点最后了解一下AArch64体系下运行程序的基础包括AArch64体系下的寄存器、运行模式、异常与中断处理以及AArch64体系的地址空间与内存模型。
话不多说,下面我们进入正题。
## 什么是AArch64体系
ARM架构在不断发展现在它在各个领域都得到了非常广泛地应用。
自从Acorn公司于1983年开始发布第一个版本到目前为止有九个主要版本版本号由1到9表示。2011年Acorn公司发布了ARMv8版本。
ARMv8是首款支持64位指令集的ARM处理器架构它兼容了ARMv7与之前处理器的技术基础同样它也兼容现有的A32ARM 32bit指令集还扩充了基于64bit的AArch64架构。
下面我们一起来看看ARMv8一共定义了哪几种架构一共有三种。
1.**ARMv8-AApplication架构**支持基于内存管理的虚拟内存系统体系结构VMSA支持A64、A32和T32指令集主打高性能在我们的移动智能设备中广泛应用。
2.**ARMv8-RReal-time架构**支持基于内存保护的受保护内存系统架构PMSA支持A32和T32指令集一般用于实时计算系统。
3.**ARMv8-MMicrocontroller架构**是一个压缩成本的嵌入式架构而且需要极低延迟中断处理。它支持T32指令集的变体主打低功耗一般用于物联网设备。
今天我们要讨论的AArch64它只是ARMv8-A架构下的一种执行状态“64”表示内存或者数据都保存在64位的寄存器中并且它的基本指令集可以用64位寄存器进行数据运算处理。
## AArch64体系的寄存器
一款处理器要运行程序和处理数据必须要有一定数量的寄存器。特别是基于RISC精简指令集架构的ARM处理器寄存器数量非常之多因为大量的指令操作的就是寄存器。
ARMv8-AArch64体系下的寄存器简单可以分为以下几类。
1.通用寄存器
2.特殊寄存器
3.系统寄存器
下面我们分别来看看这三类寄存器。
### 通用寄存器R0-R30
首先来看通用寄存器general-purpose registers通用寄存器一共为31个从R0到R30这个31个寄存器可以作为全64位使用也可以只使用其中的低32位。
全64位的寄存器以x0到x30名称进行引用用于32位或者64位的整数运算或者64位的寻址低32位寄存器以W0到W30名称进行引用只能用于32位的整数运算或者32位的寻址。为了帮你理解我还在后面画了示意图。
![](https://static001.geekbang.org/resource/image/1c/d2/1c8535a89b4e32a8c717fd0c89a350d2.jpg?wh=2655x977 "register_common")
通用寄存器中还有32个向量寄存器SIMD编号从V0到V31。因为向量计算依然是数据运算类的所以要把它们归纳到通用寄存器中。每个向量寄存器都是128位的但是它们可以单独使用其中的8位、16位、32位、64位它们的访问方式和索引名称如下所示。
* Q0到Q31为一个128-bit的向量寄存器
* D0到D31为一个64-bit的向量寄存器
* S0到S31为一个32-bit的向量寄存器
* H0到H31为一个16-bit的向量寄存器
* B0到B31为一个8-bit的向量寄存器
![](https://static001.geekbang.org/resource/image/2b/c4/2b3d38cdac2e9cfd5ba9b86db00493c4.jpg?wh=5490x2205 "register_simd")
### 特殊寄存器
**特殊寄存器**spseical registers比通用寄存器稍微复杂一些它还可以细分包括程序计数寄存器PC栈指针寄存器SP异常链接寄存器ELR\_ELx程序状态寄存器PSTATE、SPSR\_ELx等。
![](https://static001.geekbang.org/resource/image/b7/0a/b7c24e632afd125f566b2b20dc59ef0a.jpg?wh=3060x1360 "special_registers")
**PC寄存器**
PC寄存器保存当前指令地址的64位程序计数器指向即将要执行的下一条指令CPU正是在这个寄存器的指引下一条一条地运行代码指令。在ARMv7上PC寄存器就是通用寄存器R15而在ARMv8上PC寄存器不再是通用寄存器不能直接被修改只可以通过隐式的指令来改变例如PC-relative load。
![](https://static001.geekbang.org/resource/image/9d/48/9dc67becc01e9f35d803f2575e74b648.jpg?wh=3276x891 "PC寄存器")
**SP寄存器**
SP是64位的栈指针寄存器可以通过**WSP**寄存器访问低32位在指令中使用SP作为操作数表示使用当前栈指针。C语言调用函数和分配局部变量都需要用栈栈是一种后进先出的内存空间而SP寄存器中保存的就是栈顶的内存地址。
![](https://static001.geekbang.org/resource/image/50/a7/507e60c2882be5fdaa4d59536f022fa7.jpg?wh=3276x891 "SP寄存器")
**ELR\_ELx异常链接寄存器**
每个异常状态下都有一个ELR\_EL寄存器ELR\_ELx 寄存器是异常综合寄存器或者异常状态寄存器 负责保存异常进入Elx的地址和发生异常的原因等信息。
该寄存器只有ELR\_EL1、ELR\_EL2、ELR\_EL3这几种没用ELR\_EL0寄存器因为异常不会routing(target)到EL0。例如16bit指令的异常、32bit指令的异常、simd浮点运算的异常、MSR/MRS的异常。
![](https://static001.geekbang.org/resource/image/ed/3f/ed1f8f2yy4f3bb89abb911b82f120d3f.jpg?wh=3276x891 "ELR_ELx寄存器")
**PSTATE**
PSTATE不是单独的一个寄存器而是保存当前PEProcessing Element状态的一组寄存器统称其中可访问寄存器有NZCV、DAIF、CurrentEL、SPSel。这些属于ARMv8新增内容在64bit下可以代替CPSR32位系统下的PE信息
```
type ProcState is (
// PSTATE.{N, Z, C, V} 条件标志位这些位的含义跟之前AArch32位一样分别表示补码标志运算结果为0标志进位标志带符号位溢出标志
bits (1) N, // Negative condition flag
bits (1) Z, // Zero condition flag
bits (1) C, // Carry condition flag
bits (1) V, // oVerflow condition flag
// D表示debug异常产生比如软件断点指令/断点/观察点/向量捕获/软件单步 等;
// A, I, F表示异步异常标志异步异常会有两种类型一种是物理中断产生的包括SError系统错误类型包括外部数据终止IRQ或者FIQ
// 另一种是虚拟中断产生的这种中断发生在运行在EL2管理者enable的情况下vSErrorvIRQvFIQ
bits (1) D, // Debug mask bit [AArch64 only]
bits (1) A, // Asynchronous abort mask bit
bits (1) I, // IRQ mask bit
bits (1) F, // FIQ mask bit
// 异常发生的时候通过设置MDSCR_EL1.SS 为 1启动单步调试机制
bits (1) SS, // Software step bit
// 异常执行状态标志,非法异常产生的时候,会设置这个标志位,
bits (1) IL, // Illegal execution state bit
bits (2) EL, // Exception Level (see above)
// 表示当前ELx 所运行的状态分为AArch64和AArch32:
bits (1) nRW, // not Register Width: 0=64, 1=32
// 某个ELx 下的堆栈指针EL0下就表示sp_el0
bits (1) SP, // Stack pointer select: 0=SP0, 1=SPx [AArch64 only]
)
```
**SPSR\_ELx 程序状态寄存器**
程序在运行中处理大量数据无非是进行各种数学运算而数学运算的结果往往有各种状态如进位、结果为0、结果是负数等还有程序的运行状态是否允许中断CPU的工作模式这些信息都保存在程序状态寄存器中即PSTATE中。
但是当CPU处理异常时进程相应的ELx状态不同就要把PSTATE状态信息保存在ELx状态下对应的SPSR\_ELx寄存器中。SPSR\_ELx寄存器的格式如下所示。
![](https://static001.geekbang.org/resource/image/cd/yy/cd75b6cb85b6d20e85698e62602dd0yy.jpg?wh=3276x891 "register_SPSR_ELx")
### 系统寄存器
最后ARM的CPU上还有一些系统寄存器用于访问系统配置。
在EL0状态下大多数系统寄存器是不可访问的但是部分系统寄存器可以在EL0状态下进行访问比如Cache ID 寄存器用于EL0状态下缓存管理、调试寄存器用于代码调试如MDCCSR\_EL0、DBGDTR\_EL0等、性能监控寄存器和时钟寄存器等。
## ARM-A Arch64体系下CPU的工作模式
其实AArch64、AArch32体系都是简称从严格意义上说它们应该是处理器的两种执行方式或者状态。AArch64体系执行A64指令集这个指令集是全64位的AArch32体系则可以执行A32指令集和T32指令集这节课我们不关注这个体系所以这些指令集暂不深究
不管是AArch64体系还是AArch32体系ARM CPU的工作模式并没有差异。为了让你把握重点我们后面只是以AArch64体系为例探讨ARM处理器的工作模式。
### 工作模式分类
前面我们介绍了x86 CPU的[工作模式](https://time.geekbang.org/column/article/375278)但是x86 CPU的工作模式和ARM的CPU的工作差别很大x86 CPU的工作模式包括特权级、处理器位宽、内存的访问与保护。
ARM CPU工作模式则有些不同究竟有哪些不同呢
ARM的CPU一共有7种不同工作模式根据权限和状态以及进入工作模式的方法等方面的不同我为你用表格的方式做了梳理。
![](https://static001.geekbang.org/resource/image/6f/23/6f7eea3b6c03d81c12f6ab81974ca123.jpg?wh=1666x942 "工作模式梳理")
虽然看起来比较多但是还是比较好归纳的在7种模式中除了用户模式之外的模式被统称为**Privileged Modes**(特权模式)。
首先,我们大多数的应用程序是运行在用户模式下的,在用户模式下,是不能够访问受保护的系统资源的。此外,应用程序也无法进行处理器模式的切换的。这样就做到了应用程序和内核程序的权力分隔,确保应用程序不能破坏操作系统。
一旦代码的执行流切换到特权模式下其代码就可以访问全部的系统资源了代码也可以随时进行处理器模式的切换。而且只有在特权模式下CPU的部分内部寄存器才可以被读写。这里的代码就是指内核代码。
其次,系统模式也是特权模式,代码也是可以访问全部系统资源,也可以随时进行处理器模式的切换,主要供操作系统任务使用。系统模式和用户模式可以访问到的寄存器是同一套的,区别就是它是特权模式,不受用户模式的限制,一般系统模式用于调用操作系统的系统任务。
最后,特权模式下,除系统模式之外的其他五种模式就是异常模式。异常模式一般是在用户的应用程序发生中断异常时,随着特定的异常而进入的,比如之前我们讲过的硬件中断和软件中断,每种异常模式都有对应的一组寄存器,用来保证用户模式下的状态不被异常破坏。这样可以大大减小处理异常的时间,因为不用保存大量用户态寄存器。
### 处理器如何切换工作模式
前面我们已经了解了ARM架构下CPU的几种工作模式那么CPU的工作模式是如何切换的呢
工作模式切换大概分两种情况一是软件控制通过修改相应的寄存器或者执行相应的指令二是当外部中断或是异常发生时也会导致CPU工作模式的切换。
那么当CPU发生中断或者异常时CPU进入相应的异常模式时以下工作由CPU自动完成。
1.在异常模式的R14中保存前一个工作模式里下一条即将执行的指令地址
2.将CPSR的值复制到异常模式的SPSR中
3.将CPSR的工作模式设为该异常模式对应的工作模式
4.令PC值等于这个异常模式在异常向量表中的地址即跳转去执行异常向量表中的相应指令。
处理完中断或者异常,就需要从中断或者异常中返回到发生中断或者异常的位置,继续执行程序。这个从异常工作模式退回到之前的工作模式时,需要由软件来完成后面这两项工作。
1.将异常模式的R14减去一个适当的值4或8赋给PC寄存器
2.将异常模式SPSR的值赋给CPSR
好了以上就是CPU切换工作的细节有了这个基础接下来我们一起看看AArch64体系下CPU是如何处理中断或者异常的。
## AArch64体系如何处理中断
现在我们来看看AArch64体系是如何处理中断的首先我们要搞清楚中断和异常的区别然后了解它们的处理过程最后再研究一下中断向量表。
### 异常和中断
有时候我们习惯于把异常Exception和中断Interrupt理解成一回事儿。但是对ARM来说官方文档用了Exception这个术语来描述广义上的中断包括异常Exception和中断InterruptException和Interrupt的执行机制都是一样的只是触发方式有区别。
这里的异常,切入的视角是处理器**被动接收**到了异常。异常通常表现为错误比如CPU执行了未知指令但CPU明显不能执行这个指令所以就会产生错误。再比如说CPU访问了不能访问的内存这也是错误的。你会发现共同点是异常都是同步的不修改程序下次同样会发生。
而中断对应的视角是处理器**主动申请**你可以当作是异步的异常因外部事件产生。中断分为三种它们分别是IRQ、FIQ和SError。IRQ、FIQ通常是连接到外部中断信号当外部设备发出中断信号时CPU就能对此作出响应并处理外部设备需要完成的操作。
### 中断处理
我们在了解中断处理之前,首先要搞明白异常级别。
在全局ARMV8-A体系结构中定义了四个异常级别Exception Level从EL0到El3每个异常级别的权限不同你不妨想像一下x86 CPU的R3R0特权级。
只不过ARMV8-A体系结构下EL0为最低权限模式也就是对应用户态处理的是应用程序EL1处理的是OS内核层对应的是内核态EL2是Supervisor模式处理的则是可以跑多个虚拟OS内核的管理软件对应的是虚拟机管理态它是可选的如Hypervisor用于和virtualization扩展EL3运行的是安全管理Secure Monitor处理的是监控态用于security扩展。
开发通用的操作系统内核只需要使用到EL1EL2两个异常级别我为你画了一幅EL模型图如下所示。
![](https://static001.geekbang.org/resource/image/5f/f7/5fa247a1cbfcd7e8832db9215a92d3f7.jpg?wh=4555x1955 "EL模型图")
现在我们来看看中断或者异常发生时EL级别的切换这里分为两种情况。
第一种是高级别向低级别切换这种方式通过修改PSTATE寄存器中的值来实现EL异常级别就保存在这个寄存器中第二种是低级别向高级别切换通过触发中断或者异常的方式进行切换的。
在这两种切换过程中如果高级的状态是AArch64低级的可以是AArch64或者AArch32也就是可以向下兼容如果高级的是AArch32那么低级的也一定要是AArch32。
当一个中断或者异常触发后CPU的操作流程如下所示。
1.更新SPSR\_ELx寄存器即当前的PSTATE寄存器的信息存储在SPSR\_ELx寄存以便中断结束时恢复到 PSTATE 寄存器。
2.更新PSTATE寄存器以反映新的处理器状态这个过程中中断级别可能会发生变化。
3.发生中断时的下一条指令地址存储在 ELR\_ELx寄存器中以便中断返回后能继续运行。
4.当中断处理完成后由高级别返回低级别时需要使用ERET指令返回。
下图能帮你更加清楚地理解这一行为。
![](https://static001.geekbang.org/resource/image/67/9f/672238298e113e758de6c3a35ab5fc9f.jpg?wh=3287x3631 "Interrupt流程")
上图已经清楚地展示了,中断或者异常发生时,其中几个关键寄存器是如何保存和恢复的。
### 中断向量表
当中断或者异常发生后CPU进行相应的操作后必须要跳转到相应的地址开始运行相应的代码进行中断或者异常的处理这个地址就是**中断向量**。由于有多个中断或者异常,于是就形成了**中断向量表**。
在AArch64中每个中断或者异常触发时会产生EL级别切换。通常在EL0级别调用svc指令触发一个同步异常CPU则会切换到EL1级别如果在EL0级别来了一个IRQ或FIQ就会触发一个异步中断CPU会根据SCR寄存器中的中断配置来决定切换EL1或EL2或EL3级别同时也会区分EL级别使用的是AArch64还是AArch32的指令集。
16个向量的分类和偏移地址在向量表中的关系如下所示。
![](https://static001.geekbang.org/resource/image/0c/57/0cd64d27966ef5d7ce16c0e04555cf57.jpg?wh=1660x893)
上表中分了四个小表小表中的每一个entry由不同的中断的类型IRQFIQSErrorSynchronous决定。具体使用哪一个小表由以下几个条件决定。
1. 如果中断发生在同一中断级别并且使用的栈指针是SP\_EL0则使用SP\_EL0这张表。
2. 如果中断发生在同一中断级别并且使用的栈指针是SP\_EL1/2/3则使用SP\_EL这张表。
3. 如果中断发生在较低的中断级别使用的小表则为下一个较低级别AArch64或AArch32的执行状态。
有了这些硬件机制的支持,就可以完美支持现代意义中的操作系统了。
## AArch64体系如何访问内存
无论是操作系统内核代码还是应用程序代码它们都是放在内存中的CPU要执行相应的代码指令就要访问内存。访问内存有两大关键**一是寻址,这表现为内存的地址空间;第二个关键点是内存空间的保护,即内存地址的映射和转换**。下面我分别解读一下这两个关键点。
### AArch64体系下的地址空间
对于工作在AArch64体系下的CPU来说没有启动MMU的情况下ARM的CPU发出的地址就是物理地址直接通过这个寻址内存空间。
但是你别以为AArch64体系下有64位的寄存器能发出64位的地址就一定能寻址64位地址空间的内存。其实实际只能使用52位或者48位的地址这里我们只讨论使用48位地址的情况。如果启用了MMU那么CPU会通过虚拟地址寻址MMU负责将虚拟地址转换为物理地址进而访问实际的物理地址空间。这个过程如下图所示。
![](https://static001.geekbang.org/resource/image/dc/76/dc1ebb7c6f7a5f955b310215c470ce76.jpg?wh=3450x3395 "AArch64虚拟地址空间")
上图中可以发现如果CPU发出的虚拟地址在0x00x0000ffffffffffff范围内MMU就会使用TTBR0\_ELx寄存器指向的地址转换表进行物理地址的转换如果CPU发出的虚拟地址在0xffff0000000000000xffffffffffffffffMMU使用TTBR1\_ELx寄存器指向的地址转换表进行物理地址的转换。
究竟虚拟地址是如何转换成物理地址的呢?我们接着往下看。
### AArch64体系下地址映射和转换
按照我们以往的经验来看,这里肯定是有一张把虚拟地址转化为物理地址的表,给出一个虚拟地址,通过查表就可以查到物理地址。但是实际过程却不是这么简单,在这里通常要有一个多级的查表过程。
MMU将虚拟地址映射到物理地址是以页Page为单位的ARMv8架构的AArch64体系可以支持48位虚拟地址并配置成4级页表4K页或者3级页表64K页
例如虚拟地址0xb7001000~0xb7001fff是一个页可能被MMU映射到物理地址0x2000~0x2fff物理内存中的一个物理页面也称为一个页框Page Frame
那么MMU执行地址转换的过程是怎样呢我们看一看4K页表的情况下虚拟地址转换物理地址的逻辑图。
![](https://static001.geekbang.org/resource/image/13/09/1342ba009a4b273165d64db8d602e309.jpg?wh=4960x1975 "虚拟地址转化")
结合上图我们看到首先要将64位的虚拟内存分成多个位段这些位段就是用来索引不同级别页表中的entry的。那么MMU是如何具体操作的呢一共分五步。
第一步从虚拟地址位段\[47:39\]开始用来索引0级页表0级页表的物理基地址存放在TTBR\_ELx寄存器中以虚拟地址位段\[47:39\]为索引找到0级页表中的某个entry该entry会返回1级页表的基地址。
第二步接着之前找到的1级页表的基地址现在可以用虚拟地址位段\[38:30\]索引到1级页表的某个entry该entry在4KB页表情况下返回的是2级页表的基地址。
然后到了第三步有了2级页表基地址就可以用虚拟地址位段\[29:21\]作为索引找到2级页表中的某个entry该entry返回3级页表的基地址。
再然后是第四步有了3级页表基地址则用虚拟地址位段\[20:12\]作为索引找到3级页表中的某个entry该entry返回的是物理内存页面的基地址。
最后一步,我们得到物理内存页面基地址,用虚拟地址剩余的位段\[11:0\]作为索引就能访问到4KB大小的物理内存页面内的某个字节了。
这个过程从TTBR\_ELx寄存器开始到0级页表接着到1级页表然后到2级页表再然后到3级页表最终到物理页面CPU一次寻址其实是五次访问物理内存。这个过程完全是由硬件处理的每次寻址时MMU就自动完成前面这五步不需要我们编写指令来控制MMU但是我们要保证内核维护正确的页表项。
有了MMU硬件转换机制操作系统只需要控制页表就能控制内存的映射和隔离了。
## 总结
这节课我们一起了解了ARM的AArch64体系它是ARMV8-A下的一种执行状态。作为首款支持64位的处理器架构AArch64体系不只是32 位ARM 构架的兼容扩展还引入了新的A64指令集。
处理器想要运行程序、处理数据离不开各种寄存器。我们学习了AARch64下的三类寄存器包括通用寄存器、特殊寄存器和系统寄存器。
相比x86系统AArch64的CPU工作模式更加多样一共有七种工作模式。之后我们分别研究了工作模式切换还有基于EL0-3的异常中断处理以及AArch64下的内存架构和访问方式。访问内存你重点要掌握的是访问内存的两大关键点**一是寻址,二是内存空间的保护**。
自从2011年ARM发布首款支持64位的ARMv8版本后到现在已经过去了十年。在今年ARM也宣布了下一代芯片架构ARMv9的部分技术细节并称其为十年来最大的创新也将是未来十年内千亿级别芯片的基础其在CPU性能、安全性、AI支持上有了显著提升。
但是ARMv9不会像ARMv7到ARMv8的根本性的执行模式和指令集的变化ARMv9继续使用AArch64作为基准指令集但是在其功能上增加了一些非常重要的扩展ARMv9开发的处理器预计将在2022年正式面世让我们拭目以待
## 思考题
请问ARMv8有多少特权级每个特权级有什么作用
欢迎你在留言区记录你的思考,也欢迎把这节课分享给有需要的朋友。
我是LMOS我们下节课见。