| 编辑推荐: |
本文介绍了Keil MDK(或ARM编译器)中关于程序内存布局的一些基本概念(RO、RW、ZI和.data、.bss、heap、stack、Flash、SRAM)等相关内容。
希望能为大家提供一些参考或帮助。
文章来自于微信公众号嵌入式电子学习,由火龙果Linda编辑推荐。 |
|
内存属性
理解Keil MDK(或ARM编译器)中关于程序内存布局的一些基本概念(RO、RW、ZI和.data、.bss、heap、stack、Flash、SRAM)。这些概念对于理解程序如何被加载和运行,以及如何优化内存使用至关重要。
1. 基础概念详解
1.1 存储介质分类
Flash(非易失性存储)
• 特点:掉电数据不丢失,读取速度快,写入速度慢
• 存储内容:程序代码、常量数据、初始化数据
• 访问方式:直接读取,需要通过特定接口编程
RAM(易失性存储)
• 特点:掉电数据丢失,读写速度快
• 存储内容:变量、堆栈、运行时数据
• 访问方式:直接读写
1.2 程序段分类
RO(Read Only)段
• 存储位置:Flash
• 包含内容:
• 程序代码(.text段)
• 只读数据(.rodata段)
• 常量字符串、const变量
• 特点:运行时不可修改
RW(Read Write)段
• 存储位置:Flash中存初始值,RAM中存运行时值
• 包含内容:已初始化且非零的全局/静态变量
• 特点:启动时需要从Flash复制到RAM
ZI(Zero Initialized)段
• 存储位置:RAM
• 包含内容:未初始化或显式初始化为0的全局/静态变量
• 特点:启动时清零初始化
1.3 常见段名与内存区域
• .text:表示代码段Code,存放在Flash中。
• .constdata 或 .rodata:只读数据段RO,存放在Flash中。
• .data:已初始化的全局变量和静态变量(RW数据),在Flash中保存初始值,在RAM中存放运行时值。
• .bss:未初始化的全局变量和静态变量(ZI数据),在RAM中,程序启动时初始化为0,堆和栈属于.bss。
• 栈(stack):用于局部变量、函数调用等,由编译器自动管理,通常从RAM的高地址向低地址增长。
• 堆(heap):用于动态内存分配,由程序员管理(malloc/free),通常从RAM的低地址向高地址增长。
1.4 加载域和执行域
1、加载区域表示代码和数据下载到芯片时存储到哪段地址,可以存储到片上Flash,也可以存储到片外Flash,也可以存储到RAM。
• 对于代码,为只读类型,运行时无法更改,因此存储在Flash中即加载区域是Flash地址段。
• 对于数据,其分成几类:
• 对于RO只读数据,比如const类型、字符串等等,其存储在Flash,因此加载区域也是Flash地址段;
• 对于RW读写数据,比如.data,如果其有初值,那么初值要存放在Flash中,运行时先从Flash中取出初值对RW数据进行赋值,然后运行时RW数据的访问地址是在RAM里,也就是RW数据的加载区域是Flash地址段,执行区域是RAM地址段;
• 对于ZI数据,比如.bss和stack、heap,表示初始化为零的全局变量,因此无需在Flash中存放初值,也就无所谓加载区域,只有执行区域,执行区域也就是程序运行时,如果要访问这个变量,要去哪个地址段寻找。
2、 执行区域表示上电运行后程序和数据从哪个地址开始执行或访问。
• 对于代码:也就是从哪个地址开始读取代码语句并执行,一般是程序存储在哪里,就从哪里执行,代码的执行区域和加载区域保持一致。
• 对于数据:表示程序运行起来后,去哪个地址可以访问数据。
• 对于RO数据,例如const,需要存储在Flash中,因此其加载区域地址就处在Flash中,程序运行起来后也是去Flash地址段访问const变量,因此其执行区域也是Flash地址段,两个区域保持一致
• 对于RW数据,如果初值不为零,那么初值需要存储到Flash中(即使初值为零,加载区域似乎也是Flash段),则其加载区域是Flash地址段,运行时访问RW数据则要去RAM里,因此执行区域是RAM地址段。
2. 内存区域详细对应关系
2.1 编译时段的映射
2.2 详细对应表
| 内存区域 |
对应段 |
存储介质 |
初始化方式 |
内容示例 |
| .text |
RO |
Flash |
编译时确定 |
函数代码、中断向量表 |
| .rodata |
RO |
Flash |
编译时确定 |
const常量、字符串常量 |
| .data |
RW |
Flash+RAM |
启动时从Flash复制 |
int a = 100; |
| .bss |
ZI |
RAM |
启动时清零 |
int b; 或 int c = 0; |
| heap |
ZI(动态) |
RAM |
运行时分配 |
malloc()分配的内存 |
| stack |
ZI(动态) |
RAM |
运行时压栈 |
局部变量、函数参数 |
3. 启动过程分析
系统上电后,首先从Flash中读取代码和数据进行初始化。具体步骤:
1. 初始化栈指针(SP)和程序计数器(PC)。
2. 将RW数据从Flash中复制到RAM中(这部分数据在Flash中紧跟在RO数据之后)。
3. 将ZI数据所在的RAM区域全部清零。
4. 跳转到main函数执行。
4. Map文件解析
Map文件展示了程序的内存布局,包括各个段的大小、地址分配等。通过Map文件,我们可以查看:
• 代码段、RO数据段、RW数据段、ZI数据段的大小和位置。
• 各个模块(源文件)占用的代码和数据空间。
4.1 Map文件分析
1. 模块摘要 Module Summary: 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
|
可以看到用户每个源文件所占据内存大小,inc表示内联函数和数据。
2. 总内存占用 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)
|
汇总看到整个工程所占据的Flash和SRAM空间。
3. 内存区域分布 Memory Map of the image: Flash区域 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 RAM区域 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
|
可以看到Flash和SRAM中具体的每一段地址存放了哪些数据和代码。
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中占用空间(除了初始化为0的说明信息,但不占用实际数据空间)
重要计算公式
Flash占用 = Code + RO Data + RW Data的初始值
RAM占用 = RW Data + ZI Data + Stack + Heap
|
注意:RW数据在Flash和RAM中各有一份,Flash中存储的是初始值,RAM中是运行时的值。
5. 内存优化
5.1 常见优化方法
通过理解这些概念,我们可以有针对性地优化程序:
• 减少全局变量的使用,特别是已初始化的全局变量(RW数据)和未初始化的全局变量(ZI数据),可以节省RAM空间。
• 将常量数据尽量使用const关键字定义为只读数据,使其存储在Flash中,而不是RAM中。
• 优化代码大小,减少Flash占用。
• 合理设置堆栈大小,避免溢出。
5.2 优化建议说明
• 定义一个全局变量,带有非零初始值,属于RW数据,在Flash中存储初始值100,在RAM中有一个变量占4字节。
• 定义一个全局变量,零初始值,属于ZI数据,在RAM中占4字节,启动时被初始化为0。
const int global_const = 200;
|
• 定义一个const数据,属于RO数据,存储在Flash中,不占用RAM。
• 堆和栈的大小通常由启动文件(startup.s)中的设置决定,在Map文件中可以查看它们的地址范围。
查看MAP文件
• 查看各个模块的代码和数据占用,找出占用较大的模块,进行优化。
• 检查RW和ZI数据的大小,优化全局变量和静态变量的使用。
• 确认堆栈大小是否足够,避免堆栈溢出。
优化方向
• 如果Flash紧张,可以优化代码和常量数据,例如使用更高效的算法,减少常量数据(如字符串、数组)等。
• 如果RAM紧张,可以减少全局变量和静态变量,使用局部变量(栈上分配),减少动态内存分配(堆)等。
• 注意:栈和堆的增长方向以及边界检查很重要,如果堆和栈发生重叠,会导致程序崩溃。因此,需要合理设置堆栈大小,并可能使用内存保护功能。在代码中监控堆栈使用。
/*********************************************************************************************************************** * Function Name: StackFillMagic * Description : 初始化阶段调用一次将栈区全部填充幻数 * Arguments : None * Return Value : None ***********************************************************************************************************************/ void StackFillMagic(void) { uint32_t* base = &__base_sp; //栈顶边界 uint32_t* top = (uint32_t*)__get_MSP(); //这里要使用当前栈指针 while(base < top) { * base++ = 0xDEADBEEF; //填充幻数 } }
|
/*********************************************************************************************************************** * Function Name: CheckStackOverflow * Description : 程序运行过程中一直调用此函数检测栈空间使用是否溢出 * Arguments : None * Return Value : None ***********************************************************************************************************************/ uint16_t CheckStackOverflow(void) { uint16_t use_size = 0; uint32_t* base = &__base_sp; //栈顶边界 while(*base == 0xDEADBEEF && base < (&__initial_sp)) { base++; //检查哪些地方的数据不是幻数,表示此区域已经使用了 } use_size = (base - (&__base_sp))*sizeof(uint32_t); //字节个数 return (use_size); }
|
5.3 优化检查项
• 检查全局变量是否必要,能否改为局部变量
• 常量数据使用const修饰,确保存储在Flash
• 大数组考虑使用动态分配或放在特定内存区域
• 定期检查堆栈使用情况,避免溢出
• 使用合适的编译优化选项(-Os, -O2等)
• 分析Map文件,找出内存占用大的模块
|