快速连接
👉👉👉【精选】ARMv8/ARMv9架构入门到精通-目录 👈👈👈
1 简介
本文介绍了 Armv8-A 中的内存模型。 它首先解释描述内存的属性来自哪里以及它们如何分配给内存区域。 然后介绍可用的不同属性并解释内存排序的基础知识。
此信息对于开发低级代码(如引导代码或驱动程序)的任何人都很有用。 它与编写代码来设置或管理内存管理单元 (MMU) 的任何人都特别相关。
2 内存模型是什么,为什么需要它?
内存模型是一种组织和定义记忆行为的方式。 它提供了一种结构和一组规则,供您在配置如何在系统中访问和使用地址或地址区域时遵循。
内存模型提供了可以应用于地址的属性,它定义了与内存访问顺序相关的规则。 考虑一个带有地址空间的简单系统,如下图所示:
地址空间中内存区域的排列称为地址映射。 在这里,map包含:
- 内存和外围设备
- 内存中的代码和数据中
- 属于操作系统的资源和属于用户应用程序的资源
您希望处理器与外设交互的方式与它应该与内存交互的方式不同。 您通常希望缓存内存,但不想缓存外围设备。 缓存是将信息的副本从内存存储到某个位置(称为缓存)的行为。 缓存更靠近内核,因此内核访问速度更快。 同样,您通常希望处理器阻止用户访问内核资源。 此图显示了具有一些您可能希望应用于内存区域的不同内存属性的地址映射:
您需要能够向处理器描述这些不同的属性,以便处理器适当地访问每个位置。
3 页表中的Describing
虚拟地址空间和物理地址空间之间的映射在一组转换表中定义,有时也称为页表。 对于虚拟地址的每个块或页面,转换表提供相应的物理地址和访问该页面的属性。 每个转换表条目称为块或页描述符。 在大多数情况下,属性来自这个描述符。 此图显示了一个示例块描述符,以及其中的属性字段:
重要的比特位:
- SH - The shareable attribute
- AP - The access permission
- UXN and PXN – Execution permissions
3.1. 属性的层次
某些内存属性可以在更高级别表的表描述符中指定。 这些是分层属性。 这适用于访问权限、执行权限和物理地址空间。
如果这些位被设置,那么它们将覆盖较低级别的条目,如果这些位被清除,则较低级别的条目将不加修改地使用。 一个例子,使用PXNTable(执行权限)
从 Armv8.1-A 开始,您可以禁用对使用表描述符中的分层属性设置访问权限和执行权限的支持。 这是通过相关的 TCR_ELx 寄存器控制的。 禁用时,以前用于分层控制的位可供软件用于其他用途。
3.2. 关闭MMU
总而言之,地址的属性来自转换表。 转换表位于内存中,用于存储虚拟地址和物理地址之间的映射。 这些表还包含物理内存位置的属性。
转换表由内存管理单元 (MMU) 访问。 如果 MMU 被禁用会发生什么? 在编写将在重置后立即运行的代码时,这是一个需要解决的重要问题。
当 Stage 1 MMU 被禁用时:
- 所有数据访问都是 Device_nGnRnE。 我们将在本指南的后面解释这一点。
- 所有指令提取都被视为可缓存的。
- 所有地址都具有读/写访问权限并且是可执行的。
对于虚拟化涵盖的异常级别,当禁用第 2 阶段时,将使用未经修改的第 1 阶段属性。
4 内存访问排序
在我们的指南 Armv8-A 指令集架构中,我们介绍了简单顺序执行 (SSE)。 SSE 是指令排序的概念模型。内存访问顺序和指令顺序是两个不同但相关的概念。了解它们之间的区别很重要。
SSE 描述了处理器执行指令的顺序。总而言之,现代处理器拥有长而复杂的管道。这些流水线通常能够重新排序指令或并行执行多条指令,以帮助它们最大限度地提高性能。 SSE 意味着处理器必须像处理器一次执行一条指令一样,按照它们在程序代码中给出的顺序。这意味着硬件对指令的任何重新排序或多次发布必须对软件不可见。
内存排序是关于内存访问在内存系统中出现的顺序。由于写缓冲区和缓存等机制,即使指令按顺序执行,相关的内存访问也可能不会按顺序执行。这就是为什么即使处理器遵循 SSE 模型来获取指令,内存排序也是一个需要考虑的重要事项。
5 内存类型
系统中所有未被标记为故障的地址都被分配了一个内存类型。 内存类型是处理器应如何与地址区域交互的高级描述。 Armv8-A中有两种内存类型:Normal memory和device。
注意:Armv6 和 Armv7 包括第三种内存类型:Strongly Ordered。 在 Armv8 中,这映射到 Device_nGnRnE。
6 Normal memory
普通内存类型用于任何行为类似于内存的东西,包括 RAM、闪存或 ROM。 代码只能放置在标记为 Normal 的位置。 Normal 通常是系统中最常见的内存类型,如下图所示:
6.1. 访问排序
传统上,计算机处理器按照程序中指定的顺序执行指令。事情按照程序中指定的次数发生,并且一次只发生一次。这称为简单顺序执行 (SSE) 模型。大多数现代处理器似乎都遵循此模型,但实际上,许多优化都已应用并可供您使用,以帮助提高性能。我们将在这里介绍其中的一些优化。
标记为 Normal 的位置在访问时没有直接的副作用。这意味着读取位置只是将数据返回给我们,但不会导致数据更改或直接触发另一个进程。因此,对于标记为“正常”的位置,处理器可能会:
• 合并访问。代码可以多次访问一个位置,或访问多个连续的位置。为了效率,允许处理器检测这些访问并将这些访问合并为单个访问。例如,如果软件多次写入一个变量,处理器可能只显示最后一次写入内存系统。
• 推测性地执行访问。允许处理器读取标记为“正常”的位置,而无需软件特别请求。例如,处理器可能会根据先前访问的模式,在软件请求数据之前使用模式识别来预取数据。该技术用于通过预测行为来加快访问速度。
• 重新排序访问。在内存系统中看到的访问顺序可能与软件发出访问的顺序不匹配。例如,处理器可能会重新排序两次读取以允许它生成更有效的总线访问。对同一位置的访问不能重新排序,但可能会合并。
想想这些优化,比如允许处理器采用技术来加速性能和提高电源效率的自由。这意味着 Normal 内存类型通常会提供最佳性能。
注意:允许处理器以这些方式进行优化,但这并不意味着它总是如此。给定处理器对这些自由的利用程度取决于其微架构。从软件的角度来看,您应该假设处理器可能会执行其中的任何一项或全部。
6.2. 重新排序
概括地说,对标记为“正常”的位置的访问可以重新排序。 让我们考虑这个示例代码,其中包含三个内存访问、两个存储和一个加载的序列:
如果处理器对这些访问进行重新排序,这可能会导致内存中出现错误的值,这是不允许的。对于对相同字节的访问,必须保持顺序。处理器需要检测危险并确保为预期结果正确排序访问。
这并不意味着本示例没有优化的可能性。处理器可以将两个存储合并在一起,向内存系统呈现一个合并的存储。它还可以检测到加载操作来自存储指令写入的字节,以便它可以返回新值而无需从内存中重新读取它。
注意:示例中给出的序列是故意设计来说明这一点的。在实践中,这些类型的危害往往更加微妙。
还有其他强制排序的情况,例如地址依赖关系。地址依赖是指加载或存储使用先前加载的结果作为地址。在此代码示例中,第二条指令取决于第一条指令的结果
LDR X0,[X1]
STR X2,[X0] // The result of the previous load is the address in this store.
此示例还显示了地址依赖关系,其中第二条指令依赖于第一条指令的结果:
LDR X0,[X1]
STR X2,[X5, X0] // The result of the previous load is used to calculate the address.
在两次内存访问之间存在地址相关性的情况下,处理器必须保持顺序。 此规则不适用于控制依赖项。 控制依赖是指使用先前加载的值来做出决定。 此代码示例显示了一个负载,然后是一个依赖于负载值的比较和零分支操作:
LDRX0, [X1]
CBZ X0, <somewhere_else>
STRX2, [X5][Symbol] // There is a control dependency on X0, this does not guarantee
ordering.
在某些情况下,需要在访问 Normal 内存或访问 Normal 和 Device 内存之间强制执行排序。 这可以使用屏障指令来实现
7 Device memory
设备内存类型用于描述外设。 外设寄存器通常称为内存映射 I/O (MMIO)。 在这里,我们可以看到在我们的示例地址映射中通常标记为设备的内容:
回顾一下,Normal 内存类型意味着访问没有副作用。对于设备类型内存,情况正好相反。设备内存类型用于可能产生副作用的位置。
例如,对 FIFO 的读取通常会导致它前进到下一个数据。这意味着对 FIFO 的访问次数很重要,因此处理器必须遵守程序指定的内容。设备区域永远不可缓存。这是因为您不太可能希望缓存对外围设备的访问。
不允许对标记为设备的区域进行推测数据访问。如果在架构上访问该位置,则处理器只能访问该位置。这意味着已在架构上执行的指令已访问该位置。
说明不应放置在标记为设备的区域中。我们建议始终将设备区域标记为不可执行。否则,处理器可能会推测性地从中获取指令,这可能会导致读取敏感设备(如 FIFO)出现问题。
注意:这里有一个很容易被忽略的细微差别。将区域标记为设备仅可防止推测性数据访问。将区域标记为不可执行可防止推测性指令访问。这意味着,为了防止任何推测性访问,必须将区域标记为设备和不可执行。
以下对device memory的类型进行总结
- Device-nGnRnE : 处理器必须严格按照代码中内存访问来进行、必须严格执行program order(无需重排序)、写操作的ack必须来自最终的目的地
- Device-nGnRE : 处理器必须严格按照代码中内存访问来进行、必须严格执行program order(无需重排序)、写操作的ack可以来自中间的write buffer
- Device-nGRE : 处理器必须严格按照代码中内存访问来进行、内存访问指令可以进行重排、写操作的ack可以来自中间的write buffer
- Device-GRE : 处理器对多个memory的访问是否可以合并、内存访问指令可以进行重排、写操作的ack可以来自中间的write buffer
➨Gathering和non Gathering(G or nG):表示对多个memory的访问是否可以合并,如果是nG,表示处理器必须严格按照代码中内存访问来进行,不能把两次访问合并成一次。例如:代码中有2次对同样的一个地址的读访问,那么处理器必须严格进行两次read transaction
➨Reordering(R or nR):表示是否允许处理器对内存访问指令进行重排。nR表示必须严格执行program order
➨Early Write Acknowledgement(E or nE):PE访问memory是有问有答的(更专业的术语叫做transaction),对于write而言,PE需要write ack操作以便确定完成一个write transaction。为了加快写的速度,系统的中间环节可能会设定一些write buffer。nE表示写操作的ack必须来自最终的目的地而不是中间的write buffer
8 Describing the memory type
9 Cacheability 和 shareability属性
- 如果将block的内存属性配置成Non-cacheable,那么数据就不会被缓存到cache,那么所有observer看到的内存是一致的,也就说此时也相当于Outer Shareable。
其实官方文档,也有这一句的描述:
在B2.7.2章节 “Data accesses to memory locations are coherent for all observers in the system, and correspondingly are treated as being Outer Shareable” - 如果将block的内存属性配置成write-through cacheable 或 write-back cacheable,那么数据会被缓存cache中。write-through和write-back是缓存策略。
- 如果将block的内存属性配置成 non-shareable, 那么core0访问该内存时,数据缓存的到Core0的L1 d-cache 和 cluster0的L2 cache,不会缓存到其它cache中
- 如果将block的内存属性配置成 inner-shareable, 那么core0访问该内存时,数据只会缓存到core 0和core 1的L1 d-cache中, 也会缓存到clustor0的L2 cache,不会缓存到clustor1中的任何cache里。
- 如果将block的内存属性配置成 outer-shareable, 那么core0访问该内存时,数据会缓存到所有cache中
以下也总结了一下shareable、cacheable属性对缓存策略的影响:
Non-cacheable | write-back cacheable | write-through cacheable | |
---|---|---|---|
non-shareable | 数据不会缓存到cache (对于观察则而言,又相当于outer-shareable) | core访问该内存时,数据只缓存的到Core的 cache 中,不会缓存到其它cache中 | 同左侧 |
inner-shareable | 数据不会缓存到cache (对于观察则而言,又相当于outer-shareable) | core访问该内存时,数据只会缓存到core的cache和 cluster的 cache中,该地址的TAG也不会存到snoop filter中,即不会被其它ACE Master snoop | 同左侧 |
outer-shareable | 数据不会缓存到cache (对于观察则而言,又相当于outer-shareable) | core访问该内存时,数据只会缓存到core的cache和 cluster的 cache中,该地址的TAG会存到snoop filter中,会被其它ACE Master snoop | 同左侧 |
10 权限属性
访问权限 (AP) 属性控制是否可以读取和写入位置,以及需要什么权限。下表显示了 AP 位设置:
如果访问破坏了指定的权限,例如对只读区域的写入,则会生成异常(标记为权限错误)。
10.1. 对非特权数据的特权访问
标准权限模型是特权较高的实体可以访问属于特权较低的实体的任何内容。换句话说,操作系统 (OS) 可以看到分配给应用程序的所有资源。例如,hypervisor可以查看分配给虚拟机的所有资源。这是因为在更高的异常级别执行意味着特权级别也更高。
然而,这并不总是可取的。恶意应用程序可能会试图欺骗操作系统代表应用程序访问数据,而应用程序不应该看到这些数据。这需要操作系统检查系统调用中的指针。
Arm 架构提供了几个控件来简化此操作。首先,有 PSTATE.PAN(从不特权访问)位。
当该位置位时,从 EL1(或 E2H==1 时的 EL2)加载和存储到非特权区域将产生异常(Permission Fault),如下图所示:
(注意是在 Armv8.1‑A 中添加的PAN。)
PAN 允许对非特权数据的意外访问被捕获。例如,操作系统执行访问认为目的地是特权的。事实上,目的地是没有特权的。这意味着操作系统的期望(目的地是特权)和现实(目的地是非特权的)之间存在不匹配。这可能是由于编程错误,也可能是系统受到攻击的结果。在任何一种情况下,PAN 都允许我们在访问发生之前对其进行捕获,从而确保安全操作。
有时操作系统确实需要访问非特权区域,例如,写入应用程序拥有的缓冲区。为了支持这一点,指令集提供了 LDTR 和 STTR 指令。
LDTR 和 STTR 是非特权加载和存储。即使在 EL1 或 EL2 由操作系统执行时,它们也会根据 EL0 权限检查进行检查。因为这些是明确的非特权访
问,所以它们不会被 PAN 阻止,如下图所示:
这允许操作系统区分旨在访问特权数据的访问和预期访问非特权数据的访问。这也允许硬件使用该信息来检查访问。
注意: LDTR 中的 T 代表翻译。这是因为第一个支持虚拟到物理转换的 Arm 处理器只针对用户模式应用程序,而不是操作系统。为了让操作系统访问应用程序数据,它需要一个特殊的负载,一个带有翻译的负载。
当然,今天所有软件都可以看到虚拟地址,但名称仍然存在。
10.2. 执行权限
除了访问权限,还有执行权限。这些属性允许您指定不能从地址获取指令:
- UXN. User (EL0) Execute Never (Not used at EL3, or EL2 when HCR_EL2.E2H==0) :EL0不允许执行
- PXN. Privileged Execute Never (Called XN at EL3, and EL2 when HCR_EL2.E2H==0):特权程序不允许执行
有单独的 Privileged 和 Unprivileged 位,因为应用程序代码需要在用户空间 (EL0) 中可执行,但绝不应使用内核权限 (EL1/EL2) 执行,如下图所示:
该架构还在系统控制寄存器 (SCTLR_ELx) 中提供控制位,以使所有可写地址都不可执行。
具有 EL0 写入权限的位置永远不能在 EL1 上执行。注意:请记住,Arm 建议始终将设备区域标记为从不执行 (XN)。
11 Access Flag :访问标志
您可以使用访问标志 (AF) 位来跟踪转换表条目所覆盖的区域是否已被访问。你可以设置AF 位:
- AF=0. Region not accessed.
- AF=1. Region accessed.
AF 位对操作系统很有用,因为您可以使用它来识别哪些页面当前未被使用并且可以被调出(从 RAM 中删除)。
注意:访问标志通常不用于裸机环境,您可以使用预先设置的 AF 位生成表。
11.1. 更新 AF 位
当使用 AF 位时,会创建转换表,且 AF 位最初是清零的。当一个页面被访问时,它的 AF 位被置位。软件可以解析表格去检查 AF 位是设置还是清除。AF=0表示该页面没用被访问,将其页面换出去可能是更好选择.
有两种方法可以在访问时设置 AF 位:‧
- 软件更新:访问页面会导致同步异常(Access Flag fault)。在异常处理程序中,软件负责设置相关转换表条目中的AF位并返回。
- 硬件更新:访问页面会导致硬件自动设置 AF 位,而无需生成异常,此行为需要启用并已添加到 Armv8.1‑A 中。
11.2. Dirty状态
Armv8.1‑A 引入了处理器管理块或页面的脏状态的能力。Dirty状态记录块或页是否已被写入。这很有用,因为如果块或页面被调出,Dirty状态会告诉管理软件是否需要将 RAM 的内容写出到存储中。
例如,让我们考虑一个文本文件。该文件最初从磁盘(闪存或硬盘驱动器)加载到 RAM 中。当它稍后从内存中删除时,操作系统需要知道 RAM 中的内容是否比磁盘上的内容更新。如果 RAM 中的内容更新,则需要更新磁盘上的副本。如果不是,则可以删除 RAM 中的副本。
当启用Dirty状态管理时,软件最初创建转换表条目,并将访问权限设置为只读,并设置 DBM(Dirty位修饰符)位。如果该页面被写入,硬件会自动将访问权限更新为读写。
将 DBM 位设置为 1 会更改访问权限位(AP[2] 和 S2AP[1])的功能,以便记录Dirty状态而不是记录访问权限。这意味着当 DBM 位设置为 1 时,访问权限位不会导致访问错误。
注意:不使用硬件更新选项也可以获得相同的结果。该页面将被标记为只读,导致第一次写入时出现异常(权限错误)。异常处理程序会手动将页面标记为可读写,然后返回。如果软件想要进行写时复制,则可能仍会使用此方法。
12 对⻬和大小端
12.1. 对齐
如果地址是元素大小的倍数,则访问被描述为对⻬。
对于 LDR 和 STR 指令,元素大小是访问的大小。例如,一条 LDRH 指令加载一个 16 位的值,并且必须来自一个 16 位的倍数的地址才能被视为对⻬。
LDP 和 STP 指令分别加载和存储一对元素。要对⻬,地址必须是元素大小的倍数,而不是两个元素的组合大小。例如:LDP X0, X1, [X2]
.
此示例加载两个 64 位值,因此总共 128 位。 X2 中的地址需要是 64 位的倍数才能被视为对⻬ . 同样的原则也适用于向量加载和存储。
当地址不是元素大小的倍数时,访问是不对⻬的。允许对标记为正常的地址进行未对⻬的访问,但不允许对设备区域进行非对⻬访问。对设备区域的未对⻬访问将触发异常(对⻬错误)。
通过设置 SCTLR_ELx.A 可以捕获对标记为 Normal 的区域的未对⻬访问。如果该位置位,对正常区域的未对⻬访问也会产生对⻬错误。
12.2. 大小端
在 Armv8‑A 中,指令提取始终被视为小端
对于数据访问,是否支持 little‑endian 和 big‑endian 由实现定义。如果只支持一个,则由实现定义支持哪一个。
对于同时支持大端和小端的处理器,字节序是按异常级别配置的。
注意:如果您不记得 IMPLEMENTATION DEFINED 的定义,请阅读介绍 Arm 架构。Arm Cortex‑A 处理器同时支持大端和小端。
13 内存别名和不匹配的内存类型
当物理地址空间中的给定位置有多个虚拟地址时,这称为别名。
属性基于虚拟地址。这是因为属性来自翻译表。当一个物理位置有多个别名时,所有虚拟别名都必须具有兼容的属性,这一点很重要。我们将兼容描述为:
- Same memory type, and for Device the same sub-type
- For Normal locations, the same cacheability and shareability
如果属性不兼容,则内存访问的行为可能与您预期的不同,这可能会影响性能。
此图显示了别名的两个示例。位置 A 的两个别名具有兼容的属性。这是推荐的方法。位置 B 的两个别名具有不兼容的属性(Normal 和 Device),这会对一致性和性能产生负面影响:
Arm 强烈建议软件不要将不兼容的属性分配给同一位置的不同别名。
14 Stage 1 和 Stage 2 的属性位的结合
使用虚拟化时,虚拟地址要经过两个转换阶段。一个阶段在操作系统的控制下,另一阶段在管理程序的控制下。 Stage 1 和 Stage 2 表都包含属性。这些是如何结合的?
下图显示了一个示例,其中阶段 1 将位置标记为device,但相应的阶段 2 转换标记为normal。结果类型应该是什么?
在 Arm 架构中,默认是使用最严格的类型。在此示例中,Device 比 Normal 更严格。因此,生成的类型是 Device。
对于类型和可缓存性,附加控件 (HCR_EL2.FWB) 允许覆盖此行为。设置 FWB 后,Stage 2 可以覆盖 Stage 1 的类型和可缓存性设置,而不是组合行为。
(注意: HCR_EL2.FWB 是在 Armv8.4‑A 中引入的。)
14.1 错误处理
我们先看一下 下图的两个示例;
在这两个示例中,结果属性都是 RO(只读)。如果软件要写入位置,则会产生错误(权限错误)。但是,在第一种情况下,这是一个阶段 1 故障,而在第二种情况下,这将是一个阶段 2 故障。在该示例中,阶段 1 故障将发送到 EL1 的操作系统,而阶段 2 故障将发送到 EL2 并由管理程序处理。
最后,我们来看一个 Stage 1 和 Stage 2 属性相同的例子:
关注"Arm精选"公众号,备注进ARM交流讨论区。