1.开始
首先,在启动代码的开头附近调用 sb_MPUInit()。这会打开 MPU 并启用其背景区域。您的应用程序应该正常运行。注意:在 sb_MPUInit() 中禁止将 sys_code 加载到 MPU[5] [2]和 sys_data 到 MPU[6],因为这些区域尚未定义。
2. 系统区域
接下来,定义 .sys_code 和 .sys_data 部分。sys_code 应包含所有处理程序和 ISR shell [3]代码。如果 ISR 不使用 shell,则必须包含 ISR 本身。这是按照以下示例完成的,首先是汇编代码:
部分`.sys_code`:代码:NOROOTTHUMBsmx_PendSV_Handler:smx_MPU_BR_ON;打开 MPU 背景区域……;处理程序代码 smx_MPU_BR_OFF ;关闭 MPU 后台区域 cpsid f sb_INT_ENABLE pop {pc}then for C 代码:#pragma default_function_attributes =
@ ".sys_code"void sb_OS_ISR0(void) /* ISR shell */{ smx_ISR_ENTER(); /* 打开 MPU 后台区域 */ /* ISR body 或在此处调用 ISR smx_ISR_EXIT(); /* 关闭 MPU 背景区域 */}#pragma default_function_attributes =
sys_data 包含系统堆栈 (SS) [4]。只要 sys_code 和 sys_data 是 ISR 或处理程序运行所需的全部,就可以消除 BR 宏。
然后在链接器命令文件中:
定义导出符号scsz = 0x1000;定义导出符号sdsz = 0x400;定义块sys_code,大小为0x1000,对齐= scsz {ro section .sys_code};定义块sys_data,大小为0x400,对齐= sdsz {块CSTACK,块EVT} ;
当然,实际尺寸取决于应用。它们应该是足够大的下一个 2 的幂。对齐必须等于大小。现在在 sb_MPUInit() 中重新启用将 sys_code 加载到 MPU[5] 和 sys_data 到 MPU[6]。这些是存在于每个 ptask 的永久区域,如图 2 所示。它们也可能存在于 utasks 中,但在 umode 中无法访问,因为它们是特权区域。
3.超级区域
下一步是为系统中的 SRAM、ROM、DRAM、其他存储器和 I/O 区域定义超级区域。这些用作 BR 的临时替代品,直到定义了特定于分区或任务的区域。查阅链接器映射以确定起始地址以及每个内存中使用的内存量。然后为大小选择下一个较大的 2 次方。以下模板是一个示例:
const MPA mpa_tmplt_sr ={ {0x20000000 | V | 0, PRW_DATA | N67 | (0x11 << 1) | EN}, /* 正在使用的 SRAM */ {0x00200000 | V | 1、PCODE | N57 | (0x11 << 1) | EN}, /* 正在使用的 ROM */ {0xC0000000 | V | 2、PRW_DATA | (0x10 << 1) | EN}, /* 正在使用的 RAM */ {0x40040000 | V | 3、PRW_DATA | (0x11 << 1) | EN}, /* Synopsys HS */ {0x40011000 | V | 4、PRW_DATA | (0x09 << 1) | EN} /* UART1 */ };
超级区域包含每个内存或 I/O 区域的所有其他区域。因此,使用物理地址和大小更简单,如上所示,而不是通过在其中定义超级块来使链接器命令文件复杂化。在创建后,将此模板加载到内存保护阵列 (MPA) 中,用于每个要转换的任务。例如:
smx_Idle = smx_TaskCreate(ainit, PRI_SYS, 500, SMX_FL_LOCK, "idle");#if SMX_CFG_MPU smx_TaskSet(smx_Idle, SMX_ST_MPA, (u32)&mpa_tmplt_sr);#endif
加载任务的 MPA 时, 会设置其mpav标志。设置了 mpav 的任务在 BR 关闭的情况下运行;所有其他任务都在 BR 上运行。这允许一次处理一项任务。它还允许单独保留旨在保持 pmode 的任务。
现在运行系统。目标任务很可能会出现内存管理错误 (MMF)。这表明它需要访问其他东西,例如函数、静态变量或外围设备。处理这个问题可能需要扩大一个超过一个人想要的区域。但是,如果可以避免代码更改,这是此时的首选方法。(代码更改最好留待以后通过,当更多地了解需要什么时。)
对于在 BR 关闭的情况下运行的每个任务,刚刚获得了安全收益:处理程序、ISR 和其他任务像以前一样运行,但此任务正在以减少的内存区域运行,并且这些区域具有严格控制的属性(RO、XN等)很有可能潜在的错误会开始出现并得到修复——尤其是当 SOUP 很厚而评论很薄的时候。
4. Pmode 操作
下一步是 pmode 操作。为简单起见,我们假设单个任务 taskA 被隔离。这一步处理的任务必须在超区域运行,BR关闭。因此,MPU[5] 和 MPU[6] 中需要 sys_code 和 sys_data 区域来处理异常。
第一步是将代码和数据分组到特定于任务的区域中,并在链接器命令文件中定义块以保存这些区域。以任务命名它们很方便,例如:taskA_code 和taskA_data或以分区命名它们,例如 usbh_code和usbh_data。
接下来,定义公共代码和数据区域来保存 RTOS 和其他系统服务,并保存它们所需的公共数据。我们分别将这些命名为pcom_code 和pcom_data。此时taskA是一个ptask,所以pcom_code需要包含taskA需要的RTOS等系统服务,pcom_data需要包含这些服务需要的数据
然后,创建 mpu_tmplt_taskA 并添加代码将其加载到 taskA 的 MPA 中,如步骤 3 所示。此时 mpa_tmplt_sr 已替换为该任务的 mpu_tmplt_taskA。taskA 是独立的,与所有其他任务部分隔离。会跑吗?这就是“轮胎与道路相遇”的地方。来自 taskA 的 MMF 可能是由于其区域之外的引用或由于属性违规(例如写入 ROM)。前者表示任务或分区需要访问预期之外的其他代码或其他数据。
在许多情况下,解决 MMF 可能只是将 taskA 特定的代码和数据分别移动到 taskA_code 和 taskA_data 区域。为任务分配区域因任务而异——即使在同一分区内。有些任务可能不需要所有的代码和数据区域,但可能需要其他区域,例如 I/O 区域。标准 C 库调用也可能出现。它们的库可以包含在链接器命令文件的 pcom_code 中。
对于 pmode,SB_MPA_SIZE 可以增加到 5。如果这还不够,可能需要将一个或多个区域合并为更大的区域。例如,在 pmode 的 mpa_tmplt_usbh(参见图 2)中,UART1 和 Synopsys 区域已合并为一个名为 apb0_csr 的区域。该区域包含所有 APB0 外设,这是不可取的。然而,这一举动将在下一步中逆转。
5. Umode 操作
最后的步骤是将 SMX_CFG_UMODE 设置为 1 并将 taskA 移动到 umode。这是通过设置其 umode 标志来完成的,如下所示:
#if SMX_CFG_UMODE smx_TaskSet(taskA, SMX_ST_UMODE, 1);#endif
这通常在步骤 4 中显示的 smx_TaskSet() 之后。现在,当调度 taskA 时,sb_PendSV_Handler() 将在开始任务之前设置处理器 CONTROL 寄存器 = 3。这会导致处理器使用任务的堆栈以非特权线程模式运行。当任务恢复时,EXE_RETURN 值 (0xFFFFFFED) 也会导致这种情况。
此外,添加:
#if SMX_CFG_UMODE#include "xapiu.h"#endif
umode 中调用系统服务的代码。这会强制 taskA 将 SWI API 用于这些调用。只在 pmode 中运行的 taskA 函数(例如初始化)应该移到 taskA 模块的顶部或放入它们自己的模块中。在前一种情况下,#include “xapiu.h”放在它们之后,直到模块末尾有效;在后一种情况下,它根本不应该放在模块中。#include “xapiu.h” 也不需要在不进行其中列出的任何调用的函数之前。
如果错过了进行列表调用的函数,当函数在 umode 中运行时,该调用将导致 MMF,因此很容易找到和修复。
在实际运行 taskA 之前,必须更改 mpu_tmplt_taskA。taskA_code 和 taskA_data 区域应该保持不变。但是,pcom_code 和 pcom_data 必须替换为 ucom_code 和 ucom_data。至少 smx 和系统服务调用函数必须替换为 smx 和系统服务外壳。然而,要做的可能远不止这些。
如 mpa_tmplt_usbh 所示,在图 2 中,syn_csr 和 ur1_csr 区域已分开。这通过阻止 usbh 和 fs 任务访问 APB0 总线上的所有其他外设显着提高了安全性,这在步骤 4 中是可能的。此外,SB_MPA_SIZE 必须增加到 6。MPU_CTRL 中的 BR 标志现在自动设置为1 用于 utasks 以处理中断和异常,并且 utasks 不再需要 sys_code 和 sys_data,但它们仍然被隔离的 ptasks 需要。如果需要,可以将 MPA[6] 用于额外的区域,方法是将 SB_MPA_SIZE 增加到 7。因此,最多有 7 个区域可用于 utask,而最多只有 5 个区域可用于隔离 ptask。第 8个 区域 MPU[7] 为任务堆栈保留,由任务调度程序控制。
当 taskA 首次作为 utask 开始运行时,您可能会看到 MMF 和 PRIVILEGE VIOLATION 错误。后者表明 taskA 正在发出受限制的服务调用。这可能需要重新编码以不在 taskA 中使用这些服务,而是从 ptask 调用它们。或者将 taskA 拆分为调用这些服务的 ptask 和不调用这些服务的 utask 可能会更好。或者,taskA 可以作为 ptask 启动,进行所有受限制的服务调用,然后作为 utask 重新启动。(它必须重新启动自己,以便 PendSV_Handler() 将 CONTROL 更改为 0x3。)
限制 utask 的系统服务很重要,因为如果 utask 中的恶意软件可以调用例如删除另一个任务或关闭系统,那么安全性显然不好。但是,可能有必要在第一遍中允许这些弱点,以避免过度重新编码。可以将不同版本的 xapiu.h 应用于不同的分区可能会有所帮助。因此,相对好的代码可能比不太好的代码有更多的自由。
如果将 ptask 转换为 utask 导致它与另一个 ptask 共享代码或数据,则可能存在问题。可能的解决方案是:
将所有任务放入一个分区并一次转换整个分区,而不是逐个任务。如果所有的 ptasks 都注定是 utasks,那就没问题了。
定义两个任务都可以访问的公共区域。这对于 C 库等常见代码可能是可以接受的,但对于 pdata 是不可接受的。
通过消息或管道传递全局数据。
将 taskA 拆分为 ptask 和 utask,其中 ptask 与其他 ptask 共享 pcode 和 pdata,而 utask 不共享。
将任务作为 ptask 启动以执行任务初始化所需的 pfunction,然后将其切换为仅执行 ufunction 的 utask。
使用不同名称复制常见例程。
重构任务通常涉及最少的代码更改——复杂的代码通常驻留在从任务调用的子例程中,它们可能不会受到影响。
大小和性能影响
MPU-Plus 向应用程序添加了大约 2000 字节的代码。它将每个 MPA 模板的 8*SB_MPA_SIZE * NUM_TASKS 字节的 RAM 和 8*SB_MPA_SIZE 字节的 ROM 添加到应用程序代码中。对于示例端口,浪费的代码空间仅为 14.5%,浪费的数据空间甚至更小 2.5%。由于这是大量不是为 MPU 定制的代码,因此这些可能是典型的数字。如果浪费的代码空间仍然太多,使用较小的区域会有所帮助(数据空间的区域比代码空间小)。还有其他技术可以应用。
由于在 umode 中可以直接访问 I/O 区域,并且由于代码在 umode 中的运行速度与在 pmode 中一样快,因此性能下降主要是由于系统服务和任务切换的开销。smx_SemTest() 或 smx_SemSignal() 等典型系统服务的执行开销约为 100%。对于较长的系统服务,开销百分比较小,因为开销是每个系统服务的固定时间量。任务启动的开销约为 25%,用于将任务的 MPA 加载到 MPU,以及其他 MPU 条件代码。任务恢复大约是这个的一半,任务暂停或停止可以忽略不计。
结论
前面表明,有一个可行的分步过程来逐步提高具有 MPU 的 Cortex-M 嵌入式系统的安全性,并且该过程可以在已经发布的系统上执行,也可以由没有编写代码的人执行。尽管可能需要进行大量工作,但已经定义了一条清晰的路径,以经济高效的方式实现安全目标。这总比什么都不做要好,应该在灾难发生之前加以考虑。