一、什么是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博客

      转载记得说明出处哦~

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐