概述
本指南介绍了 Armv8‑A 和 Armv9‑A 中的内存属性/特性。首先解释内存的属性从何而来,以及如何将其分配给内存区域。然后介绍不同的属性,以及内存访问顺序的基础知识。
这些信息对于底层(例如启动代码或驱动程序)开发人员都非常有用。这对于通过软件,配置或管理内存管理单元 (MMU) 的人来说更为重要。
在本指南的最后,您可以检查您的知识。您将了解不同的内存类型及主要区别,并且还能列出用于指定地址的内存属性。
什么是内存属性/特性及为什么需要它们
内存属性/特性是定义内存行为的方式。其提供了一个结构和一组规则,当配置如何访问和使用内存地址时,你需要遵守这些规则。
内存属性/特性用于一个地址,并定义了与内存访问相关的规则。
考虑一个简单的系统,地址空间如下。
地址空间中内存区域的排列称为地址映射。上面例子中,地址映射包括:
- 内存和外设
- 内存,代码和数据
- 操作系统资源和用户应用程序资源
处理器与外设交互的方式和与内存不同。例如,你通常想缓存内存,但是却不想缓存外设。缓存是将内存中的信息拷贝存储到一个硬件区域(称为缓存)。缓存距离内核更近,内核访问速度更快。同样,你通常希望处理器阻止用户访问内核资源,比如外设。
下图展示一些不同内存属性的地址映射,你可以运用到相应的内存区域上。
你需要描述这些不同的属性,这样处理器就能恰当地访问每个位置。
在AArch64中描述内存
虚拟地址空间和物理地址空间之间的映射是在一组转换表中定义的,有时也称为页表。对于虚拟地址的每个块或页,转换表提供相应的物理地址和用于访问该页的属性。
转换表的每一项称为块描述符或者页描述符,在大多数情况下,属性都来自于该描述符。
下图展示了块描述符及属性字段的示例。
本指南中讨论以下一些重要的属性。
- UXN和PXN:执行权限
- DBM:脏位,表示页面是否被修改过
- AF:访问标志位
- AP:访问权限
- AttIndx:用来索引不同的内存类型和属性
后面我们将详细地解释这些属性。
层次属性
一些内存属性可以在高级页表的描述符中指定。这些是层次属性,这适用于访问权限、执行权限和物理地址空间。
如果设置了高级页表中的这些位,它们将会覆盖低级页表中的配置。如果这些位被清除,则可以使用低级页表中的配置。下图示例展示了PXNTable(执行权限)的使用。
从 Armv8.1‑A 开始,您可以禁用不支持访问和执行权限的配置。这是由 TCR_ELx 寄存器中的分层权限禁用 (HPD) 位控制的。禁用后,用于分层控制的位可供软件用于其他目的。
MMU禁用
总而言之,地址的属性是来自转换表。转换表位于内存中,用于存储虚拟地址和物理地址之间的映射。这些表还包含物理内存位置的属性。
这些转换表是由内存管理单元MMU访问。
如果MMU禁用后会发生什么呢?在复位后,编写立即执行的代码时,这是一个重要的问题。
当第1阶段的MMU禁用时:
- 所有的数据访问都是Device-nGnRnE。这将在后面的设备内存部分进行解释。
- 所有的指令视为不可缓存或者可缓存的,这取决于SCTLR_ELx.I(指令缓存控制位)的值。
- 所有的地址都具有读、写和执行权限。
对于虚拟化涵盖的异常等级,当禁用第2阶段时,将沿用第1阶段的属性。
内存类型
系统中所有未标记为故障的地址均被分配了一种内存类型,内存类型是描述处理器应当如何与地址区域交互。Armv8-A和Armv9-A中有两种内存类型:普通内存(Normal Memory)和设备内存(Device Memory)。
普通内存
普通内存类似于RAM,Flash或者ROM这类的内存,代码只能放在普通内存中。
普通内存是系统中最常见的内存类型,如下图所示。
内存访问顺序
传统计算机处理器按照程序中定义的顺序执行指令,事情按照程序中指定的次数发生,并且只发生一次,这称为简单顺序执行(SSE)模型。大多数现代处理器看起来遵循此模型,但实际上却运用了许多优化,以提高性能。
假设标记为“Normal”的内存在访问时不会引起其他变化,这意味着读取这些内存地址只是返回数据,不会导致数据更改,或者触发额外的处理流程。因此,对于标记为“Normal”的内存,处理器可以合并访问、预测访问或者重新排序访问。
有关内存访问重新排序的更多信息,请参考Memory system,ordering,and barriers guide。
设备内存
设备内存类型用于描述外设。外设寄存器通常称为内存映射 I/O (MMIO)。下面的例子展示了标记为“Device”类型的内存:
回顾一下,普通内存没有任何副作用,但设备内存则相反,可能产生副作用。
例如,读取FIFO通常会导致下一个数据前进,这对于多次访问FIFO十分重要,因此处理器必须遵守程序指定的规则。
设备区域永远不可缓存,因为你不太可能想要缓存对外设的访问。
不允许对设备内存进行预测访问,处理器只能访问架构规定的位置,这意味着已经执行的指令实际上已访问该位置。
指令不允许放在设备内存中。建议将设备区域始终标记为不可执行,否则处理器可能推测性地从中获取指令,这会导致类似FIFO的敏感设备出现问题。
设备子类型
设备有四种子类型,具有不同的限制等级。下面的子类型限制比较宽松:
- Device-GRE
- Device-nGRE
- Device-nGnRE
这个子类型限制最为严格:
- Device-nGnRnE
Device后面的字母具有不同的含义:
- 聚合(G,nG):访问可以聚合(G)或者不聚合(nG),聚合意味着可以对同一位置的多次访问合并为一次访问,或者将多次较小的访问合并为一次较大的访问。
- 重排(R,nR):这指定对于同一外设可进行重排(R)或者不重排(nR)访问。当允许重排访问时,限制同普通内存一样。你可以在Memory system,ordering,and barriers guide中找到更多关于普通内存访问重排的信息。
- 提前写应答(E,nE):这决定了何时被认为写完成。如果允许提前写应答,一旦其他观察者可见,在到达实际目的地之前,访问可以视为写完成。例如,一旦数据写到写缓冲区,其他PE就可以看到这些数据。当不允许提前写应答时,数据必须到达最终目的地。
有两个示例:
- Device-GRE:允许聚合、重排和提前写应答访问
- Device-nGnRnE:不允许聚合、重排和提前写应答访问
我们已经了解重排的工作原理,但还没有介绍聚合和提前写应答。聚合允许对相似位置的内存访问合并到单个总线事务中,从而优化访问。提前写应答意味着当数据发送到缓冲区后,总线是否可以发送写完成确认标志。这样即使外设还未收到写动作,所有PE均已能观察到此次写入。
处理器对待每种类型真的不同吗
内存类型描述了某个位置允许的行为组合。只关注设备类型,下图代表允许的行为:
可以看出,Device-nGnRnE类型限制最为严格,允许的行为最少。Device-GRE类型限制最少,允许的行为也最多。
比较重要的是,Device-nGnRnE允许的行为同样适用于Device-GRE。例如,Device-GRE并不要求一定要使用聚合访问,其只表示允许聚合访问。因此,处理器可以将Device-GRE视为Device-nGnRnE。这是比较极端的,在Arm Cortex-A处理器上不太可能发生。然而,处理器通常不区分类型和子类型,例如将Device-GRE和Device-nGRE以相同的方式处理,仅当类型或者子类型始终受到比较多的限制才允许这样做。
某些情况下无法完全支持Device-nGnRnE的要求。例如,对PCIe基地址寄存器(BAR)空间的Device-nGnRnE写入一旦到达PCiE拓扑后,就会变成后写入(不期望写入完成响应的写入)。在这种情况下,写访问将仅具有Device-nGnRE的属性,这是因为写响应无法由目标点提供,而是由于某些中间组件(如PCIe Root Port)提供。然而Device-nGnRnE对PCIe配置空间的写入不是后写入的(期望写完成响应的写入),因此对于这些类型的访问维持Device-nGnRnE的要求。
描述内存类型
内存类型并没有存放到页表项中。相反,转换表项中的AttrIndx字段(3位[2:0])用于索引选择MAIR_ELx(内存属性间接寄存器)。
所选字段确定内存类型和可缓存性信息。
为什么使用寄存器的索引,而不是直接将内存类型编码到转换表中呢?因为转换表中每一项的位数是有限的。它需要8位来编码内存类型,但是只需要3位将索引编码到MAIR_ELx中。这减少了转换表中位的使用。
缓存性与共享性
标记为“Normal”位置的内存类型还具有缓存性和共享性。缓存性控制是否某个内存位置可以被缓存,如果可以被缓存,共享性控制哪些其他观察者需要查看内存的一致性拷贝。这允许进行一些复杂的配置,这超出了本指南的范围。然而,Arm期望操作系统将大部分DRAM内存标记为“正常回写可缓存、内部可共享”。
权限
访问权限可以分为直接权限或者间接权限。本章讨论直接权限方案,下一章讨论间接权限方案。
对于直接权限方案,访问权限(AP)属性控制某个位置是否可读、可写以及需要什么权限。下表是这些AP位的设置:
AP | 非特权(EL0) | 特权(EL1/2/3) |
---|---|---|
00 | 禁止访问 | 读/写 |
01 | 读/写 | 读/写 |
10 | 禁止访问 | 只读 |
11 | 只读 | 只读 |
如果访问不满足指定的权限,例如写入只读区域,会生成异常(标记为权限错误)。
对非特权数据的特权访问
标准权限模型是较高特权实体可以访问属于较低特权实体的任何内容。例如,操作系统(OS)可以查看分配给应用程序的所有资源;虚拟机监视器可以访问分配给虚拟机(VM)的所有资源。这是因为更高的异常等级意味着特权也较高。
然而,这并不总是按预期那样。恶意应用程序可能欺骗操作系统,访问正常应用程序数据。这需要操作系统OS检查系统调用中的指针。
Arm架构提供了多种方法简化上面的操作。首先是PSTATE.PAN(特权模式禁止访问)位,当该位置位时,从EL1(或者EL2,E2H==1)加载和存储到非特权区域会导致异常(权限错误),如下图:
PAN允许捕获对非特权数据的意外访问。例如,操作系统可能试图去访问某个地方,以为那里是特权数据,但实际上那里是非特权数据。这意味着操作系统的期望(目标是特权的数据)与现实(目标是非特权的数据)之间存在不匹配。这可能是由于编程错误引起的,也可能是系统遭受攻击的结果。PAN允许我们在访问发生之前就捕捉到它,从而捕获错误或防止攻击的发生。
(注:操作系统不是对非特权的数据都有访问权限吗?为什么还要PAN这个功能?假设操作系统要访问一个数据存储区,但实际上这个区域应该是非特权的,只有特定权限的程序才能访问。如果没有PAN功能,操作系统可能会错误地访问这个数据存储区,造成系统不稳定或者数据泄露等问题。有了PAN功能,操作系统在访问之前就会被检测到,从而可以防止这种错误的访问)
有时操作系统OS的确需要访问非特权区域,例如写入到应用程序的缓冲区。为了支持这一点,指令集提供了LDTR和STTR指令。
LDTR和STTR是非特权加载和存储指令。即使它们是由EL1或EL2上的操作系统执行,EL0也会根据权限进行检查。由于这些明确是非特权访问,因此这些指令不会被PAN阻止,如下图所示:
这允许操作系统区分访问特权数据和访问非特权数据两种情况(如PAN =1,LDR用于特权访问,LDTR用于非特权访问)
执行权限
除了数据访问权限之外,还有执行权限。这些属性允许您指定哪些地址不能执行指令:
- UXN:用户模式下(EL0)不能执行(不适用于EL3,或者当HCR_EL2.E2H==0时的EL2)
- PXN:特权模式下不能执行(在EL3或者当HCR_EL2.E2H==0时EL2下,称为XN)
这些是不允许执行位,意味着如果置位,则相应的位置不可执行。
有单独的特权位和非特权位,因为应用程序代码需要在用户空间 (EL0) 中执行,但不应该在内核权限 (EL1/EL2) 下执行,如下图所示:
Arm架构在系统控制寄存器 (SCTLR_ELx) 中提供控制位,将所有写权限地址设置为不可执行。
具有 EL0 写入权限的位置永远无法在 EL1 上执行。
间接权限和权限覆盖扩展
访问权限可以使用直接权限方案或间接权限方案,本章讨论间接权限方案,直接权限方案参见上一章。
间接权限引入了一种设置权限的方法,其会有效使用转换表描述符(TTDs)中的权限位,同时引进了新的权限类型。
权限覆盖允许在EL0上运行的进程逐级限制权限,同时减少对对更高特权等级上运行进程的调用次数(如操作系统OS),并且不需要花费代价去维护转换后备缓冲区(TLB)。
间接权限和权限覆盖可以更灵活地设置内存页面上的权限,并且可以减少权限修改对性能的影响。
间接权限
当启用间接权限时,TTD中的权限间接索引(PIIndex)字段变成可用状态。PIIndex 字段索引到相应异常等级的权限间接寄存器 (PIR),以便操作系统 (EL1)、管理程序 (EL2) 或固件 (EL3) 可以设置第 1 阶段转换的基本权限:
寄存器 | 描述 |
---|---|
PIRE0_EL1 | PIR设置非特权EL1&0基本权限 |
PIRE0_EL2 | PIR设置非特权EL2&0基本权限 |
PIREL1 | PIR设置特权EL1基本权限 |
PIREL2 | PIR设置特权EL2基本权限 |
PIREL3 | PIR设置特权EL3基本权限 |
PIRE0_ELx 或 PIR_ELx 寄存器字段指示要应用于该内存页的基本权限。详细信息请参阅description of the Permission Indirection Registers。
权限覆盖
可以通过使用转换表条目中的权限覆盖索引(POIndex)字段进一步限制内存页面的基本权限。 POIndex 字段索引到执行等级的权限覆盖寄存器 (POR):
寄存器 | 描述 |
---|---|
POR_EL0 | 非特权EL0第1阶段的权限覆盖 |
POR_EL1 | 特权EL1第1阶段的权限覆盖 |
POR_EL2 | 特权EL2第1阶段的权限覆盖 |
POR_EL3 | 特权EL3第1阶段的权限覆盖 |
POR_ELx 寄存器字段除指定基本权限之外,还应用于内存页的权限覆盖。关于其他权限限制的详细信息请参阅description of the Permission Indirection Registers。
使用间接权限和权限覆盖功能的示例
在此示例中,运行在 EL1 上的操作系统想要设置一系列内存位置的基本和覆盖权限,这些位置将由运行在 EL0 上的应用程序使用。
要设置基本权限,操作系统需要执行以下操作 :
- 在权限间接寄存器 (PIR_ELx) 中分配权限索引,在这个例子中操作系统选择索引 3。
- 设置PIR_ELx 中索引 3 代表的权限为“读‘、写、覆盖”,即配置为“0b0101”。
- 将该范围内存位置的 TTD 中的 PIIndex 字段设置为 0b0011 (3) 。
这将该内存区域的基本权限设置为“读和写,覆盖”。 详细可用权限信息请参阅description of the Permission Indirection Registers。
在 EL0 上运行的应用程序可以请求操作系统指定权限覆盖,应用程序稍后可以请求将其应用于内存页面的子集。为了指定覆盖,操作系统执行以下操作:
- 在权限覆盖寄存器 (POR_ELx) 中分配权限覆盖索引。本例中操作系统选择索引6。
- 设置 POR_ELx 中索引 6的权限,配置为“只读”,即0b0001。
这将该内存位置的可用覆盖权限设置为“只读”。然后,在 EL0 上运行的应用程序可以请求(使用系统调用)操作系统将权限覆盖应用到内存范围的子集。操作系统通过将这些内存页的 TTD 中的 POIndex 字段设置为 0b110(6) 来实现此目的,以便将进一步的“只读”权限限制应用于基本权限。这允许在 EL0 上运行的应用程序去掉写入权限。关于其他权限限制的详细信息请参阅description of the Permission Indirection Registers。
此示例展示了如何使用间接权限,将更大范围的基本权限应用于内存页。它还展示了在 EL0 上运行的进程如何限制权限,而无需 TLBI 的维护开销,同时最大限度地减少了对EL1 上运行的操作系统的调用。间接权限和权限覆盖扩展可以更加灵活地应用权限。
下图展示了如何结合间接权限和权限覆盖,来设置内存页的权限。
访问标志
您可以使用访问标志 (AF) 位来跟踪一个区域是否被访问过。您可以将 AF 位设置为:
- AF=0:区域未被访问过
- AF=1:区域已被访问过
AF对于操作系统十分有用,因为你可以使用这个位来标记页面是否被使用过,并且用于页面回收(从RAM中移除)。
更新AF位
当使用AF位时,最初创建转换表时AF位会被清零。当访问页面时,AF置位。软件可以解析这些表来检查 AF 位是否已设置或清除。AF==0的页面表示还没有被访问,并且可能适合被回收。
有两种方法可以在访问时设置AF位:
- 软件更新:当第一次访问页面时会触发同步异常(Access Flag Fault)。在异常处理程序中,软件可以将相关转换表中的AF置位,然后返回。
- 硬件更新:访问页面会导致硬件自动设置AF 位,而无需生成异常。这需要启用 Armv8.1‑A 中的转换控制寄存器 (TCR_ELx.HA) 的硬件访问更新位。
脏位
Armv8.1‑A 新增了处理器管理块或者页脏状态的功能。脏状态记录块或页是否已被写入,这很有用,因为如果块或页被调出,脏状态会告诉管理软件是否需要将 RAM 的内容写出到存储中。
例如,让我们考虑一个文本文件。文件最初从磁盘(闪存或硬盘驱动器)加载到 RAM 中。当稍后从内存中删除它时,操作系统需要知道 RAM 中的内容是否比磁盘上的内容新。如果 RAM 中的内容较新,则需要更新磁盘上的副本。如果不是,则可以删除 RAM 中的副本。
当启用脏状态管理时,软件最初会创建转换表条目,并将访问权限设置为只读,并设置 DBM(Dirty Bit Modifier)位。如果写入该页,硬件会自动将访问权限更新为读写。
将 DBM 位设置为 1 会更改访问权限位(AP[2] 和 S2AP[1])的功能,以便它们记录脏状态,而不是记录写权限。这意味着当 DBM 位设置为 1 时,访问权限位不会导致权限错误。
对齐和大小端
对齐
如果访问地址是元素大小的倍数,则是访问是对齐的。
对于LDR和STR指令,元素大小是访问大小。例如,LDRH指令用于加载16位的值,如果要对齐访问,地址必须为2的倍数。
LDP和STP指令分别加载和存储一对元素。对齐方法只需要是一个元素大小的倍数,而不是两个元素加起来的大小。例如:
LDP X0, X1, [X2]
上面加载两个64位值,总共128位,X2中的地址需要是64位的倍数,才能认为是对齐操作。
规则同样适用于向量加载和存储。
当地址不是元素大小的倍数时,访问不是对⻬的。允许对标记为普通内存的地址进行未对⻬访问,但不允许对设备内存进行未对⻬访问。对设备内存的未对⻬访问将触发异常(对⻬错误)。
通过设置 SCTLR_ELx.A 可以捕获对标记为 Normal 的区域的未对⻬访问。如果置位,则对普通区域的未对⻬访问也会生成对⻬错误。
大小端
在 Armv8‑A 中,指令读取始终是小端。
对于数据访问,是否同时支持小端和大端是IMPLEMENTATION DEFINED。如果仅支持一种,具体支持哪一种也是IMPLEMENTATION DEFINED。
对于同时支持大端和小端的处理器,每个异常等级均可配置大小端。
Arm Cortex‑A 处理器同时支持大端和小端。
内存别名和不匹配的内存类型
如果给定物理地址空间具有多个虚拟地址,这就称为别名。
内存属性基于虚拟地址,这是因为属性来自转换表。当物理空间有多个别名时,所有的虚拟地址应当具备兼容的属性。我们将兼容性描述为:
- 相同的内存类型,对于设备是相同的子类型
- 对于普通内存位置,相同的缓存性和共享性
如果属性相互不兼容,内存访问可能不会按预期运行。
下图展示了两个别名示例。位置A的两个别名具有兼容的属性,这是推荐的方法。位置B的两个别名具有不兼容的属性(Normal和Device),这可能对一致性和性能产生影响。
Arm 强烈建议软件不要将不兼容的属性分配给同一位置的不同别名。
检查你的知识
以下问题可以检测你学到的知识。
地址位置的属性从何而来?
转换表,通常是块/页描述符,尽管分层属性可以覆盖它。
Armv8-A有哪两种内存类型?
普通内存和设备内存。
Device‑nGnRE 中的 ‑nGnRE 是什么意思?
不允许聚合、重排访问,但允许提前写应答访问
为什么页面可能被标记为 PXN=1、UXN=0?
应用程序代码需要在用户空间中可执行,而不是在内核空间中执行。
AF 位通常用于什么?
跟踪哪些页面已被访问过。
指令提取是大端还是小端?
小端。
在启用内存管理单元MMU之前,一些启动代码可能会非对齐访问,从而导致对齐错误。为什么?
当 MMU 被禁用时,所有访问都被视为对设备的访问。对标记为“设备”的区域的未对⻬访问始终会发生异常。
相关信息
以下是与本文相关的一些资源。
-
Arm Community
提出开发问题,并查找 Arm 专家提供的有关特定主题的文章和博客。
-
Memory ordering and barriers guide
提供有关内存排序和屏蔽使用的相关信息。
-
Security - pointer signing and landing pads guide(coming soon)
Armv8.5‑A 引入了对Branch Target Instructions (BTI) 的支持。 BTI 支持由第一阶段转换表中的 GP 位控制。
-
Armv8-A Instruction Set Architecture
包括有关简单顺序执行(SSE)的信息。
-
转换过程:如果您对转换过程的完整细节感兴趣,请访问XML for instruction中提供的伪代码。可以从函数
AArch64.FullTranslate()
开始学习。
以下是与本文主题相关的一些资源。
描述Armv8-A中的内存
指令获取的可缓存性比您想象的要复杂一些。缓存和一致性指南(即将推出)中将介绍这个主题。
缓存性和共享性
缓存和缓存一致性指南(即将推出)。
结合阶段1和阶段2的属性
Memory Management中更详细地讨论了阶段1和阶段2的转换。有关背景信息,请参阅虚拟化指南。
培训模块
以下是一些相关培训的链接:
- Introduction to Armv8-A
- Memory model overview
- What does architecture consist of?
下一步
Armv8‑A 内存属性为处理器内核如何与系统中的内存交互提供了基础。当您开始开发底层代码(例如启动代码或驱动程序)时,您可以应用在本指南中学到的规则。当您编写代码来配置内存管理单元 (MMU) 时,您还可以将所学知识付诸实践。
本系列下一步是在Memory management中讨论地址转换。
要继续了解 Armv8‑A 架构,请参阅series of guides。