news 2026/7/2 22:54:42

嵌入式存储结构详解:text、data、bss、heap、stack 到底是什么?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式存储结构详解:text、data、bss、heap、stack 到底是什么?

摘要

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 8:27:21

如何保护您的 Reddit 账户:2026 年全方位安全运行Reddit账户

如果您在 Reddit 上花过时间——不管是为了追某个小众爱好&#xff0c;还是运营品牌——您的账户里都沉淀着不少东西&#xff1a;Karma 积分、社区信誉、私信记录&#xff0c;甚至整个子版块的管理权限。这些东西平时不显眼&#xff0c;可一旦丢了&#xff0c;代价比想象中大得…

作者头像 李华
网站建设 2026/7/1 3:58:34

从“合规焦虑”到“原生安全”:信创即时通讯架构重构

从“合规焦虑”到“原生安全”&#xff1a;信创即时通讯的底层逻辑重构 某央企因即时通讯信息泄露被通报&#xff0c;消息在政企信息化圈层引发震动。问题不在于聊天记录被窃取&#xff0c;而是审计人员发现&#xff0c;涉事系统无法提供完整的会话回溯链条——谁在什么时间、通…

作者头像 李华
网站建设 2026/7/1 3:53:03

基于YOLOv8的轻量化船舶检测:实现可见光与红外图像的高精度识别

最近在做一个海上监控的项目&#xff0c;客户提了个挺实在的要求&#xff1a;能不能用一个模型&#xff0c;白天看高清可见光&#xff0c;晚上看红外热成像&#xff0c;都能把船给认出来&#xff0c;而且最好能塞进他们船载的工控机里跑。这听起来像是要一个“全能选手”&#…

作者头像 李华
网站建设 2026/7/2 6:32:06

OCRmyPDF终极指南:如何让扫描PDF秒变可搜索的智能文档

OCRmyPDF终极指南&#xff1a;如何让扫描PDF秒变可搜索的智能文档 【免费下载链接】OCRmyPDF OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched 项目地址: https://gitcode.com/GitHub_Trending/oc/OCRmyPDF 你是否曾经面对堆积如山…

作者头像 李华
网站建设 2026/7/1 3:48:37

终极图片去重解决方案:AntiDupl.NET免费开源工具完全指南

终极图片去重解决方案&#xff1a;AntiDupl.NET免费开源工具完全指南 【免费下载链接】AntiDupl A program to search similar and defect pictures on the disk 项目地址: https://gitcode.com/gh_mirrors/an/AntiDupl 你是否厌倦了电脑中堆积如山的重复照片&#xff1…

作者头像 李华