第四章:VxWorks内存保护机制:MPU工作原理、MPU区域配置、权限控制模型

好,咱们进入正题。内存保护这件事,说白了就是防止程序“乱来”。你想想看,一个跑在嵌入式设备上的系统,如果任务A不小心把任务B的数据踩了,或者某个野指针直接写到了内核空间,那后果是什么?轻则死机,重则设备失控。我早年做工业控制器时就遇到过这种事,一个中断服务程序里有个数组越界,直接把任务栈给干废了,查了三天才找到原因。从那以后,我对内存保护就特别上心。

VxWorks里实现内存保护,主要靠两个东西:MMU和MPU。MMU大家可能比较熟,带虚拟地址映射,功能强大。但MPU呢?它更轻量,更适合实时性要求极高的场景。今天咱们就专门聊聊MPU。

4.1 MPU的工作原理

MPU,全称Memory Protection Unit,内存保护单元。它不像MMU那样做地址翻译,它只做一件事:检查每一次内存访问是否合法。

怎么检查呢?CPU每次要读或写某个地址时,MPU会把这个地址跟它内部配置好的区域规则做比对。如果地址落在某个允许的区域内,并且访问类型(读、写、执行)也匹配,那就放行。否则,直接触发一个异常,把当前任务干掉。

嗯,这里要注意:MPU不改变地址,它只做检查。所以它几乎没有延迟开销。我个人的习惯是,在硬实时任务里,能用MPU就不用MMU,因为MMU的TLB miss有时候会带来不可预测的延迟。

核心要点:MPU是硬件级的访问控制,每个内存访问都经过它过滤。配置好之后,非法访问根本到不了内存控制器。

4.2 MPU区域配置

MPU把物理内存划分成若干个区域(Region)。每个区域都有起始地址、大小、以及访问权限。不同的ARM Cortex-M系列或者PowerPC芯片,支持的区域数量不一样,常见的是8个或16个。

配置一个MPU区域,你需要告诉硬件三件事:

  • 基地址:从哪个地址开始
  • 大小:这个区域有多大(必须是2的幂次,而且对齐)
  • 属性:可读?可写?可执行?是否允许缓存?

举个例子,在VxWorks里配置一个MPU区域,代码大概长这样:

#include <vxWorks.h>
#include <arch/arm/mpu.h>

/* 配置一个MPU区域:起始地址0x20000000,大小64KB,可读写,不可执行 */
MPU_REGION_CFG regionCfg;

regionCfg.regionNum = 0;          /* 区域编号 */
regionCfg.baseAddr  = 0x20000000; /* 基地址 */
regionCfg.size      = 0x10000;    /* 64KB */
regionCfg.attr      = MPU_ATTR_RW | MPU_ATTR_NON_EXEC; /* 读写,禁止执行 */
regionCfg.enable    = TRUE;

mpuRegionSet(&regionCfg);

这段代码我实际用过。当时有个任务需要一块大的数据缓冲区,我给它单独划了一个MPU区域,只允许读写,不允许执行代码。这样就算缓冲区被溢出,攻击者也很难注入代码执行。

小技巧:配置MPU区域时,区域大小必须严格对齐。比如64KB的区域,基地址必须是64KB的整数倍。我曾经因为基地址没对齐,配置死活不生效,查了半天手册才发现这个坑。

4.3 权限控制模型

MPU的权限控制模型,说白了就是“谁可以对这个地址做什么”。它通常分为三个维度:

  • 特权级访问:内核态(Supervisor)是否可以访问
  • 用户级访问:用户态(User)是否可以访问
  • 访问类型:读、写、执行

在VxWorks里,任务可以运行在两种模式:

模式 说明 典型场景
Supervisor模式 内核代码运行的模式,拥有最高权限 中断服务、系统调用、驱动
User模式 普通任务运行的模式,权限受限 应用任务、协议栈

MPU的权限控制,就是针对这两种模式分别设置。比如:

  • 内核代码区:Supervisor可读可执行,User不可访问
  • 任务栈区:Supervisor和User都可读写,但不可执行
  • 外设寄存器区:Supervisor可读写,User不可访问

我见过一个典型的配置错误:有人把任务栈设成了可执行。结果一个缓冲区溢出,攻击者把shellcode写到了栈上,然后跳转执行,系统直接沦陷。所以我的建议是:能不给执行权限,就坚决不给。

警告:千万不要把用户任务的栈或数据区设置为可执行。这是安全红线。我在做安全认证项目时,这条规则是硬性要求,通不过评审的。

4.4 实战中的避坑指南

讲了这么多理论,最后分享几个我实际踩过的坑:

  1. 区域重叠问题:MPU允许区域重叠,但优先级高的区域会覆盖优先级低的。如果你不小心把两个区域设成了重叠但权限不同,结果可能不是你想要的。我建议每个区域尽量不重叠,清晰明了。
  2. 上下文切换时MPU配置的保存与恢复:VxWorks在切换任务时,会自动保存和恢复MPU配置吗?不一定。如果你用的是老版本或者自己写的调度器,记得在任务切换时把当前任务的MPU区域配置保存下来,否则下一个任务可能拿到错误的权限。
  3. 调试时先关MPU:刚开始调试内存问题时,我建议先把MPU关掉。等基本功能跑通了,再打开MPU做保护。否则MPU一触发异常,你连断点都打不了,调试效率极低。

嗯,关于MPU的内容,今天就先聊到这儿。说白了,MPU就是个硬件级的“门卫”,你给它配好规则,它帮你盯着每一次内存访问。配置得当,系统稳如老狗;配置疏忽,调试到怀疑人生。下一章咱们会讲MMU,那个更复杂,但也更灵活。到时候再细聊。