STM32 MPU 详解与变量绝对定位指南
很多开发者在接触 STM32 MPU(Memory Protection Unit)时,往往只学会了“怎么开”,却卡在了“怎么用”。
你配置了 MPU 保护 0x24000000 这一块内存,但你怎么确信你关心的关键变量(比如 SystemConfig)真的落在了这块地盘上?
本文将从 MPU 原理讲起,手把手教你打通 链接脚本/分散加载文件 -> 变量属性 -> MPU 配置 的全链路,涵盖 STM32CubeIDE (GCC) 和 Keil MDK 两种环境。
一、 什么是 MPU?为什么要用它?
MPU(内存保护单元) 是 Cortex-M 内核中的一个硬件组件,它像一个**“内存海关”**。
在没有 MPU 的裸机或 RTOS 系统中,所有代码都可以随意读写 4GB 空间。这带来了极大的风险:
- 野指针踩踏:一个缓冲区的溢出(Buffer Overflow)可能悄悄改写了系统控制标志位。
- 代码区被改:程序跑飞后,错误地向 Flash 或 RAM 中的代码段写入数据,导致指令被篡改。
- 栈溢出:任务堆栈耗尽,直接覆盖了邻居任务的 TCB(任务控制块)。
MPU 的作用:一旦 CPU 试图访问未授权的地址(比如写“只读区”,或在“不可执行区”运行代码),MPU 会立即拦截并触发 MemManage_Handler 异常,让系统在造成更大破坏前停下来。
二、 核心原理:MPU 保护的是“地盘”
MPU 保护的是物理地址,不是变量名。要实现精准保护,必须分三步走:
- 圈地:在链接脚本(GCC)或分散加载文件(Keil)中划分专属区域。
- 落户:在 C 代码中强制将变量指定到该区域。
- 设防:配置 MPU 保护该物理区域。
三、 第一步:划分内存区域 (IDE 分类教学)
我们需要把 0x24000000 开始的 32KB 内存单独划出来,作为“安全区”。
方案 A:如果你用 STM32CubeIDE (GCC)
找到工程目录下的 .ld 文件(如 STM32H7xx_FLASH.ld),修改 MEMORY 和 SECTIONS:
/* 1. 在 MEMORY 中挖出一块 RAM_SAFE */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
/* 新增:定义 RAM_SAFE 区域,起始 0x24000000,大小 32K */
RAM_SAFE (xrw) : ORIGIN = 0x24000000, LENGTH = 32K
/* 剩下的区域改个名字或缩小,避免重叠 */
RAM_D1 (xrw) : ORIGIN = 0x24008000, LENGTH = 480K
}
/* 2. 在 SECTIONS 中定义输出段 */
SECTIONS
{
/* ... 其他段 ... */
/* 定义一个段名为 .safe_zone_data,放到 RAM_SAFE 区域 */
.safe_zone_data :
{
. = ALIGN(4);
KEEP(*(.safe_zone_data)) /* 匹配 C 代码中的 section */
. = ALIGN(4);
} >RAM_SAFE
}
方案 B:如果你用 Keil MDK (ARMCC/ARMCLANG)
Keil 不使用 .ld,而是使用 分散加载文件 (.sct)。
- 点击魔术棒 (Options for Target) -> Linker。
- 勾选
Use Memory Layout from Target Dialog取消掉。 - 点击
Edit...编辑分散加载文件。
修改代码如下:
; 这是一个典型的 Keil Scatter File 示例
LR_IROM1 0x08000000 0x00200000 { ; load region size_region
ER_IROM1 0x08000000 0x00200000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 { ; 普通 RAM 区域
.ANY (+RW +ZI)
}
; === 新增部分:定义 MPU 安全区 ===
; 名字随意,地址必须是 0x24000000,大小 0x8000 (32KB)
RW_RAM_SAFE 0x24000000 0x00008000 {
; 将所有标记为 .safe_zone_data 的变量放这里
*(.safe_zone_data)
}
}
四、 第二步:在 C 代码中“对号入座”
无论你用 Keil 还是 GCC,只要按上述配置好了链接文件,C 代码的写法是通用的(Keil AC5/AC6 均支持标准 attribute 写法)。
typedef struct {
uint32_t DeviceID;
uint32_t SecretKey[4];
float CalibrationData;
} SystemParam_t;
/* * 使用 __attribute__((section("段名")))
* 强制将变量放到刚才定义的 .safe_zone_data 段中
*/
SystemParam_t g_SysParam __attribute__((section(".safe_zone_data"))) = {
.DeviceID = 0x12345678,
.CalibrationData = 1.05f
};
/* 验证函数:打印地址确认是否成功 */
void Check_Address(void) {
// 应该输出 0x24000000
printf("SysParam Addr: %p\r\n", &g_SysParam);
}
五、 第三步:配置 MPU 设防
变量到位后,使用 HAL 库配置 MPU 规则。此代码通用。
我们将该区域配置为 “特权级只读 (Privileged RO),用户级禁止访问”。
void MPU_Config_SafeRegion(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 1. 禁止 MPU */
HAL_MPU_Disable();
/* 2. 配置 Region 0 */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
// 必须和链接文件/分散加载文件中的地址一致
MPU_InitStruct.BaseAddress = 0x24000000;
// 大小必须覆盖变量,且符合 Cortex-M 对齐规则 (2^N)
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
// 权限核心:特权级只读,用户级不可访问
// 效果:代码只能读,不能直接写。写操作会触发 HardFault/MemManage
MPU_InitStruct.AccessPermission = MPU_REGION_PRIV_RO_USER_INVALID;
// 禁止执行 (XN)
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
// 内存属性 (STM32H7 建议不用 Cache 防止一致性麻烦,F4 可根据需要开启)
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 3. 使能 MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
六、 如何安全地更新数据?
因为设为了“只读”,直接赋值 g_SysParam.CalibrationData = 2.0; 会导致死机。你需要一个“后门”函数:
void Update_SysParam(SystemParam_t *newVal)
{
// 1. 临时关闭 MPU 保护
HAL_MPU_Disable();
// 2. 只有在此期间才能写入数据
memcpy(&g_SysParam, newVal, sizeof(SystemParam_t));
// 3. 立即恢复保护
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
七、 避坑指南
Keil 的名为
at的黑魔法: 在 Keil 中你可能见过int var __attribute__((at(0x24000000)));。- 优点:不用改分散加载文件,简单。
- 缺点:它只占用了那个变量大小的空间,而 MPU Region 最小也是 32B 且要是 2 的幂。如果变量很小,MPU 保护范围会比变量大,容易误伤旁边的无辜变量。
- 建议:正规军请使用“分散加载文件 + section”的方法,这样 Linker 会自动把无关变量挤出这 32KB 的安全区。
地址对齐:MPU Region 的 BaseAddress 必须是 Size 的整数倍。如果你定义 Size 为 32KB,地址只能是
0x24000000、0x24008000等,不能是0x24000010。调试:如果触发了异常,查看
SCB->CFSR寄存器:MMFSR(Memory Manage Fault Status Register) 会指示是否发生了数据访问违规 (DACCVIOL)。
总结:
- Keil 用户:改
.sct分散加载文件。- GCC 用户:改
.ld链接脚本。- C 代码:用
__attribute__((section("...")))定位。- MPU:按物理地址设防。
四者合一,你的 STM32 系统就有了真正的“防弹衣”。