摘要在学习 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 在 .datahello 在 .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。堆也位于 SRAMheap → 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 和 RAMMEMORY { 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 和 LMA1. VMA运行地址VMA 是程序运行时使用的地址。例如g_count 运行时地址0x200000002. LMA加载地址LMA 是固件镜像中保存初始值的地址。例如g_count 的初始值 10 保存在 Flash 的 0x08005000对于.data段来说LMAFlash 中的初始值位置 VMASRAM 中的运行位置对于.bss段来说通常只有运行地址 VMA因为它没有实际初始数据需要放在 Flash 中。十五、MCU 启动过程一个典型 Cortex-M MCU 上电后的启动流程如下1. CPU 从中断向量表读取初始栈顶 MSP 2. CPU 跳转到 Reset_Handler 3. Reset_Handler 拷贝 .dataFlash → 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.datag_b.bssg_c 20.datag_d.bssg_e 30.rodatalocal_astack或者被优化到寄存器local_b 2.datalocal_c.bsslocal_dstack、寄存器或者被优化成立即数需要特别注意static int local_b 2;虽然它写在函数里面但它不是普通局部变量。它的生命周期是整个程序运行期间所以不会随着函数退出而销毁通常放在.data段。十七、和 Bootloader / OTA 的关系如果系统中有 BootloaderFlash 通常会被分区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 0x00App 初始栈顶 APP_ADDR 0x04App 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未初始化或零初始化的全局变量、静态变量通常不占实际内容SRAMheap动态分配内存否SRAMstack局部变量、函数调用现场、中断现场否SRAM.noinit复位后不清零的数据否SRAM二十、最终总结嵌入式固件存储结构可以用下面几句话记住代码和只读常量放 Flash 可修改的全局变量和静态变量运行时放 RAM .dataFlash 中保存初始值启动时复制到 RAM .bssFlash 中不保存实际内容启动时在 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 的重要基础。
嵌入式存储结构详解: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 在 .datahello 在 .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。堆也位于 SRAMheap → 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 和 RAMMEMORY { 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 和 LMA1. VMA运行地址VMA 是程序运行时使用的地址。例如g_count 运行时地址0x200000002. LMA加载地址LMA 是固件镜像中保存初始值的地址。例如g_count 的初始值 10 保存在 Flash 的 0x08005000对于.data段来说LMAFlash 中的初始值位置 VMASRAM 中的运行位置对于.bss段来说通常只有运行地址 VMA因为它没有实际初始数据需要放在 Flash 中。十五、MCU 启动过程一个典型 Cortex-M MCU 上电后的启动流程如下1. CPU 从中断向量表读取初始栈顶 MSP 2. CPU 跳转到 Reset_Handler 3. Reset_Handler 拷贝 .dataFlash → 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.datag_b.bssg_c 20.datag_d.bssg_e 30.rodatalocal_astack或者被优化到寄存器local_b 2.datalocal_c.bsslocal_dstack、寄存器或者被优化成立即数需要特别注意static int local_b 2;虽然它写在函数里面但它不是普通局部变量。它的生命周期是整个程序运行期间所以不会随着函数退出而销毁通常放在.data段。十七、和 Bootloader / OTA 的关系如果系统中有 BootloaderFlash 通常会被分区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 0x00App 初始栈顶 APP_ADDR 0x04App 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未初始化或零初始化的全局变量、静态变量通常不占实际内容SRAMheap动态分配内存否SRAMstack局部变量、函数调用现场、中断现场否SRAM.noinit复位后不清零的数据否SRAM二十、最终总结嵌入式固件存储结构可以用下面几句话记住代码和只读常量放 Flash 可修改的全局变量和静态变量运行时放 RAM .dataFlash 中保存初始值启动时复制到 RAM .bssFlash 中不保存实际内容启动时在 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 的重要基础。