一、什么是Map文件
简单来说,Map文件是编译器编译工程后生成的一个文件,这个文件反映了各个源文件生成的模块间的交叉引用、移除的未使用模块、符合映射表、内存映射以及各个模块的大小和汇总数据等。
所以说,当你在遇到或怀疑存在内存越界或溢出的情况时,首先想到的应该就是分析Map文件,确认嫌疑分子、构建RAM的分布图,还原问题发生的过程,才能从根本上解决问题。
那,我们就一起来看下怎么看Map文件这个问题吧~
二、如何生成Map文件
根据设置的不同,生成的Map文件包含的内容也不同。如图1所示,在“Options for Target ‘XXX’”窗口的Listing页面,通过勾选不同的项目可以定制Map文件中的记录的内容。
图1
PS:点击快捷工具栏的魔法棒按钮 或 菜单Project->“Options for Target ‘XXX’...”可以打开“Options for Target ‘XXX’”窗口。
设置完,代码编译成功后,在指定目录就可以找到生成的Map文件。
三、Map文件解析
Map文件已经生成了,那么接下来我们一起看下Map是何方神圣。既然要看,就需要先打开Map文件,那么Map文件怎么打开呢?
很简单,打开Map文件的方式有多种,在KEIL的左侧的Project窗口中目标工程上双击即可打开;或者,直接找到Map文件,用文本编辑器等方式都可以打开查看这里就不再赘述。
按照最全的配置,Map文件包括以下几个部分:
1. Section Cross References
主要是指各源文件生成的模块间的相互引用关系。
比如,下面这句表示:
spi.c文件编译生成的模块spi.o中调用了stm32f4xx_rcc.c文件编译生成的模块stm32f4xx_rcc.o z中的函数RCC_AHB1PeriphClockCmd。 剩下的也差不多都是这个意思。
Section Cross References
……
spi.o(.text) refers to stm32f4xx_rcc.o(.text) for RCC_AHB1PeriphClockCmd
……
2. Removing Unused input sections from the image
将未使用的函数之类的删除,以减少image映像的大小。
Removing Unused input sections from the image.
……
Removing data_quk.o(.rev16_text), (4 bytes).
……
这个从我个人目前接触的内容看,没用到过,如果XDJM在调试程序的过程中有用到这些信息的场景也希望不吝赐教,我也开阔下视野,多谢~
3. Image Symbol Table
映像中涉及的符号表,包括局部符号(Local Symbols)和全局符号(Global Symbols)。
Image Symbol Table
// 局部符号
Local Symbols
// 符号名 // 地址 // 类型 // 大小
Symbol Name Value Ov Type Size Object(Section)
../clib/angel/boardlib.s 0x00000000 Number 0 boardinit1.o ABSOLUTE
..\TASKS\alm_task.c 0x00000000 Number 0 alm_task.o ABSOLUTE
......
HEAP 0x20006248 Section 512 startup_stm32f40_41xxx.o(HEAP)
Heap_Mem 0x20006248 Data 512 startup_stm32f40_41xxx.o(HEAP)
STACK 0x20006448 Section 2048 startup_stm32f40_41xxx.o(STACK)
Stack_Mem 0x20006448 Data 2048 startup_stm32f40_41xxx.o(STACK)
__initial_sp 0x20006c48 Data 0 startup_stm32f40_41xxx.o(STACK)
// 全局符号
Global Symbols
// 符号名 // 地址 // 类型 // 大小
Symbol Name Value Ov Type Size Object(Section)
......
limit_check 0x0800d27d Thumb Code 566 alm_task.o(.text)
aaaaa_err 0x20000089 Data 1 global.o(.data)
play_cnt 0x2000008a Data 1 global.o(.data)
lock_cnt 0x2000008b Data 1 global.o(.data)
......
Region$$Table$$Base 0x0801bf24 Number 0 anon$$obj.o(Region$$Table)
Region$$Table$$Limit 0x0801bf44 Number 0 anon$$obj.o(Region$$Table)
......
注意,这里的符号包括函数名,变量名。局部的static变量和全局变量在这里都可以找到,如果疑似存在内存越界的变量属于这两种类型,那么可以从这里找到他们的地址,看看他上下左右的小伙伴儿都是谁,就能确定嫌疑分子了。
另外,类型包括Number、Section、Thumb Code、Data。其中,Number是指它并不占据程序空间,而只是具有一定数值的符号,类似于程序中用宏定义define和EQU。
4. Memory Map of the image
映像的内存分布,顾名思义,这部分内容主要记录了映像的加载域和运行域的起始地址、大小和最大Size以及各个段的起始地址。
在说介绍之前,我们先了解下这几个段的意义,方便理解:
段名 | 说明 |
.constdata | 只读常量数据段,属于RO-data。 |
.text | 代码段。 用来存放程序执行代码的内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(当然也有些架构允许代码段为可写,即允许修改程序)。也有可能包含一些只读的常数变量,例如字符串常量等。 |
.data | 数据段。 data 段用于存储已经赋初值(非零)的全局变量,且变量占有实际的内存空间。本段的内容由程序初始化,因此会占用exe文件空间。 |
.bss | 数据段,Block Started by Symbol。 bss段用于存储未赋初值的全局变量和静态局部变量,这些变量在程序运行前会被初始化为0或NULL。 另外,初始化为零的全局变量和静态局部变量也会存储在bss中的数据不分配实际的空间,只体现为一个占位符,只记录数据所需空间的大小,因此不会占用exe文件空间。 bss段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在data段后面。 |
heap | 堆。 用于存放运行中被动态分配的内存段,可动态扩张或缩减。 例如,malloc分配的内存就在堆上。 |
stack | 栈。 用于存放程序临时创建的局部变量,即:函数括弧“{}”中定义的临时变量。注意,不包括用static声明的变量,static声明的变量存储在data段中。当函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。 |
下面是是从Map中摘抄的Memory Map of the image的部分内容:
Memory Map of the image
Image Entry point : 0x08000189
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x0001c6b4, Max: 0x00080000, ABSOLUTE, COMPRESSED[0x0001c3ac])
Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x0001c080, Max: 0x00080000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x08000000 0x08000000 0x00000188 Data RO 458 RESET startup_stm32f40_41xxx.o
0x08000188 0x08000188 0x00000008 Code RO 2119 * !!!main c_w.l(__main.o)
......
0x080002a0 0x080002a0 0x00000478 Code RO 3 .text main.o
0x08000718 0x08000718 0x00000016 Code RO 300 .text stm32f4xx_it.o
0x0800072e 0x0800072e 0x00000002 PAD
0x08000730 0x08000730 0x00000210 Code RO 349 .text system_stm32f4xx.o
......
0x0801c064 0x0801c064 0x0000001c Data RO 2311 locale$$data c_w.l(lc_numeric_c.o)
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x0801c080, Size: 0x00006c48, Max: 0x00020000, ABSOLUTE, COMPRESSED[0x0000032c])
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x20000000 COMPRESSED 0x00000014 Data RW 350 .data system_stm32f4xx.o
0x20000014 COMPRESSED 0x000000c5 Data RW 377 .data global.o
0x200000d9 COMPRESSED 0x00000001 PAD
0x200000da COMPRESSED 0x00000200 Data RW 440 .data cr.o
......
0x20000634 - 0x000008c4 Zero RW 4 .bss main.o
0x20000ef8 - 0x000001a0 Zero RW 376 .bss global.o
0x20001098 - 0x000004e0 Zero RW 558 .bss uart.o
0x20001578 - 0x0000005c Zero RW 603 .bss dsply.o
0x200015d4 - 0x00000009 Zero RW 737 .bss spai.o
0x200015dd COMPRESSED 0x00000001 PAD
0x200015de - 0x00000014 Zero RW 863 .bss data.o
......
0x20005b04 - 0x000006e0 Zero RW 1970 .bss os_var.o
0x200061e4 - 0x00000060 Zero RW 2243 .bss c_w.l(libspace.o)
0x20006244 COMPRESSED 0x00000004 PAD
0x20006248 - 0x00000200 Zero RW 457 HEAP startup_stm32f40_41xxx.o
0x20006448 - 0x00000800 Zero RW 456 STACK startup_stm32f40_41xxx.o
从上面Map的内容可以得知,RW_IRAM1起始地址为 0x20000000, 大小为 0x00006c48,最大为0x00020000。起始地址和大小可以在Keil中配置:
从上面的Map内容,还可以得知data段、bss段、heap、stack的起始地址,这里很重要,后面我们会据此进行还原内存分布。
5. Image component sizes
映像组件大小的信息,这部分包含了*.o 文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,并按照类别Code、 RO-data、 RW-data、ZI-data 、Debug分别统计其占用的大小,最后给出总的统计信息。
名词 | 解释 |
Code (inc. Data) | 代码,显示代码和内联数据占用了多少字节。 例如: const int a = 10; // 存储在代码段 |
RO / RO-data | 只读数据,Read Only,显示只读数据占用了多少字节。 注意:不包括Code列中的已包含的内联数据哦。 例如: const char *msg = "Hello world!"; // 存储在RO-data |
RW / RW-data | 可读写的数据,Read Write,显示读写数据占用了多少字节。 Rw-data由程序初始化初始值。 例如: int c = 10; // 存储在data段,RW-data |
Zl / ZI-data | 初始化为0的数据,Zero Initialize。 没有初始化的可读写变量(即:程序中用到的且没有显式初始化的变量),编译器默认会是把没有初始化的变量都赋值一个0,显然它是存在RAM中的。 例如: int d; // 存储在bss段,ZI-data |
Debug | 调试数据,显示调试数据占用了多少字节。 例如,调试输入节以及符号和字符串。 |
Object Totals | 显示链接后生成的映像对象占用了多少字节。 |
(incl. Generated) | 链接器生成的映像内容。例如,交互操作的中间代码。 如果 Object Totals 行包含此类型的数据,则会显示在该行中。 本例中共有 1440字节的 RO 数据,其中32字节是链接器生成的 RO 数据。 |
(incl. Padding) | 链接器根据需要插入填充字节,以实现字节对齐。 本例中97160的Code中,共有8个填充字节。 |
Grand Totals | 显示真实映像统计信息。 |
ELF Image Totals (compressed) | ELF(Executable and Linking Format)可执行链接格式映像文件大小。 |
ROM Totals | 显示包含映像所需的 ROM的最小Size。 注意:这里不包括 ZI数据和存储在ROM 中的调试信息。 |
本例中,Image统计信息如下图所示。在此映像中,有112784字节的代码, 其中包括8648字节的内联数据 (inc. data例如文字池和短字符串);2032字节的RO data;1588字节的RW Data;27308字节的ZI Data。
最终的统计信息如下:
RO Size = Code + RO Data = 112784 + 2032 = 114816 字节
RW Size = RW Data + ZI Data = 1588 + 27308 = 28896 字节
ROM Size = Code + RO Data + RW Data = 112784 + 2032 + 812 = 115628 字节
注意:
(1)ZI Data在编译时只是一个占位符,故不占最终生成文件的大小。
(2)RW-data既存储在RAM中,也存储在ROM中,已初始化的数据会存储在ROM中,上电会从ROM 搬移至RAM中。
四、内存分布
根据前面解析的Map文件内容,我们可以尝试还原运行域的内存分布,即: data段、bss段、heap、stack的分布图,这在分析程序是否存在内存越界方面很有用。
根据RW_IRAM1的运行域的地址,可以反推出Memery的分布图,具体如下:
__initial_sp是栈的栈顶指针,在Map文件的Image Symbol Table的Local Symbols中有体现。
而堆和栈的大小,在Map文件的Memory Map of the image有描述,而具体的大小是由代码配置的,具体参考下图,这里栈的大小配置为0x800,堆的大小位0x200。
通过上面的内存分布图可以看出,栈顶指针指向栈地址最大的地方,所以这个栈是从高地址向低地址方向生长的,如果栈空间定义小了,出现下溢,则会影响到堆的数据。
五、栈究竟溢出了吗
通过上面的操作,我们还原出了运行域的内存分布图,那么,从这个图怎么看我这个栈究竟溢出了吗,或者出栈溢出后可能的影响呢?
别急,Keil同样给我们提供了参考数据。在生成Map的路径,同样有生成的文件可供我们参考,呶,就是下面这俩货:
文件 “xxxx .bulid_log.htm” 是工程的构建日志,而文件“xxxx.html”是链接器生成的静态调用图。在静态调用图文件中,记录了工程中各个函数之间互相调用的关系,并且还给出了静态占用最深的栈空间数量以及它对应的调用关系链。对,这就是我们需要的关键信息!
好,那我们打开“xxxx.html”这个文件看看:
找到关键信息了吗?
对,就是这个栈空间的最大使用大小!
注意,这个是静态栈的使用统计,如果你有递归,那么还需要你自行估算。当然,在嵌入式中,递归是能避免还是尽量避免的。我的工程中没用到递归,故,这里只需要参考栈的静态最大使用空间就可以了。
对于这个工程,栈静态的最大使用空间为1300个字节,而前面定义的栈空间大小为0x800(2048)个字节。1300 < 2048,妥妥滴够了,不用担心溢出了。
一般情况下,在资源充足的情况下(即:空间有余量的情况下),一般栈的大小设置为这个静态栈最大使用量的两倍,我这边也差不多够用了,就先保持。如果你的结果是这两个值接近,解决方案有两种:要么扩大栈的大小,要么缩小栈的最大使用大小。要是资源不够,那只能根据本文件反映的函数调用关系,尽可能优化代码,减少调用深度,以期最终缩小栈的最大使用大小。
我这里不改动,还有一个比较投机取巧的点,就是从上面的内存分布图可以看出,在栈上面还有个0x200堆,但实际上并没有用到堆,故,即便是栈溢出了,还有512个字节的缓冲区,发生溢出并影响到上面的bss段数据的概率很低,所以就不用担心了~
好了,结合Map分析内存分布和是否溢出的问题差不多就这些,如果有不对或更好的方法,也麻烦告诉我哦,谢谢~
六、参考资料
感谢XDJM的慷慨分享,谢谢:
两种存储器,三种内存大小,六段段 (baidu.com)
《学习笔记_问ChatGPT:单片机编译器map文件》 - 哔哩哔哩 (bilibili.com)
STM32开发之map文件学习_stm32map文件_wanwanshenyou的博客-CSDN博客
转载记得说明出处哦~