摘要
在学习 STM32、RTOS、Bootloader、OTA 升级时,经常会看到.text、.data、.bss、heap、stack、linker script、map 文件等概念。很多初学者容易把“固件在 Flash 中的存储结构”和“程序运行时在 RAM 中的内存布局”混在一起。
本文从嵌入式 MCU 的角度出发,系统梳理固件镜像中的典型段划分,并解释它们在 Flash 和 SRAM 中的关系,帮助理解启动过程、内存占用、Bootloader 跳转以及 RTOS 任务栈等问题。
一、先看整体结构
以 STM32 这类 Cortex-M MCU 为例,芯片内部通常有两类主要存储空间:
Flash:非易失存储,掉电后数据不丢失,用于存放程序代码和常量 SRAM :易失性内存,掉电后数据丢失,用于程序运行时读写一个典型固件烧录到 Flash 后,大致结构如下:
Flash 0x08000000 ┌────────────────────┐ │ 中断向量表 .isr_vector │ ├────────────────────┤ │ 代码段 .text │ ├────────────────────┤ │ 只读数据 .rodata │ ├────────────────────┤ │ .data 段初始值 │ └────────────────────┘程序运行时,SRAM 中大致是:
SRAM 0x20000000 ┌────────────────────┐ │ .data 运行区 │ ├────────────────────┤ │ .bss │ ├────────────────────┤ │ heap 堆 │ │ ↑ │ │ ↓ │ │ stack 栈 │ └────────────────────┘核心结论是:
Flash 中存放的是固件镜像,SRAM 中存放的是程序运行时需要读写的数据。
二、.text:代码段
.text段用于存放程序编译后的机器指令,也就是函数代码。
例如:
void led_on(void) { GPIOA->BSRR = GPIO_PIN_5; }这个函数经过编译后会变成机器指令,并被放入.text段。
通常情况下:
.text → Flash因为代码一般不会在运行过程中被修改,所以放在 Flash 中即可,不需要占用宝贵的 SRAM。
三、.rodata:只读数据段
.rodata用于存放只读数据,比如字符串常量、全局 const 常量等。
例如:
const int table[3] = {1, 2, 3}; printf("hello world\n");其中:
table "hello world\n"通常会被放入.rodata段。
.rodata一般也位于 Flash 中:
.rodata → Flash因为这些数据是只读的,不需要放到 RAM 中。
四、.data:已初始化的全局变量和静态变量
.data段存放的是“已经初始化,并且运行时可能被修改”的全局变量或静态变量。
例如:
int g_count = 10; static int flag = 1;这些变量有初始值,而且程序运行时可以修改它们。
这里要注意,.data有两个位置:
初始值存放在 Flash 运行时变量存放在 SRAM也就是说:
Flash 中保存 g_count 的初始值 10 SRAM 中保存 g_count 运行时的当前值上电启动时,启动代码会把.data的初始值从 Flash 拷贝到 SRAM。
伪代码如下:
uint32_t *src = &_sidata; // Flash 中 .data 初始值地址 uint32_t *dst = &_sdata; // SRAM 中 .data 运行地址 while (dst < &_edata) { *dst++ = *src++; }所以:
int g_count = 10;它的初始值10烧录在 Flash 中;上电后,启动代码把这个值复制到 SRAM;程序运行时修改的是 SRAM 中的g_count。
五、.bss:未初始化或零初始化的全局变量和静态变量
.bss段用于存放未初始化或初始化为 0 的全局变量、静态变量。
例如:
int g_value; static int buffer[1024]; int count = 0;这些变量的共同特点是:初始值为 0。
.bss和.data的最大区别是:
.bss 不需要在固件镜像中保存实际内容因为.bss里面全是 0,如果在 Flash 中存一大堆 0,会浪费固件空间。
例如:
uint8_t big_buffer[1024 * 10];这个数组如果是全局未初始化变量,那么它会占用 10KB SRAM,但通常不会让 bin 文件增加 10KB。
启动时,启动代码会把.bss对应的 RAM 区域清零。
伪代码如下:
uint32_t *dst = &_sbss; while (dst < &_ebss) { *dst++ = 0; }因此:
.bss → 运行时占用 SRAM .bss → 通常不占用 Flash 中的实际固件内容六、.data和.bss的区别
下面用表格总结:
| 变量写法 | 所属段 | Flash 中是否保存初始值 | SRAM 中是否占空间 |
|---|---|---|---|
int a = 10; | .data | 是 | 是 |
int b; | .bss | 否 | 是 |
int c = 0; | .bss | 否 | 是 |
const int d = 5; | .rodata | 是 | 通常否 |
static int e = 3; | .data | 是 | 是 |
static int f; | .bss | 否 | 是 |
例如:
int a = 10; // .data int b; // .bss int c = 0; // .bss const int d = 5; // .rodata static int e = 3; // .data static int f; // .bss char *p = "hello"; // p 在 .data,"hello" 在 .rodata最后一行很经典:
char *p = "hello";这里其实有两个东西:
p 这个指针变量:可修改,通常放在 .data "hello" 字符串内容:只读,通常放在 .rodata七、stack:栈
栈用于保存函数调用过程中的临时数据,例如:
局部变量 函数返回地址 函数调用现场 中断现场 临时寄存器保存例如:
void func(void) { int local = 10; uint8_t temp[100]; }这里的local和temp通常位于栈上。
栈在 SRAM 中:
stack → SRAM在 Cortex-M MCU 中,复位后 CPU 会从中断向量表的第一个位置读取初始栈顶地址。
中断向量表开头一般类似:
0x08000000: 初始 MSP 栈顶地址 0x08000004: Reset_Handler 地址也就是说,上电后 CPU 会:
1. 从 0x08000000 读取初始栈顶地址 2. 从 0x08000004 读取 Reset_Handler 地址 3. 跳转到 Reset_Handler 开始执行八、heap:堆
堆用于动态内存分配,例如:
malloc() free() new delete比如:
uint8_t *buf = malloc(128);这块 128 字节内存通常来自 heap。
堆也位于 SRAM:
heap → SRAM不过在嵌入式系统中,很多项目会尽量避免频繁使用malloc/free,原因包括:
容易产生内存碎片 分配失败不好处理 实时性不可控 问题定位困难因此,在实时性要求较高的 RTOS 项目中,通常更推荐使用静态分配、内存池或固定大小缓冲区。
九、RTOS 中的栈和堆
在裸机程序中,系统通常只有一个主栈。
但是在 RTOS 中,每个任务都有自己的任务栈。
例如 FreeRTOS 中:
xTaskCreate(TaskA, "TaskA", 256, NULL, 2, NULL);这里的256是任务栈大小。需要注意,在 FreeRTOS 中,这个单位通常是 word,不是 byte。
如果 MCU 是 32 位架构:
256 words = 256 × 4 = 1024 bytes在 FreeRTOS 中,任务控制块 TCB、任务栈、队列、信号量、互斥锁等对象,可能来自 FreeRTOS heap。
例如:
SemaphoreHandle_t sem = xSemaphoreCreateBinary();这个信号量对象本身通常会占用 FreeRTOS heap。
RTOS 运行时,SRAM 结构可以理解为:
SRAM ┌────────────────────┐ │ .data │ ├────────────────────┤ │ .bss │ ├────────────────────┤ │ FreeRTOS heap │ │ ├─ TCB │ │ ├─ task stack │ │ ├─ queue │ │ ├─ semaphore │ │ └─ mutex │ ├────────────────────┤ │ 中断栈 / MSP stack │ └────────────────────┘在 Cortex-M 上,RTOS 运行后,普通任务通常使用 PSP,异常和中断通常使用 MSP。
十、.isr_vector:中断向量表
.isr_vector是中断向量表,通常放在 Flash 的最开始位置。
以 STM32 为例,默认用户程序从:
0x08000000开始执行。
中断向量表中存放的是一组地址:
第 0 项:初始栈顶地址 第 1 项:Reset_Handler 地址 第 2 项:NMI_Handler 地址 第 3 项:HardFault_Handler 地址 后面是各种外设中断服务函数地址例如:
Flash 0x08000000 初始栈顶地址 0x08000004 Reset_Handler 0x08000008 NMI_Handler 0x0800000C HardFault_Handler ...如果做 Bootloader,就经常会遇到中断向量表重定位的问题。
假设 App 放在:
0x08008000那么 App 的中断向量表也应该位于:
0x08008000跳转 App 前一般需要设置:
SCB->VTOR = APP_ADDR;否则中断发生时,CPU 可能仍然去 Bootloader 的向量表中查找中断入口,导致程序异常。
十一、.noinit:不初始化段
有些变量希望在软复位之后不要被清零,可以放到.noinit段。
例如:
__attribute__((section(".noinit"))) uint32_t boot_magic;.noinit的特点是:
位于 SRAM 启动时不清零 启动时不从 Flash 拷贝它常用于:
Bootloader 和 App 之间传递标志 保存软复位前的信息 保存崩溃现场 低功耗唤醒后保留数据例如 App 想让 Bootloader 进入升级模式,可以这样做:
boot_magic = 0xA5A55A5A; NVIC_SystemReset();复位后 Bootloader 检查boot_magic,如果启动代码没有清零.noinit,这个值仍然可以保留下来。
十二、ELF、BIN、HEX、MAP 文件的区别
嵌入式编译后常见文件有:
.elf .bin .hex .map它们的作用不同。
1. ELF 文件
ELF 文件信息最完整,通常包含:
代码 数据 符号表 调试信息 段信息 入口地址 函数地址 变量地址调试器通常依赖 ELF 文件进行源码级调试。
例如 GDB 需要知道:
main 函数在哪里 某个变量在哪里 某一行 C 代码对应哪条汇编指令这些信息都在 ELF 文件中。
2. BIN 文件
BIN 文件是裸二进制镜像,本身不带地址信息。
它可以理解为:
从某个 Flash 起始地址开始,连续排列的机器码和数据因此烧录 BIN 文件时,必须告诉烧录工具它应该烧写到哪个地址:
0x08000000 或者 0x080080003. HEX 文件
HEX 通常是 Intel HEX 格式,是文本文件,内部带有地址信息。
它可以表示:
某段数据烧到 0x08000000 另一段数据烧到 0x08010000所以 HEX 比 BIN 更适合表达非连续地址区域。
4. MAP 文件
MAP 文件是链接器生成的内存分布报告,非常重要。
它可以告诉我们:
.text 占了多少 Flash .data 占了多少 Flash 和 RAM .bss 占了多少 RAM 某个函数位于哪个地址 某个全局变量位于哪个地址 哪个模块占用空间最大做嵌入式内存优化时,.map文件非常有价值。
十三、链接脚本中的段划分
在 GNU ld 链接脚本中,通常会先定义 Flash 和 RAM:
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K }这表示:
Flash 起始地址:0x08000000,大小 512KB RAM 起始地址:0x20000000,大小 128KB然后描述各个段放到哪里:
.text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) } > FLASH意思是:
中断向量表、代码段、只读数据段都放到 Flash.data段比较特殊:
_sidata = LOADADDR(.data); .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT> FLASH其中:
> RAM AT> FLASH意思是:
.data 的运行地址在 RAM .data 的加载地址在 Flash这就引出了两个概念:VMA 和 LMA。
十四、VMA 和 LMA
1. VMA:运行地址
VMA 是程序运行时使用的地址。
例如:
g_count 运行时地址:0x200000002. LMA:加载地址
LMA 是固件镜像中保存初始值的地址。
例如:
g_count 的初始值 10 保存在 Flash 的 0x08005000对于.data段来说:
LMA:Flash 中的初始值位置 VMA:SRAM 中的运行位置对于.bss段来说,通常只有运行地址 VMA,因为它没有实际初始数据需要放在 Flash 中。
十五、MCU 启动过程
一个典型 Cortex-M MCU 上电后的启动流程如下:
1. CPU 从中断向量表读取初始栈顶 MSP 2. CPU 跳转到 Reset_Handler 3. Reset_Handler 拷贝 .data:Flash → SRAM 4. Reset_Handler 清零 .bss 5. 初始化系统时钟 SystemInit() 6. 初始化 C/C++ 运行环境 7. 跳转 main() 8. 如果使用 RTOS,在 main 中创建任务并启动调度器可以理解为:
Flash 中的固件镜像 ↓ 启动代码搬运和初始化 ↓ SRAM 中形成运行时内存布局 ↓ main() 开始执行十六、一个变量到底放在哪里?
看下面这段代码:
int g_a = 10; int g_b; static int g_c = 20; static int g_d; const int g_e = 30; void func(void) { int local_a = 1; static int local_b = 2; static int local_c; const int local_d = 3; }大致分类如下:
| 变量 | 位置 |
|---|---|
g_a = 10 | .data |
g_b | .bss |
g_c = 20 | .data |
g_d | .bss |
g_e = 30 | .rodata |
local_a | stack,或者被优化到寄存器 |
local_b = 2 | .data |
local_c | .bss |
local_d | stack、寄存器,或者被优化成立即数 |
需要特别注意:
static int local_b = 2;虽然它写在函数里面,但它不是普通局部变量。它的生命周期是整个程序运行期间,所以不会随着函数退出而销毁,通常放在.data段。
十七、和 Bootloader / OTA 的关系
如果系统中有 Bootloader,Flash 通常会被分区:
Flash 0x08000000 ┌────────────────────┐ │ Bootloader │ 0x08008000 ├────────────────────┤ │ App 固件 │ 0x08040000 ├────────────────────┤ │ OTA 下载区 │ 0x08078000 ├────────────────────┤ │ 参数区 / 标志区 │ └────────────────────┘每个 App 固件内部仍然有自己的:
.isr_vector .text .rodata .data 初始值如果 App 放在:
0x08008000那么 App 的链接脚本中 Flash 起始地址也应该设置为:
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = ...否则函数地址、中断向量表地址、跳转地址都可能出错。
Bootloader 跳转 App 时,通常需要做几件事:
1. 检查 App 地址是否合法 2. 关闭中断或外设 3. 设置 MSP 为 App 向量表第 0 项 4. 设置 VTOR 为 App 起始地址 5. 跳转到 App Reset_Handler其中 App 向量表结构为:
APP_ADDR + 0x00:App 初始栈顶 APP_ADDR + 0x04:App Reset_Handler十八、常见误区总结
误区一:.data只在 RAM 中
不完全正确。
.data的运行位置在 RAM,但它的初始值必须保存在 Flash 中。否则掉电后,系统不知道变量应该初始化成什么值。
例如:
int a = 10;这里的10必须保存在 Flash 中,上电后再复制到 RAM。
误区二:.bss会增加 bin 文件大小
一般不会。
例如:
uint8_t buffer[100 * 1024];如果它是全局未初始化数组,那么它会占用 100KB RAM,但通常不会让 bin 文件增加 100KB。
但是如果写成:
uint8_t buffer[100 * 1024] = {1};那么它可能进入.data段,固件体积会明显变大。
误区三:const一定在 Flash 中
在 STM32 这类 Flash 可直接寻址的 Cortex-M MCU 上,全局const通常放在 Flash 的.rodata中。
但是具体情况还和编译器、链接脚本、优化等级、变量是否被取地址等因素有关。
误区四:局部变量一定在栈上
通常是,但不绝对。
例如:
void func(void) { int a = 10; }a可能在栈上,也可能被编译器优化到寄存器里,甚至直接被优化掉。
十九、各段总结表
| 段 | 存放内容 | 是否占固件空间 | 运行时位置 |
|---|---|---|---|
.isr_vector | 中断向量表 | 是 | Flash |
.text | 程序代码 | 是 | Flash |
.rodata | 只读常量、字符串 | 是 | Flash |
.data | 已初始化的全局变量、静态变量 | 是,保存初始值 | SRAM |
.bss | 未初始化或零初始化的全局变量、静态变量 | 通常不占实际内容 | SRAM |
| heap | 动态分配内存 | 否 | SRAM |
| stack | 局部变量、函数调用现场、中断现场 | 否 | SRAM |
.noinit | 复位后不清零的数据 | 否 | SRAM |
二十、最终总结
嵌入式固件存储结构可以用下面几句话记住:
代码和只读常量放 Flash 可修改的全局变量和静态变量运行时放 RAM .data:Flash 中保存初始值,启动时复制到 RAM .bss:Flash 中不保存实际内容,启动时在 RAM 中清零 stack/heap:运行时使用 SRAM RTOS 中每个任务通常都有自己的任务栈理解这些内容之后,再看下面这些问题就会清晰很多:
为什么全局大数组会导致 RAM 不够? 为什么 .bss 很大但 bin 文件不大? 为什么 .data 会同时占用 Flash 和 RAM? 为什么 Bootloader 跳转 App 要设置 MSP 和 VTOR? 为什么 FreeRTOS 中任务栈设置太小会导致 HardFault? 为什么 map 文件对内存优化很重要?本质上,固件不是简单地“从上到下存放一堆代码”,而是由链接器根据链接脚本划分成多个段。启动代码再根据这些段的信息,把 Flash 中的固件镜像初始化成 RAM 中的运行时内存布局。
把.text、.rodata、.data、.bss、heap、stack 的关系理解透,是学习 STM32、RTOS、Bootloader 和 OTA 的重要基础。