摘要

在学习 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];
}

这里的 localtemp 通常位于栈上。

栈在 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
或者
0x08008000

3. 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 运行时地址:0x20000000

2. 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_astack,或者被优化到寄存器
local_b = 2.data
local_c.bss
local_dstack、寄存器,或者被优化成立即数

需要特别注意:

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 的重要基础。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐