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

相关推荐
wuyk5559 小时前
Keil工具链-第05讲-启动文件与__main到底做了什么
c语言·stm32·单片机·嵌入式硬件·物联网
shijuejianceliu15 小时前
传统规则视觉与嵌入式AI智能相机的技术差异
人工智能·嵌入式硬件·视觉检测·相机
不脱发的程序猿19 小时前
GD32串口数据收进内存之后DMA如何处理?
单片机·嵌入式硬件·dma·嵌入式·串口通信·gd32
FPGA小迷弟1 天前
FPGA 与千兆以太网校验和:从原理到 Verilog 实现
嵌入式硬件·fpga开发·无人机·ai编程·fpga
国科安芯2 天前
83 倍:ASC0108S 在推挽和开漏之间,差的不是速度,是回程
单片机·嵌入式硬件·架构·电平转换·抗辐射
国科安芯2 天前
从遥控到自治:ASP4644S2B支撑星载电源健康管理与容错配电
网络·单片机·嵌入式硬件·安全·dcdc·低轨卫星·商业航天
rhythm-ring2 天前
《汽车热管理系统:电子水泵单线PWM双向诊断机制》
单片机·嵌入式硬件·汽车
Dev_XH2 天前
【成品设计】基于物联网的车间安全生产监控系统设计
stm32·单片机·嵌入式硬件·物联网
xiebs_2 天前
【无标题】111
单片机
元器件BOM超人2 天前
GD32E230C8T6 兆易创新Cortex-M23超值型MCU解析:规格参数、替代选型与常见问题
单片机·嵌入式硬件