本节摘要:默认构建里所有任务同住特权态,一个野指针可以改写任何角落——包括内核自己。内存保护单元(MPU)版本的内核改变这一切:任务分特权与受限两级,每个受限任务只被授予自己的栈、明确的程序区与数据区,越界与越权在硬件层第一次发生时即触发异常。本节讲清这个模型的构建形态、区域划分的实操、受限创建接口的用法、它能拦与不能拦的边界,以及开启它要付的真实代价。
前六章的所有防护都是"软件级"的:溢出检测要等切换时才查(2.4 节的盲区清单)、堆校验要等释放时才验(5.2 节)。它们共同的天花板是:检查发生在事后。本节引入的是唯一"事前零漏报"的防线——硬件内存保护。
默认构建的任务模型可以概括为"人人皆王":每个任务都以特权态运行,地址空间全开放。这个模型在小而受控的固件里没毛病——代码都是自己写的,出了错大家一起死,至少死得平等。但它有三个日益放大的风险。其一,缺陷半径无界:任何一个任务里的野指针(越界数组、悬垂指针、栈溢出)都能改写内核数据结构、别的任务的栈、甚至中断向量表——2.4 节那类"症状漂移"的工单,病根都是缺陷半径太大。其二,第三方代码不可控:现代固件里塞着协议栈、文件系统、脚本引擎,这些代码的体积与缺陷率都远超自家业务,让它们与内核同权是巨大的暴露面。其三,安全攻击的入口:联网设备上,一个被攻破的协议栈如果能直接改内核结构,攻击者就能提权驻留。三个风险指向同一个解法:把缺陷与攻击关进各自的任务笼子。
开启内存保护支持的构建里,任务分两种形态。特权任务:与默认构建无异,全地址空间可访问——内核任务、你最信任的核心业务住这里。受限任务:以非特权态运行,只能访问被明确授予的区域,越权即硬件异常——协议栈、第三方库、新员工的代码都该住这里。
受限任务的"笼子"由一组硬件区域(典型数量为八个,随内核实现而定)拼成,每个区域一段地址范围加一组权限(读、写、执行)。FreeRTOS 的受限任务标准配置把区域用在三处:自己的栈(可读写,栈界之外一步即异常——2.4 节"检测盲区"的终极解法)、允许执行的程序区(通常包括共享内核代码与该任务自己的代码,只读加可执行)、允许访问的数据区(自己的全局变量、共享的只读常量)。程序区之外不可执行这条权限,顺带堵住了"把数据当代码执行"的经典攻击路径。
/* 受限任务的创建:区域表随任务声明,静态栈单独给出 */ static const TaskParameters_t xSensorTaskParams = { vSensorTask, /* 任务函数 */ "sensor", /* 名字 */ 128, /* 栈深 字 */ NULL, /* 参数 */ 3, /* 优先级 */ vSensorTaskStack, /* 栈数组 受限任务必须外部提供 */ { /* 区域表:起始地址 大小 权限 */ { vSensorTaskStack, sizeof vSensorTaskStack, PORT_MPU_REGION_READ_WRITE }, { vSensorTaskCode, vSensorCodeSize, PORT_MPU_REGION_READ_EXECUTE }, { vSensorData, vSensorDataSize, PORT_MPU_REGION_READ_WRITE }, /* 末项约定填全零表示区域表结束 */ } }; void create_sensor(void) { TaskHandle_t h; /* 受限创建接口:额外参数指定栈与区域表的位置 */ xTaskCreateRestricted(&xSensorTaskParams, &h); }
接口与普通创建的区别就两点:栈必须由创建者提供(区域表要覆盖它)、区域表随任务声明。系统调用怎么办?受限任务不能直接进内核数据区,它调用内核接口时经由一层包装:先临时提权(切换到特权栈)、执行内核逻辑、再降回。这层包装就是受限版本内核与普通内核的主要代码差异,也是它性能代价的主要来源。
能拦的三类:越界写(栈溢出、数组冲界,第一次写就越界即触发,零漏报零延迟);越权访问(受限任务读改别人的数据、碰内核结构,同上即刻拦截);代码注入执行(程序区外的任何地址不可执行)。拦不了的:特权任务自己犯的错(它们同住特权态,互相之间无防线——所以特权任务该少而精);共享区内的错误(两个任务都被授予的公共读写区,彼此仍能伤害);逻辑错误(权限管得住"写到哪",管不住"写错值")。
代价清单要诚实:其一,每次系统调用的提权降权开销(受限任务频繁调内核接口时可观);其二,区域数量有限,区域表要精心规划,划不进区域的访问就得开共享(防线打折);其三,构建与调试复杂度上升(受限任务里有些调试操作受限);其四,第三方代码搬进受限任务通常要一轮适配(它们的内存布局要显式声明)。工程结论:按暴露面分级使用——网络-facing 的、第三方的、新进的代码进受限区;核心控制逻辑保持特权,享受零开销。
⚠️ 迁移到 MPU 构建的高发坑:把大而杂的既有模块整体塞进一个受限任务,区域表立刻不够用,被迫开大片共享区——防线名存实亡。正确姿势是先按"数据归属"拆分模块,一个模块一个受限任务、一张小区域表;拆不动的模块先留在特权区,列入整改清单逐步收敛。
MPU 不是替代前面的软件防线,而是补上它们的盲区,全套防线的分工至此完整:溢出检测(2.4 节)低成本广覆盖,量产可保留一级;水位巡检给"将溢未溢"的慢性病画像;堆校验抓越界写堆;MPU补上三者都做不到的"第一次越界当场抓获"。推荐组合是分层部署:开发期全开(MPU 加二级检测加巡检),量产期按产品性质裁剪——安全关键产品 MPU 必留,成本敏感的消费电子至少保留一级检测加水印巡检。
异常处理也有讲究:MPU 触发的异常携带出错地址与访问类型,处理函数应把它格式化为"某任务访问某地址越权"的工单记录(掉电存储),这比默认的硬故障信息 actionable 得多——7.3 节的解剖流程会直接消费这里的输出。
围栏立好了,下一节装仪表盘:运行时统计怎么把"系统现在怎么样"变成一条命令的事,CPU 负荷画像又怎么从数字里读出病灶。