ARM Cortex-M 架构于 2004 年推出,是目前市场上最流行的 32 位架构,被大多数(如果不是所有)主要 MCU 制造商采用。Cortex-M 从一开始就设计为对 RTOS 内核友好:专用的 RTOS 滴答计时器、上下文切换处理程序、用 C 编写的中断服务例程、尾链、简单的临界区管理等等。许多 Cortex-M MCU 实施都辅以浮点单元 (FPU)、DSP 扩展、高度通用的调试端口和内存保护单元 (MPU)。
在这个四部分系列的第 2 部分中,让我们看看如何使用 Cortex-M MPU 来提高嵌入式设备的安全性。在此处阅读其他三个部分:第 1部分、第 3部分和第 4 部分。
ARM Cortex-M
2004 年,Arm 推出了基于精简指令集计算机 (RISC) 架构的新系列 CPU 内核,称为 Cortex-M(M 代表微控制器)。第一个 Cortex-M 被称为 Cortex-M3,该系列已经发展到包括许多衍生内核:Cortex-M0/M0+、Cortex-M4、高性能 Cortex-M7,以及最近推出的 Cortex-M23 和M33 采用 TrustZone 安全技术。
Cortex-M 处理器系列的程序员模型(见图 1)高度一致。例如,R0 到 R15、PSR、CONTROL 和 PRIMASK 可用于所有 Cortex-M 处理器。两个特殊寄存器 - FAULTMASK 和 BASEPRI - 仅在 Cortex-M3、Cortex-M4、Cortex-M7 和 Cortex-M33 上可用,浮点寄存器组和浮点状态和控制寄存器 (FPSCR) 在可选浮点内的 Cortex-M4、Cortex-M7 和 Cortex-M33。一些 Cortex-M 实现还配备了内存保护单元 (MPU)。
【图1 | 基于 Armv7-M 的 CPU 寄存器模型。]
在上下文切换期间,RTOS 会保存和恢复 CPU 寄存器和 FPU 寄存器(假设处理器配备了一个)。因为 MPU 配置是从表格中获取的,所以我们只需要在任务切换时加载 MPU 寄存器。换句话说,不需要为被切换的任务保存MPU配置。详细信息将在下一节中描述。
Cortex-M 特权级别
上电时,Cortex-M 以特权模式启动,使其能够访问 CPU 的所有功能。它可以访问任何内存或 I/O 位置,启用/禁用中断,设置嵌套向量中断控制器 (NVIC),以及配置 FPU 和 MPU,等等。
为了保证系统的安全和可靠,特权模式代码必须保留给经过充分测试且已知可信的代码。由于大多数 RTOS 都经过了彻底的测试,因此 RTOS 通常被认为是受信任的,而大多数应用程序代码则不是。这种做法很少有例外。例如,通常假定 ISR 是受信任的,因此也可以在特权模式下运行,只要这些 ISR 不被滥用并尽可能短。这是大多数 RTOS 供应商的典型建议。
可以使应用程序代码以非特权模式在 Cortex-M 上运行,从而限制代码可以执行的操作。具体来说,非特权模式会阻止代码禁用中断、更改嵌套向量中断控制器 (NVIC) 的设置、将模式改回特权模式以及更改 MPU 设置以及其他一些事情。这是一个理想的特性,因为我们不希望不受信任的代码赋予自己特权,从而改变系统设计者实施的保护。
由于 CPU 总是以特权模式启动,因此需要从一开始就创建任务以在非特权模式下运行,或者在启动后不久切换到非特权模式(通过调用 API)。一旦进入非特权模式,CPU 只能在服务中断或异常时切换回特权模式。
SVC 处理程序
由于非特权代码无法通过 CPU 或 NVIC 禁用中断,因此应用程序代码被迫使用 RTOS 服务来访问共享资源。因为 RTOS 服务需要在特权模式下运行(在关键部分禁用中断),非特权任务必须通过 Cortex-M 上称为 SuperVisor Call (SVC) 的特殊机制才能切换回特权模式。SVC 的行为类似于中断,但由一条名为 SVC 的 CPU 指令调用。这也称为软件中断。
在 Cortex-M 上,SVC 指令使用一个 8 位参数来指定调用者想要执行的 256 个可能的 RTOS 函数(或服务)中的哪一个。系统设计者决定哪些 RTOS 服务应该对非特权代码可用。例如,您可能不希望允许非特权任务终止另一个任务(或它本身)。此外,这些服务都不允许禁用中断,因为这会破坏在非特权模式下运行代码的原因之一。一旦被调用,SVC 指令将引导至称为 SVC 处理程序的异常处理程序。
这个过程如图 2 所示。 (1) 一些非特权代码执行 SVC #5 以等待互斥体。(2) SVC指令强制SVC异常处理程序执行。该行为与生成中断时相同。SVC 处理程序提取参数(即值 5)并使用该参数将 (3) 索引到 SVC 跳转表中。(4) 执行所需的 RTOS 服务(特权模式),完成后,RTOS 返回到非特权代码。
SVC 处理程序是 RTOS 的一部分,因此您不必担心实现它。事实上,无论您的任务是在特权模式还是非特权模式下运行,您的应用程序代码都会调用相同的 RTOS API。
通过 SVC 处理程序需要付出代价:额外的代码和 CPU 周期。在 Cortex-M3 上,SVC 处理程序添加了大约 1 KB 的代码并执行 75 到 125 条 CPU 指令来执行。因此,与从特权模式调用相同的 RTOS 服务相比,由非特权模式调用的任何 RTOS 服务都需要更多的处理时间。
【图2 | 限制来自非特权代码的 CPU、NVIC 和 MPU 访问。]
在非特权模式下运行代码还可以防止用户代码禁用中断,从而减少锁定系统的机会。当然,如果用户代码进入无限循环,锁定仍然可能发生,尤其是在高优先级任务或 ISR 中发生这种情况时。但是,在这种情况下,可以通过使用看门狗来恢复锁定。
附带说明一下,如果非特权任务尝试通过 NVIC 禁用中断,Cortex-M 会生成故障(总线故障)。您的应用程序代码需要考虑到这一点。
在非特权模式下运行仍然不会阻止应用程序代码访问任何内存位置和外围设备或阻止代码在 RAM 之外执行。这就是 MPU 的用武之地。
Armv7-M 架构中的 Cortex-M MPU
Cortex-M(假设为 Armv7-M)上的 MPU 是一种设备,它允许进程访问多达八 (8) 或十六 (16) 个内存或外围区域(取决于 MCU 实现)。每个区域的位置和大小是可配置的。每个区域的大小必须是 2 的幂的倍数,但不能小于 32 字节。此外,区域的基地址必须与区域大小的整数倍值对齐。因此,如果该区域为 8K 字节,则该区域必须在 8K 边界上对齐。由于 MPU 中可用的区域相对较少,因此区域通常用于限制对 RAM 和外围设备的访问,而不是太多代码。但是,必须使用至少一个区域来提供对代码空间的访问。
组织内存的一种方便方法是将进程所需的 RAM 分组到一个连续的块中,如图 3 所示。每个进程都将以类似的方式设置。进程 A 的扩展视图显示它由四个任务组成,每个任务都有自己的堆栈。进程 A 还管理一个外围设备。空白代表可能由于 MPU 的对齐限制而未使用的内存或 I/O 空间。
【图3 | 按进程对区域进行分组。]
F3(1) 需要一个 MPU 区域来提供对代码空间的访问。该区域可以设置为只允许访问与进程关联的代码,但是当一个进程与其他进程共享代码(即库)时,有时可能会出现问题。
F3(2) 需要一个 MPU 区域来允许进程内的所有任务访问分配给进程的外围设备。例如,如果进程 A 管理一个以太网控制器,则该区域必须允许访问与该设备关联的所有寄存器。
F3(3) MPU 区域用于访问分配给进程的所有 RAM。这里假设进程全局变量和进程堆由进程内的所有任务共享。附带说明一下,不可能使用所有进程都可以使用的全局堆,因为您无法设置 MPU 表来将一个进程的动态分配内存与另一个进程的动态分配内存分开。
F3(4) MPU 区域用于 RedZone 堆栈检查。事实上,我们只需要一个区域来覆盖一个进程中的所有任务堆栈,因为我们只需要在上下文切换期间移动 RedZone。然而,这意味着每个任务将需要一个稍微不同的 MPU 进程表。话虽如此,这在很大程度上取决于 RTOS 在上下文切换期间如何管理 MPU。例如,RTOS 可能决定只加载 MPU 进程表中的前七个区域,并使用堆栈的基地址加载最后一个区域以设置 RedZone。大多数时候,RTOS 将任务堆栈的基地址存储在任务的控制块 (TCB) 中。使用这种方案,一个进程中的所有任务可以共享完全相同的进程表,同时为任务堆栈正确设置 RedZone。
F3(5) 这表示由于 Cortex-M 的 MPU 要求所有区域的大小必须是 2 的二进制幂而导致的未使用 RAM。因此,如果进程 A 需要 7 KB 或 RAM,则由于进程 A 需要 8 K,因此会丢失 1K。您可能只想增加某些堆栈的大小,而不是浪费该空间在进程中减少堆栈溢出的机会。但是,这样做的缺点是,如果您需要向进程添加功能,那么您可能不记得可以回收多少内存。事实上,从安全关键的角度来看,如果您使用内存配置来限定您的系统,那么您可能无法收回它。因此,最好分配进程所需的堆栈并忍受浪费的空间。
从程序员的角度来看,Cortex-M MPU 是一个相当简单的设备,它由 19 个 32 位寄存器组成,如图 4 所示。您会注意到,该模型与图 1 中的模型不同,因为一些寄存器实际上是存储的,因此可以间接寻址,但在内部,它们就是这样出现的。
【图4 | Cortex-M MPU 寄存器。]
TYPE寄存器用于决定MPU支持的MPU区域数量,该寄存器的DREGION字段会一直读为0、8或16。CTRL寄存器用于配置MPU的某些方面,但实际上,该寄存器用于启用或禁用 MPU。事实上,在更改任何或所有区域的配置之前,应禁用 MPU。RNR编号允许您寻址特定的 MPU 区域。
参考图 4,您会注意到 RBAR 的低五位具有固定值。当设置为 1 时,“V 位”表示低 4 位用于指定区域编号。RBAR 的高位用于指定区域的基地址。基地址必须在与区域大小匹配的边界上对齐;例如,1 KB 区域必须在 1 KB 边界上对齐。
在大多数情况下,为给定区域设置属性非常简单:
RASR.XN 当区域覆盖 RAM 并且您不希望在该区域之外执行代码时,强烈建议您将此位设置为 1。这将捕获来自黑客的代码注入攻击。
RASR.AP: 如果该区域覆盖了 RAM 区域,那么您将这些位设置为“011”,如果该区域覆盖 ROM,则将该字段设置为“110”。
RASR.TEX SCB 图 4 显示了基于内存区域所在位置的这些位的典型值。
RASR.SRD 此字段允许您将区域细分为八个相等的部分。此功能可以大大减少内存浪费。例如,一个 16 KB 的区域有八个 2 KB 的子区域,因此如果一个进程只需要 5 KB(3 个子区域),那么您可以禁用其中的五个子区域并将它们分配给不同的进程。
RASR.SIZE 这个字段设置起来有点复杂,因为它需要一些人工干预,并专门查看链接器映射文件以确定两个大小属性的编码二进制幂。
RASR.EN 该位启用 (1) 或禁用 (0) 区域。如果您不需要所有八个区域,则必须禁用该区域,以免无意中启用来自不同进程的区域。
清单 1 显示了加载所有八个 MPU 区域的优化函数的汇编语言代码。我将此作为一个示例,说明我们可以如何有效地更改 MPU 配置,但这不是您必须担心的事情。确定管理 MPU 的最佳方式确实是 RTOS 的责任。但是,您需要遵循 RTOS 指南,了解如何为每个任务设置 MPU 进程表。对于这个特定的实现,您需要创建一个 MPU 进程表来分配所有八个区域,即使使用的区域更少。该函数的原型是:
void OS_MPU_ProcessSet (ARM_MPU_Region_t *p_process);
p_process 是指向包含八对 RBAR 和 RASR 值的 MPU 进程表的指针。ARM_MPU_Region_t 是 ARM 的 Cortex 微控制器软件接口标准 (CMSIS) 3定义的数据类型,声明如下:
typedef struct
{
uint32_t RBAR; // Region base address
uint32_t RASR; // Region attributes (type, region size, enable, etc.)
} ARM_MPU_Region_t;
因此,对于每个任务,您需要声明一个包含八个条目的 ARM_MPU_Region_t 数组,如下所示:
请注意,最后一个条目包含任务堆栈的基地址,并且还假定 RedZone 大小为 32 字节。
[清单 1 | 配置所有 8 个 MPU 区域。]
概括
Cortex-M 中的 MPU 是一个相当简单的设备。RTOS 负责在每次上下文切换时配置 MPU。但是,为应用程序设置 MPU 进程表是应用程序开发人员的责任。如果 RTOS 为每个任务设置了 RedZone,则一个进程中的任务可以共享同一个 MPU 进程表。
要让应用程序在 MPU 上运行,仍然需要注意一些事项。具体来说,如何按进程对 RAM 进行分组?一个进程如何与另一个进程通信?如果任务访问其分配的内存空间之外的内存或外围设备会发生什么?除了任务栈,内核对象是否应该分配在进程内存空间中?我们将在第 3 部分解决这些问题。
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !