avatar

Catalog
Arm-v8架构简介

前言

Arm-v8架构是在2011年10月宣布的,至今也有10年了,但网上大部分关于Arm处理器的内容主要是基于早期经典的Arm-v6架构下的,甚至Arm-v7架构下的资料都很少,关于这两个架构的部分已经在前一篇进行了整理,也是对网上关于Arm寄存器最常见的一些说法进行了总结和筛选。本篇,目标移至目前主流的Arm-v8架构进行简要介绍(由于内容太多了,指令的部分被阉割了),主要内容均参考这本2015年发布的Arm白皮书手册

新增特性

禁止直接修改PC

在x86架构中,直接修改EIP(Extended Instruction Pointer,该寄存器指向即将执行的指令,类似Arm处理器中的PC)的值是不被允许的,只能通过间接的方式来实现地址的跳转,例如call, jmp, jcc。但在Arm架构的处理器下,汇编代码是可以直接修改PC(R15)寄存器的值的,这确实给了程序设计者更大的自由,对于艺高胆大的人来说,甚至可以玩出多种花样来。

然而,这种自由的代价就是增加了编译器的复杂度,也阻碍了更深的指令流水线的设计,最终在Arm-v8架构中,直接访问PC的方式也同x86一样,被禁止了

异常级别

在Arm-v8架构下,新增了异常级别(Exception Level)EL0~EL4,用来整合早期架构中的运行模式(Arm-v7架构中的9种运行模式,除了Usr模式以外均为特权模式)与特权级别PLn(Privilege Level,与特权模式对应,只有Usr模式属于非特权级别,其余模式均为特权级别,大多数情况下,软件都运行在用户模式)。有点类似x86架构上的Ring0-Ring3,不同的是x86架构上,Ring3权限最小,Ring0权限最大,并且Ring1-Ring2通常不被使用。Arm-v8架构的异常级别则是,EL4权限最大,EL0权限最小。

有了异常级别的概念后,运行模式与特权级别的概念也就淡化了,EL0属于非特权级别,EL1/2/3属于特权级别。异常级别的使用模型如下:

异常级别 运行的软件
EL0 Normal user applications
EL1 Linux OS kernel
EL2 Hypervisor(虚拟机监视器,可以跑多个虚拟OS)
EL3 Low-level firmware, including the Secure Monitor

总的来说,任何一个软件,不论是应用程序、操作系统内核还是虚拟机监视器,都只能运行在一个异常等级中。但是这个机制也有例外,对于内核虚拟机监视器(in-kernel hypervisor),例如KVM,就可以横跨EL2和EL1这两个异常级别运行。

安全模型

Arm-v8架构提供2种安全状态,Secure与Non-secure。Non-secure状态也就是常说的Normal World,Secure状态则对应Secure World。Arm-v8的安全模型基本沿用了Arm-v7 security extension的思路,主要目的保护一些安全应用的数据,例如指纹信息,人脸信息以及加密用的私钥等。该模型不同于特权级别等软件逻辑上的保护,而是一种物理上的隔离,即不同安全状态下,可以访问的物理内存是不同的。和Arm-v7架构一样,Secure Monitor起到看门的作用,用于隔离Normal World和Secure World。

执行状态

Arm-v8架构定义了两种执行状态,AArch64与AArch32。这两种执行状态用的是同一套通用寄存器(General-Purpose Registers),AArch32状态是对Arm-v7架构的兼容,它只使用通用寄存器低32位那部分。当处于Arm-v8架构的AArch32状态下,保留了Arm-v7架构定义的特权级别,而在AArch64状态下,特权级别将被异常级别所替代。因此,在执行状态进行转换的时候,就需要根据ELn与PLn的对应关系了。

当处于AArch64状态时,处理器执行的是A64指令集。当进入AArch32状态,处理器会执行A32或T32(Thumb)指令集种的一种。

下图展示了异常级别在不同执行状态下的组织结构:

寄存器组织(AArch64)

寄存器组织这里主要以AArch64执行状态为主,AArch32与Arm-v7架构类似,在最后一部分会提到,关于Arm-v7架构的寄存器组织可以先参考前一篇文章进行了解。

通用寄存器

首先是通用寄存器,Arm-v8架构有31个通用寄存器X0-X30,而Arm-v7架构仅有16个通用寄存器R0-R15。下面按组介绍各通用寄存器的功能:

  • 参数寄存器(X0~X7):

    通常用来传递函数的参数或是保存函数返回值,最多可以用来暂存函数的前8个参数。

  • 易失性寄存器(X9~X15):

    Caller-saved temporary registers,顾名思义,调用者需要保存的临时寄存器,也叫做易失性寄存器。当调用者试图调用子程序时,如果想继续使用这几个寄存器中的值,就需要在调用前,将这几个寄存器的值暂存到堆栈中。如果子程序中修改了这几个寄存器的值,在返回前,是不会恢复这几个寄存器中的值的。

  • 非易失性寄存器(X19~X28):

    Callee-saved registers,同理,被调用的子程序需要保存的寄存器,也叫做非易失性寄存器。在进入子程序时,子程序需要先将这些寄存器保存到堆栈中,然后再执行主体程序。在返回上层函数之前,也需要将这些寄存器恢复后再返回。因此调用者,可以不必保存这些寄存器。

  • 特殊用途寄存器(X8, X16~X18, X29, X30):

    • X8:间接结果寄存器。用来保存间接结果的地址,例如返回值是一个结构体时,保存结构体的地址。
    • 子程序内部调用寄存器(Intra-Procedure-Call Temporary Registers),IP0与IP1,这俩寄存器可被Veneers(实现Arm/Thumb状态切换,由链接器插入的一小段代码)或类似的代码所用。或是用于在子程序调用前,作为存储中间值的临时寄存器。使用时不需要保存,但尽量不要用。
    • x18:平台寄存器(Platform Register),用于保存当前所用的平台的ABI,尽量不要使用。
    • x29:帧指针寄存器(FP),用于连接栈帧,使用时必须保存。
    • x30:链接寄存器(LR),用于保存子程序的返回地址。

特殊寄存器

除了31个核心寄存器外,还有一些特殊的寄存器。如下图所示:

这里注意一点,官方白皮书特意说明了,没有X31/W31寄存器,很多指令集用X31/W31寄存器来表示零寄存器(ZR)或者栈帧寄存器(SP),这个说法是错误的,但因为传播广泛,从而造成了误导,包括兰新宇这篇中也是。

  • 零寄存器(ZR):

    在Linux的根文件系统中,dev目录下有个特殊的设备文件”zero”。作为source,读取”/dev/zero”可以产生众多的null字符(0x00);作为sink,任何写入”/dev/zero”的字符都将被吞没。

    事实上,这两种应用的需求都是广泛存在的,所以ARMv8也在硬件层面引入了一个新的Zero Register(XZR/WZR),效果和软件层面的”/dev/zero”类似,作为源寄存器产生0,作为目标寄存器丢弃传入的数据。

    比如我们要将一些变量赋值为0,因为ARM不允许直接操作内存单元上的数据,所以需要先将一个寄存器置为0,然后再讲这个寄存器的内容store到变量所在的内存单元上,像这样:

    Code
    1
    2
    mov  r0, #0
    str r0, [...]

    有了zero register,一条指令就可以解决问题:

    Code
    1
    str  wzr, [...]
  • 栈指针寄存器(SP):

    SP(Stack Pointer)寄存器,这个Arm-v7也有,很好理解。不同的是,Arm-v8架构下,随着异常级别的引入。每个异常级别都有自己的栈指针寄存器,SP_EL0、SP_EL1、SP_EL2、SP_EL3。对于不同的异常级别,可访问的SP寄存器情况如下:

    表中EL1t/EL2t/EL3t指的就是SP_EL0,简单来说就是处于EL0只能访问SP_EL0,处于ELn(n>0)可以访问SP_ELn和SP_EL0。

  • 程序计数器(PC):

    PC(Program Counter)寄存器的作用还是和Arm-v7一样,这里不重复,只是不能被直接访问和修改了,这部分在开头新增特性那也提到过。

  • 异常链接寄存器(ELR):

    ELR(Exception Link Register)寄存器也很熟悉,在介绍Arm-v7时就提到过,ELR_hyp与LR_hyp在32位运行状态下共用一个。这里,ELR的作用是保存异常返回地址

  • 进程状态保存寄存器(SPSR):

    SPSR(Saved Program Status Register)寄存器,Arm-v8里的SPSR与Arm-v7里的类似,区别在于Arm-v7的SPSR用来保存CPSR(Current Program Status Register)的值,而Arm-v8里的SPSR用来保存PSTATE(Processor STATE)的值。下图为SPSR的基本

    AArch64状态下,SPSR各个位所代表的含义如下:

    • N:负数标志位(Negative result)
    • Z:零标志位(Zero result)
    • C:进位标志位(Carry out)
    • V:溢出标志位(oVerflow)
    • SS:软件单步(Software Step),表示异常发生时是否开启软件单步,我的理解是类似EFlags的TF(Trap Flag)位。
    • IL:非法(代码)执行状态位(IlLegal Execution State bit),异常一发生,PSTATE的IL位就会被设置。
    • D:进程状态调试位(Process state Debug mask)。表示是否从检测点(断点处)开始调试异常,软件单步调试事件会根据异常发生时,所在异常级别的SPSR_ELn的D位,决定是否进行调试。
    • A:系统错误标志位(System Error)
    • I:IRQ标志位。
    • F:FIQ标志位。
    • M[4]:表示异常发生时的执行状态,值为0时表示执行状态为AArch64。
    • M[3:0]:异常发生时,所在模式或所处的异常级别。

    在Arm-v8架构下,被写入值的SPSR依赖当前所处的异常级别。例如异常发生在EL1,使用的就是SPSR_EL1,以此类推。

  • 处理器状态(PSTATE):

    AArch64没有CPSR,取而代之的是PSTATE(Processor STATE),注意一点,CPSR与PSTATE并不相等,PSTATE是一组特殊寄存器的组合。下图分别为PSTATE各字段单独的含义,以及各组寄存器单独的含义(包含仅AArch32才有的寄存器),这里呢不单独列出来了,直接看英文或者参考上面的SPSR。

系统寄存器

在AArch64状态下,系统配置是通过系统寄存器来控制的,主要通过MSRMRS指令进行操作。在以前的Arm-v7架构上,是通过协处理器(CP15)来操作系统配置的,但是Arm-v8架构的AArch64运行状态不支持协处理器。完整的寄存器名会声明最低可访问的异常级别,例如:

  • TTBR0_EL1,可被EL1,EL2,EL3的程序访问
  • TTBR0_EL2,可被EL2,EL3的程序访问

访问系统寄存器代码形式如下:

Code
1
2
MRS x0, TTBR0_EL1		// Move TTBR0_EL1 into x0
MSR TTBR0_EL1, x0 // Move x0 into TTBR0_EL1

以下为几个比较重要的系统寄存器,这里仅对功能作简要介绍,不展开。更详细的内容,可以参考白皮书,或者后期对Arm架构进一步深入时会介绍到。

寄存器 说明 作用 n的可选值
CTR_ELn 缓存类型寄存器(Cache Type Register) 有关集成缓存体系结构的信息 0
ELR_ELn 异常链接寄存器(Exception Link Register) 存储导致异常发生的指令的地址 1, 2, 3
ESR_ELn 异常综合表征寄存器(Exception Syndrome Register) 包含导致异常发生的原因等信息 1, 2, 3
HCR_ELn Hypervisor配置寄存器(Hypervisor Configuration Register) 控制虚拟化功能的配置以及EL2的异常捕获。捕获异常时,会将异常(IRQ/FIQ/SError)路由到hypervisor中处理。 2
SCR_ELn 安全配置寄存器(Secure Configuration Register) 控制安全状态以及EL3的异常捕获 3
SCTLR_ELn 系统控制寄存器(System Control Register) 控制体系结构的特征,例如MMU,缓存以及对齐检测 0, 1, 2, 3
SPSR_ELn 进程状态保存寄存器(Saved Program Status Register) 当异常发生,切换运行模式或者异常等级时,保存处理器状态 abt, fiq, irq, und, 1, 2, 3
TPIDR_ELn 用户读写线程ID寄存器(User Read/Write Thread ID Register) 为执行在某个异常级别的软件,提供一个存储其线程身份信息的地方,以方便操作系统管理 0, 1, 2, 3
TTBR0_ELn Hypervisor转换表基址寄存器0(Translation Table Base Register 0) 保存着转换表0的基址,以及被占用的内存信息。在AArch64中,TTBR0有3个,TTBR1只有1个。在双系统切换时(Linux/Tee),需要在ATF中保存/恢复这些寄存器。 1, 2, 3
TTBR1_ELn Hypervisor转换表基址寄存器1(Translation Table Base Register 1) 保存着转换表1的基址,以及被占用的内存信息 1
VBAR_ELn 向量基址寄存器(Vector Based Address Register) 保存任意异常进入ELn时的跳转向量基地址 1, 2, 3

系统控制寄存器

系统控制寄存器(System ConTroL Register ),用来控制标准内存,系统设施,并为核心功能函数的实现提供状态信息。系统控制寄存器属于系统寄存器,同样用MSRMRS指令进行操作。这里不展开介绍了,就先贴个图吧,等以后遇到了再展开讲讲。

NEON与浮点寄存器

Arm-v8有32个128-bit的浮点寄存器V0~V31,这32个寄存器用来处理标量浮点运算和NEON指令。需要注意一点,v8-v15是Callee需要保存的,v16-v31是Caller需要保存的,这与通用寄存器是反着的

对Arm-v7架构的兼容

寄存器组织的映射

先上图,看的明白:

左图很熟悉了,就是Arm-v7架构下的寄存器组织。再看右图,右图则是Arm-v8架构下,将AArch32状态对应的寄存器组织(也就是Arm-v7架构的寄存器组织)映射到Arm-v8架构下的31个通用寄存器上的情况。所以现在很多说法是,Arm处理器拥有31个通用寄存器和6个状态寄存器,这种说法其实是有误区的。在Arm-v7架构下,仅有16个通用寄存器;而在Arm-v8架构下,有31个通用寄存器,并且在AArch32执行状态下,原先Arm-v7架构所对应的各个寄存器的位置(除状态寄存器外)全部映射到了这31个寄存器上。因此才会被说成,有31个通用寄存器和6个状态寄存器,这也是至今仍有37个寄存器说法的原因之一。

AArch32运行状态下的SPSR与ELR_hyp寄存器作为额外的可以访问并使用系统指令的寄存器,它们并不会映射到AArch64状态下的通用寄存器上。这里面有着一些其它的映射关系,具体如下:

  • SPSR_svc映射到SPSR_EL1
  • SPSR_hyp映射到SPSR_EL2
  • ELR_hyp映射到ELR_EL2

还有一些寄存器,它们只在AArch32状态下才使用,但是会保留寄存器的状态,即便在AArch64执行状态下无法访问它们:

  • SPSR_abt
  • SPSR_und
  • SPSR_irq
  • SPSR_fiq

这里白皮书提到一点,如果一个异常导致从AArch32状态的异常级别切换到了AArch64状态的异常级别,那么AArch64状态下,ELR_ELn的高32位会被置零。

AArch32状态下的PSTATE

前面说到,在AArch64下,是没有CPSR寄存器的,取而代之的是名为PSTATE的一组寄存器。但是在AArch32状态下,PSTATE字段值的含义将有所改变,并利用空余的字段,构造成可以兼容Arm-v7的PSTATE。当然,这种拥有额外字段PSTATE只能在AArch32状态下可被访问,具体如下:

为了方便比较,下图为AArch64状态下的PSTATE与AArch32状态下PSTATE各字段的对比:

异常级别的切换

在Arm-v7架构上,处理器模式可以通过特权级别的软件控制,或者在发生异常时自动切换。当异常发生时,内核会保存当前的执行状态与返回地址,进入到指定的运行模式,并大概率屏蔽掉硬件中断。

下图是对Arm-v7架构上,运行模式与特权级别的总结。应用程序运行在最低特权等级的PL0上,也就是非特权模式。操作系统运行在PL1上,虚拟机监视器以及虚拟化扩展位于PL2。作为连接Secure World与 Normal World的安全监视器(Secure Monitor)同样运行在PL1。

在AArch64执行状态下,处理器模式被映射到了异常级别上(如下图所示)。当AArch32运行状态发生异常时,处理器会切换到支持当前异常处理的异常级别(运行模式)上。

异常级别的切换必须遵循如下规则:

  • 当切换到更高等级的异常级别时,例如从EL0到EL1,表明提升了软件执行时的特权。
  • 不能产生一个低于当前异常级别的异常,例如EL1不能产生一个EL0的异常。
  • 异常处理不能在EL0进行,处理异常必须在更高的异常级别上。
  • 异常会导致程序流程的改变,后面省略,参考白皮书P34。
  • 异常处理结束,并返回先前异常级别时,是通过ERET指令实现的。
  • 从一个异常返回时,可以仍然处于发生异常的异常级别,或者返回到一个更低的异常级别中,但是不能进入到更高的异常级别中。例如从EL1返回,可能仍然位于EL1,或者返回到EL0,但是不能进入到EL2。
  • 安全状态通常不会随着异常级别的切换而发生改变,除非是EL3直接返回到一个Non-secure状态。(这部分白皮书好像也错了,少了一个not)

执行状态的切换

你的操作系统会经常发生执行状态的切换。它可能是这样一种情况,例如,你所运行的是64位操作系统,但你想运行一个32位的应用程序在EL0,这时,你不得不将系统切换成AArch32状态。当程序执行完,再将操作系统切回AArch64执行状态。

一般来说,只能通过切换异常级别来实现执行状态的切换。当发生一个异常时,或许会导致从AArch32切换到AArch64,当从这个异常返回时,又会从AArch64切换回AArch32。这样就实现了执行状态的切换。更多注意要点,这里不再翻译了,参考白皮书P35。

参考资料

  1. 《Arm Cortex-A Series Programmer’s Guide for ARMv8-A》2015
  2. Armv8架构寄存器组织
  3. AArch64异常(一):AArch64异常简介
  4. Arm体系结构知识:汇编、架构、异常级别和安全状态
  5. Winddong-Armv8-AArch64 寄存器和指令集
  6. 思否-Armv8架构虚拟化介绍
  7. 知乎-Armv8带来的变化
  8. CSDN-Armv8-a架构简介
  9. CSDN-Armv8体系架构简介: AArch64系统级体系结构之编程模型
  10. CSDN-Armv8异常处理简介
  11. CSDN-Armv8架构与指令集.学习笔记
  12. CSDN-Arm通用寄存器传递参数介绍
  13. CSDN-Armv8对CPU虚拟化的支持及L4_fiasco中实现
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2021/06/03/Armv8%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶