第四章 ARMv8寄存器
4.1 通用寄存器
AArch64运行模式提供31个64位的通用寄存器,这些寄存器在所有异常等级(EL)和时间段内都是可用的。编号从X0到X30。
从上图中我们注意到,支持AArch32运行模式的32位寄存器实际上是复用64位寄存器的低字。在AArch32运行模式实际操作中,有如下过程。
- 写入:在AArch32的运行模式下写入寄存器会清空64位寄存器的高32位。
- 读取:忽略高32位的数据,不同于写入过程,寄存器的高32位将保持不变。
4.2 AArch64特殊寄存器
除了31个通用寄存器,我们还有几个特殊寄存器。
注意:
实际上,寄存器并不是被命名为X31或W31,许多指令将31编码为0寄存器(WZR,XZR),还有一些指令,它们将数字31编码为堆栈指针(SP)。
当访问0寄存器时(WZR, XZR),所有对0寄存器的写入都会直接丢弃,对0寄存器的读取结果恒为0。
注意,64位的SP寄存器不会使用X前缀。如下表所示。
在ARMv8架构中,当运行在AArch64模式,异常的返回状态被写入对应异常等级的ELR寄存器和SPSR寄存器。
- ELR: Exception Link Register
- SPSR: Saved Process State Register
每一个异常等级都有自己的SP寄存器,但是并不用于存储返回状态。
4.2.1 0寄存器
当0寄存器作为源寄存器时,读取的结果恒为0,当作为目的寄存器时,写入的结果将被直接丢弃。不是所有,但大部分指令都可以使用0寄存器。
4.2.2 栈指针 SP寄存器
在ARMv8架构中,SP寄存器的选择在一定程度上与异常等级相互独立。默认情况下,处理异常时需要选择目标异常等级的SP寄存器。例如处理一个上升到异常等级为EL1的异常,需要选择SP_EL1。每一个异常等级都有自己的SP寄存器。
当处理器运行在AArch64模式,除了异常等级为EL0时,对SP寄存器可以有以下两种选择:
- 与当前异常等级对应的SP寄存器
- 异常等级为EL0时的SP寄存器(SP_EL0)
但是,EL0异常等级下只能使用SP_EL0。以上内容可以使用下表来概括:
上图中后缀t代表SP_EL0寄存器被选中,后缀h代表对应异常等级的SP_ELn寄存器被选中。
大多数指令都不能访问SP寄存器。但是一些数学运算指令可以访问,例如ADD指令,可以读写当前的栈指针,调整栈指针的位置。例如:
A D D S P , S P , # 0 x 10 ADD\ \ SP, SP ,\#0x10 ADD SP,SP,#0x10
4.2.3 PC 程序计数器
原ARMv7指令级的一个功能就是对R15 程序计数器(PC)的运用,作为一个通用寄存器,PC可以通过指令直接访问,一些程序员巧妙利用PC写出了一些技巧性的代码,但这同时也很会对编译器和复杂流水线的设计带来一些并发症。所以在ARMv8架构中,取消了对PC的直接访问,这样可以减少返回值的不确定性,简化ABI规范。
在ARMv8架构中PC不能通过寄存器命名显式地访问,但它可以通过特定地指令隐式地进行使用,例如与PC相关的加载和地址生成指令。PC不能作为数据处理指令或load指令的目的寄存器。
4.2.4 ELR Exception Link Register 异常连接寄存器
异常连接寄存器(ELR)保存有所在异常等级的返回地址。
4.2.5 SPSR 程序状态寄存器 Saved Process Status Register
当处理异常时,处理器的状态被保存在对应异常等级的程序状态寄存器中(SPSR),这一点与ARMv7类似。在异常处理之前,PSTATE(Processor State)的值被加载到SPSR中,当异常处理结束后,恢复PSTATE的值。寄存器的有效位定义如下图所示:
位编号 | 位命名 | 作用 |
---|---|---|
31 | N | 指示上一个运算结果为负数 |
30 | Z | 指示上一个运算结果为0 |
29 | C | 指示上一个运算结果产生了进位 |
28 | V | 指示上一个运算结果溢出 |
21 | SS | 软件步进,指示异常处理时软件步进是否启用 |
20 | IL | 非法异常状态位,在异常处理之前立刻显示PSTATE.IL的值 |
9 | D | 处理状态Debug掩码,表示发生在特定异常级别上的检测点、断点调试异常,以及软件步进调试事件是否被屏蔽 |
8 | A | 系统错误掩码位 |
7 | I | IRQ中断请求掩码位 |
6 | F | FIQ快速中断请求掩码位 |
4 | M | 处理器运行模式指示位,0表示运行在AArch64 |
0-3 | M[3:0] | 当前的模式或异常等级 |
在ARMv8架构中,使用哪一个SPSR取决于当前的异常等级,例如在EL1下使用的就是SPSR_EL1。至于什么时候选择使用哪一个SPSR,是由内核决定的。
注意:
寄存器对ELR_ELn和SPSR_ELn与对应的异常等级相关联,当跳转到更低等级的异常状态时寄存器内的数值保持不变。
4.3 处理器状态
AArch64运行模式下,当前程序状态寄存器(Current Program Status Register, CPSR)并不是与ARMv7直接等价。在AArch64运行模式下,传统的CPSR各位可以被独立访问,它们集中反映了处理器状态(Processor State,PSTATE)。
处理器状态,或者说PSTATE域的定义如下表所示:
通过对比上图和SPSR寄存器的未定义可以发现,两者基本是一一对应的关系,除了EL(2)部分位数的区别以及SP位在SPSR中并没有定义。
在AArch64运行模式下,可以通过执行ERET命令从一个异常中返回,并且这将导致SPSR_ELn的内容拷贝到PSTATE中,以恢复被异常中断前的数值运算单元标志位(ALU Flag),运行模式标志位,异常等级以及处理器分支。之后,我们可以从异常连接寄存器(ELR)中的地址处继续执行之前被异常中断的程序。
4.4 系统寄存器(Sytem registers)
在AArch64运行模式下,系统配置被系统寄存器(system registers)控制。系统寄存器可以通过MSR或MRS指令进行访问。这与ARMv7-A处理器有明显的不同之处,ARMv7通常通过协处理器15(CP15)访问系统寄存器。
系统寄存器的编号可以告知我们该寄存器可以被访问时的最低异常等级。例如:
- TTBR0_EL1可以在EL1,EL2和EL3异常等级进行访问。
- TTBR0_EL2可以从EL2和EL3异常等级进行访问。
这些带有ELn后缀的寄存器在所有可用异常等级上都有一个独立、保留的备份。绝大多数系统寄存器不能从EL0等级进行访问,但Cache Type Register (CTR_EL0)是一个例外。
系统寄存器通过MSR和MRS指令进行访问的示例如下所示: - MRS x0, TTBR0_EL1 // 从TTBR0_EL1中复制到x0
- MSR TTBR0_EL1, x0 // 从x0中复制到TTBR0_EL1
之前以带的ARM架构(ARMv7)使用协处理器进行系统配置。然而,AArch64架构不需要协处理器的辅助,下表中列出了用户需要关心的系统寄存器:
略,以后在各个章节会有讲解(原书第43页)
4.4.1 系统控制寄存器 SCTLR_ELn
系统控制寄存器(SCTLR)用于控制系统标准存储空间、系统设备以及提供系统内核执行功能的信息。
并不是所有位都在高于EL1的异常等级中可用,各个位的含意如下所示:
寄存器位名称 | 寄存器功能 | 寄存器取值的含义 |
---|---|---|
UCI | 当该位被设置为1,AArch64运行模式中将允许在EL0异常等级之下访问DC CVAU, DC CIVAC, DC CVAC,和 IC IVAU指令 | |
EE | 该寄存器对应异常等级的字节顺序 | 0: 小端,从小到大;1: 大端,从大到小 |
E0E | 在EL0下显式字节访问的字节顺序 | 0:在EL0中的显式数据访问采用小端模式;1:在EL0中的显式数据访问采用大端模式 |
WXN | 对写入允许使用从不执行(XN,execute never) | 0: 对可以写入的区域不使用“从不执行”;1:对可以写入的区域使用“从不执行” |
nTWE | 不拦截WFE | 1:WFE指令可以正常执行;0:拦截WFE指令 |
nTWI | 不拦截WFI | 1:WFI指令可以正常执行;0:拦截WFI指令 |
UCT | 当设置为1,使能在EL0异常等级下访问CTR_EL0寄存器 | |
DZE | 是否可以在EL0异常等级下使用DC ZVA指令 | 0:不允许执行;1:允许执行 |
I | 指令缓冲区使能(cache) | 这是对EL0和EL1异常等级下指令cache的使能位。访问可缓冲的普通内存空间的指令将被缓冲 |
UMA | 用户屏蔽使能 | 当在AArch64运行模式下处于EL0异常等级,该位可以用于控制是否可以访问中断的屏蔽位 |
SED | 禁止SETEND | 当运行在AArch32的EL0异常等级时,禁止SETEND指令 |
ITD | 禁止IT | 0:IT指令可用;1:IT指令被当作16位指令,只有另一个16位指令或32位指令的前半部分可以跟随,这取决于具体的应用 |
CP15BEN | CP15屏障使能 | 如果该位有效,它将使能AArch32运行模式下 CP15 DMB,DSB和ISB屏障操作 |
SA0 | 对EL0异常等级下使能栈对齐检查 | |
C | 数据缓冲区(cache)使能 | 这是对于EL0和EL1异常登记下数据cache的使能位,可缓冲普通内存区的数据将被缓冲 |
A | 对齐检查使能位 | |
M | 使能MMU |
如何访问SCTLR
- MRS , SCTLR_ELn // 将SCTLR中的内容读取到Xt
- MSR SCTLR_ELn, // 将Xt中的内容写入SCTLR
注意:
在使能cache之前,必须首先开启处理器在所有异常等级下的数据和指令使能。
4.5 字节顺序
有两种基本方式来查看内存中的字节:小端模式(LE)或大端模式(BE)。在大端模式下,权重更大的字节被存储在低地址,对应的,在小端模式下,权重小的字节被存储在低地址。所谓的低地址是指数字更靠近0的地址。大小端的含义可以用下图来表示:
在各个异常等级中数据的大小端模式可以单独控制。对于EL3,EL2和EL1,通过该异常等级下的SCTLR_ELn寄存器(系统控制寄存器)中的EE位来设置。那么EL0下数据的大小端应该如何设置呢?在前面的表格中,E0E位用于设置EL0的大小端模式,该拓展位只有EL1的系统控制寄存器(SCTLR_EL1)才有。在AArch64运行模式下,数据访问可以使用大端(BE)或者小端(LE0)格式,但是各个指令是以小端格式获取数据。
对于一个处理器是否同时支持大小端格式取决于处理器的具体实现(这里的意思应该是ARM核本身是同时支持大小端格式,但各个芯片厂商在拿到ARM的设计后,具体处理器实现起来究竟是支持哪种数据格式最终还是取决于各个厂商的设计)。如果支持小端模式,那么E0E和EE就固化为0,反之则固化为1。
当使用AArch32运行模式时,CPSR寄存器中的E位等效于系统控制寄存器(SCTLR)中的EE位,(不等价于E0E,或者说不包含EL0的原因可能是无法从EL0降低异常等级来到AArch32运行模式),但是两者会出现不一致的情况,现在CPSR.E已经弃用(ARM的设计缺陷?)。ARMv7的SETEND指令也已经被弃用。在ARMv8架构中使用SETEND指令可能会导致未定义指令异常,通过置位SCTLR.SED禁止SETEND来避免这种情况发生(v8并没有做到完全向后兼容)。
4.6 改变运行模式(重申)
前面我们提到过如何实现AArch64与AArch32之间的相互转换,现在我们从寄存器角度重新认识这件事。
当从AArch32运行模式的一个异常等级进入到AArch64运行模式的一个异常等级:
- 对于在低异常等级时AArch32可以访问的寄存器高32位的值在转换到AArch64运行模式后是未知的。(前面提到必须通过提高异常等级来从AArch32运行模式切换到AArch64运行模式,反之则必须降低异常等级来实现从AArch64到AArch32的转换)。
- 对于在AArch32运行模式下不可访问的寄存器,将会继续保持切换到AArch32之前的数值。
- 如果AArch32运行模式下的异常等级为EL2,那么来到AArch64后异常等级将提高到EL3,此时ELR_EL2的高32位是未知的。
- 在AArch32模式下无法访问的各个异常等级下的栈指针寄存器(SPs)和异常连接寄存器(ELRs)将保持它们切换到AArch32之前的值。(这一条与第二条有点重复?)符合该条规则的寄存器有:SP_EL0/1/2以及ELR_EL1。
- 总的来说,程序员在编写AArch32或AArch64应用程序时,只能且必须由操作系统承担起运行模式切换的任务。
4.6.1 AArch32运行模式下的寄存器
和ARMv7几乎相同,意味着AArch32实现了和ARMv7一致的优先级规则,但AArch32仅能处理ARMv7 32位的通用目的寄存器,因此,ARMv8必须提供对应的替代解决方案。
记得ARMv7架构中有16个(R0-R15)32位的通用寄存器供软件使用。其中前15个(R0-R14)可以用做数据存储,剩余的一个,R15,用作程序计数器(PC),PC中的值随着指令的执行而不断发生变化。除了通用寄存器,软件也可以访问CPSR(当前程序状态寄存器),SPSR中保存有上一个运行模式中CPSR的一份拷贝。当异常发生时,CPSR中的值被拷贝到对应异常模式的SPSR中(ARMv7)。
对于哪些寄存器可以在何时访问,取决于处理器的状态和正在运行的软件以及寄存器本身的性质。这种寄存器的可用性(accessibility)随运行环境而发生变化的现象被称作屏蔽(banking)。这些寄存器都是真实存在的有自己特定的物理地址,只有在进入特定的运行模式时才能被用户访问。
屏蔽(banking)被应用于ARMv7,初衷是用于减少异常状态下的延迟。然而,这也意味着我们需要更多的寄存器,实际运行时,根据运行模式,起作用的只有其中一部分。如下图所示:
作为对比,AArch64运行模式有31×64位的通用寄存器,它们在所有异常等级和时间段都可以被用户访问。运行状态的改变(AArch64和AArch32之间的相互切换)意味着AArch64的寄存器必须向AArch32(ARMv7)寄存器组进行必要的映射。映射关系如下如所示:
AArch64寄存器的高32位在AArch32运行模式下不可用,如果处理器运行在AArch32模式,它只使用32位的W寄存器(64位寄存器的前缀为X),此时寄存器与原ARMv7架构的32位寄存器是等价的。
如上图所示,SPSR寄存器和ELR_HYP是AArch32运行模式下额外的寄存器,仅可以使用系统指令进行访问,它们并没有映射到AArch64架构的通用寄存器的空间中。其他一些特殊寄存器在AArch64和AArch32中有如下的映射关系:
AArch32 | AArch64 |
---|---|
SPSR_svc | SPSR_EL1 |
SPSR_hyp | SPSR_EL2 |
ELR_hyp | ELR_EL2 |
下面的几个寄存器仅在AArch32模式中使用,然而,因为异常等级EL1需要在AArch64模式下运行,这些寄存器在切换到AArch64中的不可访问状态时仍会保持原来的值。
- SPSR_abt
- SPSR_und
- SPSR_irq
- SPSR_fiq
SPSR寄存器仅在AArch64运行模式下为了进行上下文切换而上升到更高的异常等级时才可以访问。
重申,如果一个异常处理使处理器从AArch32的异常等级来到AArch64的异常等级,此时AArch64 ELR_ELn的高32位为0。
4.6.2 AArch32运行模式下的处理器状态(PSTATE)
在AArch64运行模式下,传统的CPSR寄存器的各个位表示的就是可以被独立访问的处理器状态(PSTATE)域。在AArch23运行模式下,有额外的处理器域用于和原ARMv7架构的CPSR寄存器形成对应关系。
为了保证与ARMv7架构中的CPSR兼容,ARMv8加入了一些额外的处理器状态位,这些状态位仅有在AArch32模式下可以访问。
在前文中我们提到,E位已经被弃用,因为它不能与AArch64 EL1/2/3异常等级中的EE位保持一致。
4.7 NEON和浮点运算寄存器
除了通用寄存器意外,ARMv8也有32个128位浮点寄存器,标号位V0-V31,32号寄存器用于保存标量浮点指令的操作数,以及NEON操作的标量和向量操作数。
4.7.1 AArch64 中浮点寄存器的组织形式
在对标量进行操作的NEON和浮点运算指令中,浮点运算和NEON寄存器的行为与主要的通用寄存器类似。在浮点运算中只有寄存器的低位可以被访问,高位在读取寄存器时被忽略,在写入寄存器时被清零。标量浮点运算和NEON寄存器的命名规则中包含寄存器的有效位数信息,寄存器的编号n从0-31,如下表所示。
寄存器的浮点运算使用方式有三种,如下图所示,图中只给出了V0和V31作为例子:
上图所表示的意思是,根据存储数据的精度不同,寄存器的有效位数也会相应发生变化。
注意:
16位浮点也是支持的,但是只能从其他的精度转换过来(不能直接定义)。16位精度也不支持有关数据处理操作。
浮点运算的ADD指令会有一个F前缀,浮点运算的精度与寄存器有关,示例如下:
- FADD Sd, Sn, Sm // 单精度运算
- FADD Dd, Dn, Dm // 双精度运算
半精度浮点运算指令用于对两个不同精度的数据进行转换。 - FCVT Sd, Hn // 半精度到单精度的转换
- FCVT Dd, Hn // 半精度到双精度的转换
- FCVT Hd, Sn // 单精度到半精度的转换
- FCVT Hd, Dn // 双精度到半精度的转换
4.7.2 标量寄存器的尺寸
在AArch64中,整数标量寄存器的映射从ARMv7演化到了下图中的映射形式:
通过上图我们可以看到,在寄存器保存标量时,一共有5种组织形式,相邻组织形式之间相差两倍,这样的设计可以解决编译器对顶层代码自动向量化时产生的诸多问题。
通过上面的说明,我们可以发现如下的复用关系:
- 每一个Q寄存器的低64位可以被用作浮点和NEON运算的64位寄存器。
- 每一个Q寄存器的低32位也可以被用作浮点和NEON运算的32位寄存器。
- 每一个Q寄存器的低16位也可以被用作浮点和NEON运算的16位寄存器。
- 每一个Q寄存器的低8位也可以被用作浮点和NEON运算的8位寄存器。
注意:
在寄存器的诸多使用方式中都是优先使用低位,不使用的高位在读取时被忽略,在写入时被清零。
这种映射方式所造成的后果是:如果一段程序运行在AArch64模式下,在处理由AArch32运行模式下产生的双精度(64位,长整型)或单精度(32位,整型)数据时必须先解包(个人理解是AArch32用两个32位寄存器存储一个双精度的数据,AArch64在使用时需要先计算出对应的64位数据然后才能存储到128位寄存器中,32位单精度道理大概也类似,毕竟寄存器的长度并不统一,会有额外的读取和写入过程,至于半精度可能两种运行模式都会有解包的过程)
对于ADD指令的标量运算:
ADD Vd, Vn, Vm
如果寄存器的尺寸是32位,对应的加法运算指令是:
ADD Sd, Sn, Sm
对于整型标量的寄存器的命名方式如下表所示:
4.7.3 向量寄存器的尺寸
向量长度可以是64位,包含一个或更多元素;或者是128位,包含有两个或更多元素,如下图所示:
关于向量运算的ADD指令有如下示例:
- ADD Vd.T, Vn.T, Vm.T
对于32位,长度为4的向量,加法运算指令可以写作: - ADD Vd.4S, Vn. 4S, Vm.4S
当这些寄存器被用于特定的指令格式,寄存器的名字必须说明数据的具体组织形式。更具体地说,寄存器地名字中必须说明向量元素的位数,以及向量中包含多少元素。更多相关示例可以参照下表:
4.7.4 AArch32运行模式下的NEON寄存器
在AArch32运行模式下,两个较小的寄存器被封装在一起,构成一个较大的,例如D0和D1被封装到一起构成Q1,这导致了一些棘手的循环依赖,可能会导致编译器向量化循环结构的能力下降。具体组织形式可以参考下图:
AArch32运行模式中的浮点、高级SIMD(单指令多数据)寄存器被映射为AArch64的浮点、SIMD寄存器。这将允许浮点、NEON寄存器在具体应用程序或虚拟机中被更高层的系统软件解析,例如操作系统或超级管理员(管理程序)。
AArch64的V16-V31浮点、NEON寄存器在AArch32中不可访问,与通用寄存器一样,这些寄存器将继续保持从AArch64进入到AArch32运行模式之前的值。