MCU 进程内存布局详解(.text, .rodata, .data, .bss, 堆, 栈)

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里面, 否则启动之后程序都不知道它的初始值

相关推荐
记帖5 小时前
STM32C542开发(1)----点亮LED
嵌入式硬件·stm32cubemx·stm32cubeide·stm32cubemx2·stm32c542cct6
m0_377108145 小时前
stm32平衡车mpu6050
stm32·单片机·嵌入式硬件
资深流水灯工程师6 小时前
STM32 SAI 通讯原理与 TDM 应用
stm32·单片机·嵌入式硬件
Deitymoon6 小时前
FreeRTOS——任务信息查询API
stm32·单片机·嵌入式硬件
踏着七彩祥云的小丑7 小时前
嵌入式测试学习第 24 天:串口通信详细流程、收发数据原理
单片机·嵌入式硬件
周周记笔记7 小时前
【元器件专题】MOS管内部结构
嵌入式硬件
周周记笔记7 小时前
【元器件专题】MOS管的设计应用
单片机·嵌入式硬件
一路往蓝-Anbo7 小时前
第九章:OTA 与 Flash 驱动 —— 如何用TDD验证固件升级逻辑的鲁棒性
stm32·单片机·嵌入式硬件·软件工程·tdd·ota·嵌入式测试驱动开发
zlinear数据采集卡8 小时前
电源纹波无处遁形!工业采集卡电源去耦与滤波电路深度解析
c语言·嵌入式硬件·fpga开发·自动化·硬件架构
一路往蓝-Anbo8 小时前
第十章:TDD部署 —— Ceedling 环境的深度集成
stm32·单片机·嵌入式硬件·单元测试·测试驱动开发·tdd