大家都知道,由于受成本限制,嵌入式设备的RAM、Flash等硬件资源有限,比如有些低成本的开发板只有内置的64KB ROM、20KB RAM。在编写代码、逐渐丰富功能时,编译出的镜像会越来越大,RAM、ROM占用越来越多,最终可能超出开发板的资源限制。对于硬件资源相对宽裕的开发板,规范化编码、规划合理的镜像大小,也会提升性能。本文带给大家LiteOS Studio的镜像分析工具,它可是个评估、优化镜像文件RAM、ROM占用大小的利器,帮你合理规划镜像大小。
开发环境准备
前期的系列文章,我们掌握了如何安装LiteOS Studio,
如何新建STM32F769IDISCOVERY和Qemu realview-pbx-a9工程。这次我们以STM32F429IGTx开发板为例,创建工程的方式是一样的,开发板选择STM32F429IGTx即可。镜像分析特性对任何LiteOS工程都是适用的,我们只是以该开发板为例。工程创建完毕后,执行编译,输出如下:
终端控制台输出显示编译成功了,可执行文件Huawei_LiteOS.elf对应的text段为74774 bytes,data段大小1444 bytes,bss段13080 bytes,dec表示前面三段大小的合计,为89268bytes。这些text、data、bss数据代表什么?有什么意义?我们继续,后文中会详细解释。
LiteOS 镜像分析特性
点击LiteOS Studio工具栏的调测工具Debug Tools按钮,打开调试工具,选择镜像分析,这就是本文要给大家介绍的LiteOS Studio的镜像分析工具。填写可执行文件路径、Map文件路径等,如图:
点击确定按钮,会自动打开镜像分析窗口。包含内存区域、详细信息、文件大小、模块大小等4个选项卡。我们依次演示如何使用:
内存区域
内存区域页面评估分析LiteOS开发板工程对内存的细分使用情况。对于STM32F429IGTx开发板,显示的内存区域region包含FLASH、RAM、CCMRAM,展示的信息包含每个内存区域的名称、起始内存地址、总大小、空闲大小、已用大小,使用比例。在这个内存区域页面,除了数值展示分析,还提供饼图可以宏观的评估每个区域的使用、剩余情况。如下图所示,FLASH总大小 1024Kb,RAM总大小192Kb,对FLASH、RAM的使用率较低,刚刚超7%。对于CCMRAM更是没有使用,CCM(Core Coupled Memory)是给STM32F4内核专用的全速64KB RAM。
详细信息
继续点击详细信息选项卡打开镜像分析详细信息页面,该页面展示每个内存区域包含的内存段section,内存段包含的符号symbol的详细信息。比如FLASH下面包含.isr_vector、.text、.rodata等内存段, 内存段又包含分配在该段的程序符号。每一行展示的信息包含运行地址VMA(Virtual Memory Address)、装载地址LMA(Load MemoryAddress)、内存段/符号的大小。其中,LMA表示程序装载的内存地址;VMA表示程序运行时的内存地址。嵌入式系统中RAM内存空间有限,一般把程序放在FLASH中,LMA地址为Flash中的地址,等到程序运行时,再载入到RAM中的运行地址VMA。内存段.data、.vector_ram就属于这种情况,VMA、LMA地址不一样,并且在FLASH、RAM均出现。在使用详情页面,支持排序和搜索过滤,支持程序符号快速跳转到源代码行。我们可以很直观的评估每个内存段、程序符号的使用大小情况,可以快速定位到潜在可优化的资源消耗大户。如图:
从镜像分析图表中,可以看出text、data段存放在Flash存储,data、bss段数据存放在RAM存储。那么,链接器linker是怎么知道如何把各个段的符号数据存放在ROM、RAM的?这就要靠链接脚本。
链接脚本和Text、Data、BSS Section段
链接脚本包含一些规则来约束链接器如何把函数、变量放到ROM或RAM,STM32F429IGTx工程的链接脚本位置在targets\Cloud_STM32F429IGTx_FIRE\liteos.ld。我们分栏同时展示镜像分析页面和链接脚本,如下图,可以看出镜像分析页面展示的内存段使用情况和链接脚本中所定义的是一致的,开发板的内存使用情况在LiteOS Studio 镜像分析页面得到非常直观的展示。内存段.ccmram在链接脚本中没有定义,已使用大小的也为0 byte。对于内存段.data、.vector_ram,链接脚本中使用关键字 RAM AT> FLASH,来表示装载地址、实际运行地址分别在FLASH、RAM。
链接脚本片段1如下,定义了中断处理向量.isr_vector、程序.text存放在Flash存储,使用的关键字为>FLASH,其中FLASH在上文的MEMORY{......}中定义。
/* The startup code goes first into FLASH */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } >FLASH /* The program code and other data goes into FLASH */ .text : { . = ALIGN(4); __text_start = .; *(.text) /* .text sections (code) */ *(.text*) /* .text* sections (code) */ *(.glue_7) /* glue arm to thumb code */ *(.glue_7t) /* glue thumb to arm code */ *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; /* define a global symbols at end of code */ __text_end = _etext; } >FLASH
链接脚本片段2如下,定义了.vector_ram、.data存放在Flash存储,在程序启动时,会从Flash复制到RAM。使用的关键字为 >RAM AT> FLASH,其中RAM在上文的MEMORY{}中定义。
/* Initialized liteos vector sections goes into RAM, load LMA copy after code */ .vector_ram : { . = ORIGIN(RAM); _s_liteos_vector = .; *(.data.vector) /* liteos vector in ram */ _e_liteos_vector = .; } > RAM AT> FLASH /* used by the startup to initialize data */ _sidata = LOADADDR(.data); /* Initialized data sections goes into RAM, load LMA copy after code */ .data ALIGN(0x1000): { __ram_data_start = _sdata; . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ KEEP(*( SORT (.liteos.table.*))); . = ALIGN(4); _edata = .; /* define a global symbol at data end */ __ram_data_end = _edata; } >RAM AT> FLASH
链接脚本片段3如下,定义了.bss、._user_heap_stack占用RAM存储。使用的关键字为 >RAM。
.bss : { /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; __bss_start = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; __bss_end = _ebss; } >RAM /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM
现在来总结一下,通常是这样的:
Text段
Text存储在只读的ROM内存区域,包含向量表、程序代码、只读常量数据。
Data段
表示初始化过的变量。存储在FLASH、RAM。链接器分配data段数据在Flash区域,在startup启动时,从ROM中复制到RAM。ROM中保存的是只读的副本,开发板重启也不会改变;RAM中保存的是读写副本,在程序运行过程中,变量值可能会变化。
BSS段
表示未初始化的变量。RAM中未初始化的数据,在程序启动时初始化为0。
其他内存段
.user_heap_stack内存堆,malloc()申请内存使用此区域;.ARM.attributes、.debug_frame等存储调试信息。
Map映射文件、ASM反汇编文件
在程序编译链接时需要指定链接脚本,编译成功后会生成map映射文件,保存程序、数据的内存映射信息。映射文件是比较重要的辅助分析文件,可以获取程序中的函数、变量如何分配到RAM、ROM。在使用镜像分析功能的时候,我们指定了Map映射文件out\Cloud_STM32F429IGTx_FIRE\Huawei_LiteOS.map。反汇编文件是另外一个比较重要的文件,如果一个函数占用了太多的text代码段、data数据段时,此时可以分析反汇编代码。
利用镜像分析特性结合反汇编文件,我们很方便评估函数是否可以进一步优化改进。以main函数为例,在映射文件中的片段如下, main函数的存放地址为0x08000ea0,函数占用空间大小0x38 bytes(=56 bytes)。
.text.startup.main 0x08000ea0 0x38 d:/LiteOS_master/out/Cloud_STM32F429IGTx_FIRE/lib\libCloud_STM32F429IGTx_FIRE.a(main.o) 0x08000ea0 main
这个数据在LiteOS Studio镜像分析页面也可以快速查阅到:
下面是main()函数反汇编片段。可以看出,我们是C代码和反汇编混合展示的。第一列8000ea0是地址,第二列是汇编指令的机器码、然后是汇编代码。
函数开始地址为0x8000ea0,结束地址为0x8000ed4,函数占用空间大小=0x8000ed4-0x8000ea0+0x4=0x38 bytes。如果函数长度过长,结合分析反汇编代码行,进行定位优化。
08000ea0 :main():d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:45INT32 main(VOID){ 8000ea0: b598 push {r3, r4, r7, lr} 8000ea2: af00 add r7, sp, #0d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:46 HardwareInit(); 8000ea4: f7ff ffea bl 8000e7c d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:48 PRINT_RELEASE("\n********Hello Huawei LiteOS********\n" 8000ea8: 4b07 ldr r3, [pc, #28] ; (8000ec8 ) 8000eaa: 4a08 ldr r2, [pc, #32] ; (8000ecc ) 8000eac: 4908 ldr r1, [pc, #32] ; (8000ed0 ) 8000eae: 4809 ldr r0, [pc, #36] ; (8000ed4 ) 8000eb0: f000 fb84 bl 80015bc d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:54 "\nLiteOS Kernel Version : %s\n" "build data : %s %s\n\n" "**********************************\n", HW_LITEOS_KERNEL_VERSION_STRING, __DATE__, __TIME__); UINT32 ret = OsMain(); 8000eb4: f003 fd18 bl 80048e8 d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:55 if (ret != LOS_OK) { 8000eb8: b108 cbz r0, 8000ebe d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:56 return LOS_NOK; 8000eba: 2001 movs r0, #1d:\LiteOS_master\targets\Cloud_STM32F429IGTx_FIRE/Src/main.c:62 } OsStart(); return 0;} 8000ebc: bd98 pop {r3, r4, r7, pc} 8000ebe: 4604 mov r4, r0
动手试验时间
我们动手编码创建些不同类型的变量、函数,看看这些会分配到哪些内存段,实际分配是否符合我们已掌握的知识。增加下述代码片段到targets\Cloud_STM32F429IGTx_FIRE\Src\user_task.c文件中的函数UINT32 app_init(VOID)的上方,在该UINT32 app_init(VOID)函数内首行增加对 ABC_FunName(0);的调用。
static UINT32 ABC_static_global_init = 1; UINT32 ABC_global_init = 1; UINT32 ABC_global_noInit; const UINT32 ABC_global_const = 1;static VOID ABC_Static_FunName(VOID){ printf("ABC_static_global_init is %d.\n", ABC_static_global_init); printf("ABC_global_init is %d.\n", ABC_global_init); printf("ABC_global_noInit is %d.\n", ABC_global_noInit); printf("ABC_global_const is %d.\n", ABC_global_const);} UINT32 ABC_FunName(UINT32 ABC_num){ CHAR *ABC_var_inFun = "1234567890"; UINT32 ABC_var_inFuc_init = 1; static UINT32 ABC_static_InFuc_init = 1; static UINT32 ABC_static_InFuc_noinit; const UINT32 ABC_inFuc_const = 1; ABC_static_InFuc_noinit = 99; printf("ABC_var_inFuc_init is %d.\n", ABC_var_inFuc_init); printf("ABC_static_InFuc_init is %d.\n", ABC_static_InFuc_init); printf("ABC_static_InFuc_noinit is %d.\n", ABC_static_InFuc_noinit); printf("ABC_inFuc_const is %d.\n", ABC_inFuc_const); CHAR *buf = LOS_MemAlloc(m_aucSysMem0, 8); buf[0] = ABC_var_inFun[0]; LOS_MemFree(m_aucSysMem0, buf); (VOID)ABC_Static_FunName(); return 0; }
重新编译,点击镜像分析页面的刷新按钮重新展示。我们新增的符号都以ABC_开头,我们以这个关键字搜索一下,可以看出来,ABC_FunName函数属于.text代码段,全局初始化的变量ABC_global_init属于.data段,全局未初始化变量ABC_global_noInit属于.bss段。静态函数ABC_Static_FunName、函数栈没有在这个区域展示。这符合我们已掌握的知识,如下图,大家也可以自行尝试一下。
本文介绍了嵌入式开发中的内存布局、链接脚本,映射文件,通过实例演示了如何利用LiteOS Studio的镜像分析特性,如何结合反汇编文件来评估函数空间占用。LiteOS Studio是我们LiteOS物联网开发的利器!欢迎大家去使用这个特性,并分享使用心得,有任何问题、建议,都可以留言给我们,谢谢!
https://gitee.com/LiteOS/LiteOS_Studio/issues