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

相关推荐
Lugas Luo2 小时前
车载录像存储性能模拟测试工具设计
linux·嵌入式硬件·测试工具
v132665623682 小时前
BK7258 wifi6音视频soc芯片应用分析
嵌入式硬件·物联网·音视频·iot·wifi6
風清掦3 小时前
【江科大STM32学习笔记-10】I2C通信协议 - 10.2 硬件 I2C 读写MPU6050
笔记·stm32·单片机·嵌入式硬件·学习
ALINX技术博客3 小时前
【黑金云课堂】FPGA技术教程Vitis开发:RTC中断讲解
单片机·嵌入式硬件·fpga开发
进击的小头3 小时前
第10篇:嵌入式芯片中断系统详解:NVIC与硬实时性优化设计
单片机·嵌入式硬件
菠萝地亚狂想曲3 小时前
Zephyr_02,SEM
单片机
天狼IoT4 小时前
STM32-keil+CubeMX快速开发【外设配置篇】:NVIC与中断通用处理方法
stm32·单片机·嵌入式硬件
llilian_164 小时前
国产铷原子钟 铷频标挑选指南 铷原子频标 国产铷钟
大数据·网络·功能测试·单片机·嵌入式硬件·测试工具
NQBJT4 小时前
嵌入式从零开始(第十一篇):芯片选型 —— STM32 vs ESP32
stm32·单片机·嵌入式硬件