理解嵌入式开发中程序的内存布局是至关重要的。对于使用Keil MDK或ARM编译器的开发者而言,清晰掌握RO、RW、ZI、.data、.bss、堆(heap)、栈(stack)等核心概念,不仅能帮助我们理解程序如何被加载和运行,更是进行有效内存优化的基础。本文将系统梳理这些关键概念及其在Flash与SRAM中的映射关系。
1. 基础概念详解
1.1 存储介质分类
嵌入式系统中的存储介质主要分为两类:
Flash(非易失性存储)
- 特点:掉电后数据不丢失,读取速度快,但写入(编程)速度相对较慢。
- 存储内容:程序代码、常量数据、以及初始化变量的初值。
- 访问方式:可直接读取,但写入需通过特定接口或协议。
RAM(易失性存储,常指SRAM)
- 特点:掉电后数据丢失,读写速度都非常快。
- 存储内容:程序运行时的变量、堆栈空间以及其他临时数据。
- 访问方式:支持直接读写。
1.2 程序段分类
根据数据的读写属性和初始化方式,程序在编译链接后被划分为不同的段。
RO(Read Only,只读)段
- 存储位置:Flash。
- 包含内容:
- 程序代码(
.text段)
- 只读数据(
.rodata段),如常量字符串、用const修饰的全局变量。
- 特点:在程序运行时不可修改。
RW(Read Write,读写)段
- 存储位置:其初始值存储在Flash中,运行时值存储在RAM中。
- 包含内容:已初始化且初始值非零的全局变量和静态变量。
- 特点:系统启动时,需要一段初始化代码将这部分数据的初始值从Flash复制到对应的RAM地址。
ZI(Zero Initialized,零初始化)段
- 存储位置:RAM。
- 包含内容:未初始化,或显式初始化为0的全局变量和静态变量。
- 特点:系统启动时,由启动代码将对应的RAM区域清零。
1.3 常见段名与内存区域对应关系
.text:代码段,存放程序指令,位于Flash中。
.rodata / .constdata:只读数据段(属于RO),存放常量,位于Flash中。
.data:已初始化的全局/静态变量段(属于RW)。其初始值在Flash中,运行时变量本体在RAM中。
.bss:未初始化或零初始化的全局/静态变量段(属于ZI),位于RAM中。通常,编译器也会将堆(heap)和栈(stack)的管理区域划归到.bss段或类似区域。
- 栈 (Stack):用于存储局部变量、函数参数、返回地址等,由编译器自动管理,通常从RAM的高地址向低地址增长。
- 堆 (Heap):用于动态内存分配(如
malloc/free),由程序员管理,通常从RAM的低地址向高地址增长。
1.4 加载域与执行域
这两个概念对于理解程序从存储介质到运行时的转换至关重要。
-
加载域 (Load Region):指程序烧录(下载)到芯片时,代码和数据存储在哪个物理地址区间。可以是片内Flash、片外Flash或RAM。
- 对于代码和只读数据(RO),因其运行时无需改变,通常直接存储在Flash中,其加载域即Flash地址空间。
- 对于读写数据(RW),若其有非零初始值,则该初始值必须保存在非易失存储介质(如Flash)中,因此其加载域也是Flash地址空间。
- 对于零初始化数据(ZI),无需在Flash中保存具体值,因此没有实质的加载域内容。
-
执行域 (Execution Region):指芯片上电运行后,CPU从哪个地址读取指令或访问数据。
- 对于代码:其执行域通常与加载域一致,即CPU直接从Flash中取指执行。
- 对于数据:
- RO数据:从Flash中直接读取,执行域同加载域。
- RW数据:程序运行时访问的是RAM中的副本,因此其执行域是RAM地址空间。
- ZI数据:同样在RAM中被访问,执行域是RAM地址空间。
简单来说,加载域解决“数据存哪儿”,执行域解决“运行时去哪儿找”。系统启动过程的核心任务之一,就是根据分散加载描述文件,将数据从“存”的地方搬到“用”的地方。
2. 内存区域详细对应关系
2.1 编译时段的映射


2.2 详细对应表
| 内存区域 |
对应段 |
存储介质 |
初始化方式 |
内容示例 |
.text |
RO (代码) |
Flash |
编译时确定 |
函数代码、中断向量表 |
.rodata |
RO (数据) |
Flash |
编译时确定 |
const常量、字符串常量 |
.data |
RW |
Flash(初值)+RAM |
启动时从Flash复制初值到RAM |
int a = 100; |
.bss |
ZI |
RAM |
启动时清零 |
int b; 或 int c = 0; |
| heap |
ZI (动态) |
RAM |
运行时分配 (malloc等) |
malloc()分配的内存块 |
| stack |
ZI (动态) |
RAM |
运行时压栈 |
局部变量、函数参数、返回地址 |
3. 启动过程分析
系统上电复位后,在跳转到main()函数之前,启动文件(如startup_xxx.s)会执行一系列关键的初始化操作,这与操作系统底层启动原理有相通之处。主要步骤包括:
- 初始化栈指针(SP)和程序计数器(PC)。
- 将RW段的数据从其在Flash中的存储地址(加载域)复制到RAM中的运行时地址(执行域)。
- 将ZI段对应的整个RAM区域清零。
- 调用
__main(C库初始化,可能包含分散加载代码),最后跳转到用户的main()函数。
4. Map文件解析
Map文件是Keil MDK编译链接后生成的重要报告,它详细展示了程序的内存布局。分析Map文件是进行内存优化的关键步骤。
4.1 Map文件内容分析
1. 模块摘要 (Module Summary)
这部分列出了每个目标文件(.o)所占用的各类内存大小。
模块摘要示例:
Code (inc. data) RO Data RW Data ZI Data Debug Object Name
1200 200 400 100 500 8000 main.o
800 150 200 50 300 6000 library.o
Code:包含内联数据在内的代码大小。
RO Data:只读数据大小。
RW Data:已初始化的读写数据大小(RAM占用)。
ZI Data:零初始化数据大小。
Debug:调试信息大小,不烧录到芯片。
2. 总内存占用 (Grand Totals)
汇总整个工程的内存需求。
总内存占用示例:
Total RO Size (Code + RO Data) 1600 ( 1.56kB)
Total RW Size (RW Data + ZI Data) 900 ( 0.88kB)
Total ROM Size (Code + RO Data + RW Data) 1700 ( 1.66kB)
Total RO Size:占用Flash的只读部分总和(Code + RO Data)。
Total RW Size:运行时需要的RAM总和(RW Data + ZI Data),不含堆栈。
Total ROM Size:实际需要烧录到Flash的最小空间(Code + RO Data + RW Data的初始值)。
3. 内存区域分布 (Memory Map of the image)
最详细的部分,展示了每个段在加载域和执行域中的具体地址和大小。
内存映射表示例:
Load Region LR_FLASH (Base: 0x08000000, Size: 0x00000800, Max: 0x00080000)
Execution Region ER_FLASH (Base: 0x08000000, Size: 0x00000650)
Base Addr Size Type Attr Idx E Section Name Object
0x08000000 0x00000200 Code RO 1 .text startup_stm32f10x.o
0x08000200 0x00000400 Data RO 2 .constdata main.o
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000400)
Base Addr Size Type Attr Idx E Section Name Object
0x20000000 0x00000100 Data RW 10 .data main.o
0x20000100 0x00000200 Zero RW 11 .bss main.o
0x20000300 0x00000100 Zero RW 12 heap .o




4.2 关键指标解读
在编译输出窗口常见的信息行:
Program Size: Code=xxxx RO-data=xxxx RW-data=xxxx ZI-data=xxxx
- Code: 代码大小,存储在Flash中。
- RO-data: 只读数据大小,存储在Flash中。
- RW-data: 已初始化且非零的全局/静态变量大小。在Flash中存储其初始值,在RAM中占用同等大小的空间用于运行时。
- ZI-data: 未初始化或零初始化的全局/静态变量大小。仅占用RAM空间,且不占用Flash空间存储数据(可能占用极小空间存储元信息)。
重要计算公式:
Flash占用 ≈ Code + RO Data + RW Data (的初始值)
RAM占用 ≈ RW Data + ZI Data + Stack Size + Heap Size
注意:RW数据在Flash和RAM中各占一份空间,Flash存初值,RAM存运行值。
5. 内存优化实战
5.1 优化方向与方法
基于以上理解,我们可以有针对性地优化嵌入式程序的存储空间:
- 减少全局/静态变量:尤其是非零初始化的(RW)和未初始化的(ZI)变量,能直接节省RAM。优先使用局部变量(在栈上分配)。
- 常量使用
const:确保常量数据被放置在Flash的RO段,而非RAM。这是编程基础中提升效率的常见手段。
const int table[100] = { ... }; // 存储在Flash,节省RAM
- 优化代码尺寸:选择适当的编译优化等级(如
-Os优化尺寸),移除无用代码,减少Flash占用。
- 合理配置堆栈:根据函数调用深度和局部变量大小合理设置栈大小;根据动态内存需求合理设置堆大小,避免溢出或浪费。
- 分析Map文件:定位占用大的模块或变量,进行针对性优化。
5.2 优化实例分析
int global_var = 100; // RW数据:Flash存100,RAM占4字节。
int global_var2; // ZI数据:RAM占4字节,启动时清零。
const int global_const = 200; // RO数据:仅Flash占4字节,不占RAM。

堆栈溢出检测:在资源受限的系统中,监控堆栈使用是良好实践。可以在启动时用特定值(幻数)填充栈空间,并在运行时检查。
// 示例:栈使用检测函数
uint16_t CheckStackUsage(void) {
extern uint32_t __StackTop; // 栈顶(起始地址,低地址)
extern uint32_t __StackLimit; // 栈底限制(高地址)
uint32_t *p = &__StackLimit;
uint32_t magic = 0xDEADBEEF;
uint16_t used = 0;
// 从栈底向栈顶查找第一个未被修改(仍为幻数)的位置
while (p < &__StackTop && *p == magic) {
p++;
}
// 计算已使用的栈空间(字节)
used = ((uint32_t)p - (uint32_t)&__StackLimit) * sizeof(uint32_t);
return used;
}
5.3 优化检查清单
- [ ] 变量检查:全局/静态变量是否必要?能否改为局部变量?
- [ ] 常量修饰:所有常量数据是否都已用
const修饰?
- [ ] 大内存对象:大数组是否可改为动态分配或放在特定内存段(如CCM RAM)?
- [ ] 堆栈配置:堆栈大小是否经过评估和测试?是否有溢出检测机制?
- [ ] 编译选项:是否使用了合适的优化等级(如
-Os平衡尺寸与速度)?
- [ ] Map文件分析:是否定期查看Map文件,找出并优化内存消耗“大户”?
通过系统地应用这些概念和方法,开发者可以更有效地管理Keil MDK项目中的宝贵内存资源,提升系统稳定性和性能。