MCU 进程内存布局详解
进程/任务的完整内存布局
首先, MCU一般是不带MMU的, 所以我们只考虑单进程的情况
这是MCU的地址布局
0x00000000 Flash(代码)
0x20000000 SRAM(数据)
0x40000000 外设寄存器
注意 : 这只是一个参考, 不同厂商定义的地址可能不相同, 有些SRAM起始地址从0X0Cxxx开始的, 所以这个地址空间一定以
.ld的定义规则为准!!!
但是设计的框架差不多, 这边以上述的地址作为介绍, 内存布局框架:


假设我们在做一个 STM32 温湿度监控器,用 DHT11 传感器采集数据,OLED 屏显示,并通过串口上报。下面以这个项目的代码来对应每个内存区域
中断向量表
这是 Flash 最开头的一段固定表格,存放初始栈指针和各中断服务函数的入口地址。MCU 上电后,硬件自动从这里读取 SP 和 Reset_Handler 地址开始执行
"告诉 CPU:某个中断来了,要跳到哪个函数执行。"
相关的函数保存在startup.s文件中
c
EL1_core_exceptions_table:
b EL1_Reset_Handler /* Reset Handler */
...
EL2_Reset_Handler:
mov r0, #0
...
下面是用 readelf -l MCU_UART_IRQ.elf 工具解析出来的
shell
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .EL2_core_ex[...] PROGBITS 0cab0000 020000 000020 00 AX 0 0 16
[ 2] .EL2_Reset_H[...] PROGBITS 0cab0020 020020 000300 00 AX 0 0 4
[ 3] .EL1_core_ex[...] PROGBITS 0cab0320 020320 000040 00 AX 0 0 16
[ 4] .EL1_core_ex[...] PROGBITS 0cab0360 020360 000040 00 AX 0 0 4
.text(代码段)
所有编译后的机器指令都存放在这里,只读且可执行。你写的每一个函数最终都变成这个段里的一串二进制指令
c
// 这些函数编译后全部进入 .text
void DHT11_Read(float *temp, float *humi) {
// 时序操作,读取传感器数据
GPIO_SetBits(GPIOA, GPIO_Pin_5);
delay_us(20);
// ...
}
void OLED_ShowTemp(float temp) {
char buf[16];
sprintf(buf, "Temp: %.1f C", temp);
OLED_DrawString(0, 0, buf);
}
int main(void) {
SystemInit();
DHT11_Init();
OLED_Init();
USART_Init();
while (1) {
DHT11_Read(&temperature, &humidity);
OLED_ShowTemp(temperature);
USART_Send(temperature, humidity);
delay_ms(2000);
}
}
保存形式为:
c
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
....
[ 5] .text PROGBITS 0cab0400 020400 000e80 00 AX 0 0 16
[ 6] .text.IpcTp_ChanRxCallback PROGBITS 0cab1280 021280 000002 00 AX 0 0 2
[ 7] .text.CrySample_MdmaChanRxCallback PROGBITS 0cab1282 021282 000002 00 AX 0 0 2
[ 8] .text.IpcDriTest_ChanRxCallback PROGBITS 0cab1284 021284 000002 00 AX 0 0 2
[ 9] .text.IpcTest_SampleMdmaChanRxCallback PROGBITS 0cab1286 021286 000002 00 AX 0 0 2
[10] .text.DefaultCallback PROGBITS 0cab1288 021288 000002 00 AX 0 0 2
MCU 执行时直接从 Flash 中取指令运行(XIP,Execute In Place),不需要搬到 RAM
.rodata(只读数据段)
存放 const 修饰的常量和字符串字面量,程序运行中不可修改。
c
// 这些全部进入 .rodata
const char DEVICE_ID[] = "SENSOR_NODE_01";
const uint8_t OLED_FONT_8x16[] = { 0x00, 0x7E, 0x42, ... }; // 字库
const float TEMP_OFFSET = -0.5f; // 温度校准偏移
// 字符串字面量也在 .rodata
printf("System started\r\n"); // "System started\r\n" 存在 .rodata
字库数据往往很大,但因为放在 Flash 中,不占用宝贵的 SRAM。
.data(已初始化数据段)
存放赋了非零初值的全局变量和静态变量。它们的初始值存在 Flash 中,启动时由 startup 代码复制到 SRAM,之后程序读写的是 SRAM 中的副本。
c
// 这些变量进入 .data(有非零初始值)
uint32_t sample_interval = 2000; // 采样间隔 2000ms
uint8_t display_mode = 1; // 1=温度, 2=湿度, 3=双显
float alarm_threshold = 38.0f; // 报警温度阈值
static uint8_t retry_count = 3; // 通信重试次数
这些变量在 Flash 和 SRAM 各占一份空间------Flash 存初始值(LMA),SRAM 存运行时副本(VMA)。所以过多的已初始化全局变量会同时消耗两种存储。
.bss(未初始化数据段)
存放未赋初值或显式初始化为零的全局/静态变量。启动代码会将这段内存统一清零。它只占 SRAM,不占 Flash(链接器只记录起始地址和长度)。
c
// 这些变量进入 .bss(无初值或初值为0)
float temperature; // 当前温度,启动后清零
float humidity; // 当前湿度
uint8_t uart_rx_buf[128]; // 串口接收缓冲区
uint32_t error_count; // 错误计数器,默认0
static uint8_t dht11_raw[5]; // 传感器原始数据
所以 int buf[1024*1024] = {0}; 和 int buf[1024*1024] 效果是一样的, 编译器没有必要把一堆0放在flash里面占用空间
堆(heap)
用于运行时动态内存分配,向高地址增长。裸机用 malloc,RTOS 下通常用 pvPortMalloc 等封装。
c
// 堆上分配
char *log_buf = (char *)malloc(256); // 动态分配日志缓冲区
sprintf(log_buf, "[%lu] T=%.1f H=%.1f", tick, temperature, humidity);
USART_SendString(log_buf);
free(log_buf);
// FreeRTOS 中,任务控制块和任务栈也从堆上分配
xTaskCreate(SensorTask, "Sensor", 128, NULL, 2, NULL);
// → pvPortMalloc(sizeof(TCB_t) + 128*4) 从堆中切出内存
MCU 上堆空间有限,malloc 后必须 free,否则很快耗尽。很多嵌入式项目干脆禁用动态分配,全部用静态数组(反正都是在sram上的空间, 无非就是.bss段大一些, 无所谓~)
栈(Stack)
从高地址向低地址增长,用于存放函数调用帧(局部变量、参数、返回地址)以及中断/上下文切换时保存的寄存器。
c
void SensorTask(void *param) {
float readings[10]; // ← 在栈上分配 40 字节
uint8_t i; // ← 栈上 1 字节
for (i = 0; i < 10; i++) {
DHT11_Read(&readings[i], &humidity); // 调用函数 → 栈上压入返回地址
delay_ms(100);
}
float avg = CalcAverage(readings, 10); // 又一层调用帧
// ...
}
// 如果中断在此时触发:
void TIM2_IRQHandler(void) {
// 硬件自动压栈 xPSR, PC, LR, R12, R3-R0(32字节)
// 中断里的局部变量也在栈上
uint32_t status = TIM2->SR;
TIM2->SR = 0;
}
每一层函数调用和每次中断都会消耗栈空间。如果嵌套太深(比如递归)或者局部数组太大,栈就会向下越界,撞上堆区甚至覆盖 .bss 数据------这就是栈溢出,也是嵌入式最常见的崩溃原因之一
Q&A
程序运行时在哪? 读常量还要返回到flash里面?
绝大多数 MCU:代码直接在 Flash 上执行(XIP, Execute In Place)
| 数据类型 | 存储位置 | 访问时从哪读 |
|---|---|---|
| 函数代码 | Flash | Flash |
| const 数据 | Flash | Flash |
.data |
RAM | RAM |
.bss |
RAM | RAM |
| 栈变量 | RAM | RAM |
| malloc | RAM | RAM |
那为什么不把.bss都放在flash上
.bss绝对不能放在 Flash 上,因为它是"需要写"的数据,而 Flash 基本不能像 RAM 一样随便写
| 特性 | SRAM | Flash |
|---|---|---|
| 读 | ✅ 快 | ✅ 慢 |
| 写 | ✅ 随便写 | ❌ 受限 |
| 擦除 | ❌ 不需要 | ✅ 必须先擦 |
| 擦除粒度 | - | 扇区级(比如 4KB) |
| 写入速度 | 快 | 很慢 |
| 写次数寿命 | 无限(相对) | 有限制(如 10万次) |
bss段的数据会放在flash中?
.bss段的数据本身不会存放在 Flash 中。
Flash 里只保存".bss 的大小信息",而不保存具体数据
比如全局变量初始化没赋值, 默认就是0, 没有必要在flash中存一堆0
c
int g; // 未初始化 → 默认0
static int x; // 未初始化 → 默认0
对比 .data
c
int g = 5;
就必须在flash中放 5, 然后启动再加载到 sram里面, 否则启动之后程序都不知道它的初始值