目录
1、Section Cross References:程序段交叉引用
2、Removing Unused input sections from the image:删除映像未使用的程序段
4、Memory Map of the image:映像的内存映射
5、Image component sizes:映像组件大小
一、概述
开发过程中难免会遇到很多奇怪的bug,比如一个变量被赋值后,通过日志输出还是原来的数值,貌似没有改变,这很可能是被优化掉了。这个时候我们可以通过.map文件查看是否被优化掉,可以通过volatile来防止被优化。本章节将介绍如果生成.map文件,以及.map文件详解,最后会通过案例来实战.map的应用。
二、keil如何生成.map文件
确定后记得编译一下,这样就可以在Listings文件中找到.map文件了
三、详解
.MAP文件的组成分为五个部分:
1、Section Cross References:程序段交叉引用
2、Removing Unused input sections from the image:删除映像未使用的程序段
3、Image Symbol Table:映像符号表
3.1 Local Symbols 本地/局部符号
3.2 Global Symbols 全局符号
4、Memory Map of the image:映像的内存映射
5、Image component sizes:映像组件大小
1、Section Cross References:程序段交叉引用
这一块的内容就是精准记录函数与模块的调用关系,可以在这一部分看到函数之间的调用关系和链接关系。
可以看到其中的链接文件,main文件中的main函数调用debug文件中的Debug_Init函数。main文件中的main函数调用main文件中的demo_fun函数。后续追踪的结果如下图
2、Removing Unused input sections from the image:删除映像未使用的程序段
这一段的作用就是将未使用到的函数和全局变量从映像中删除掉,以此减小程序的体积,这在实战中可以极大的帮助优化存储空间。如下图可以看到我们在main定义中的static(静态)和const(只读)数据未使用,这里被优化掉了。static全局静态变量属于.data数据段,而const定义的数据属于.constdata数据段,也就是只读数据段。被优化掉的数据程序不占用最后bin文件的大小。
不单单变量会被优化掉,有时候不使用的函数也会被优化掉。
3、Image Symbol Table:映像符号表
映像符号表属于.map文件的核心模块之一,用于记录程序中所有符号(函数、变量、标号等)的存储地址、类型、大小及所属模块。通俗一点讲,它可以很清楚的记录全局变量、静态全局变量、静态函数、非静态函数的存储地址与大小等详细信息。其中映像符号表分为Local Symbols(本地/局部符号)和Global Symbols(全局符号)。
(1)Local Symbols(局部符号):记录用 static
声明的全局变量、函数,以及汇编文件中的标号地址(作用域限于本文件)。如下,rw_static_data3的变量名存放在0x20000003地址,属于Data段,大小100字节节,存放在main文件中。
Symbol Name Value Ov Type Size Object(Section)
rw_static_data3 0x20000003 Data 100 main.o(.data)
我们可以看到,当我们定义静态全局变量和静态函数时候,他们属于Local Symbols局部符号的内容,其函数的起始地址存放在Flash区,全局变量因为重定位的原因,存放在内存区。如下图所示,我们定义全局静态变量rw_static_data3和静态函数demo_static_fun,可以看到.map中的分布。demo_static_fun静态函数定义在Flash区,地址0x080009d5,占用16字节。
(2)Global Symbols(全局符号):记录全局变量、非静态函数等(作用域全工程)。我们可以看到,非静态型的函数和变量存放在全局符号区,如demo_global__fun函数的起始地址0x08000a75,占用58字节。
4、Memory Map of the image:映像的内存映射
该模块用于描述程序在存储器和运行时内存中的具体布局,是分析内存分配、优化资源使用和排查问题的关键工具,在这个段中,我们可以看到函数的分布地址与占用大小。能够通过文件辅助查看Flash和内存是否溢出。同时在上电初始化过程中,也可以看到重定位操作的区域,如将Flash中的data段或者bss段复制到对应内存区。以及堆栈分布的位置和字节大小等数据。补充基础知识:
加载域(Load Region)与运行域(Execution Region),加载域:程序在 Flash 中的实际存储位置,包括代码段(.text
)、只读数据(.rodata
)和已初始化的可读写数据(.data
)。运行域:程序运行时内存的实际状态,例如 .data
段从 Flash 复制到 RAM,未初始化数据(.bss
)在 RAM 中清零 。
- Code:代码段(
.text
),存储在 Flash 中。 - RO Data:只读数据(如常量字符串),存储在 Flash。
- RW Data:已初始化的全局变量,运行时从 Flash 复制到 RAM。
- ZI Data:未初始化的全局变量,在 RAM 中初始化为0。
Flash区域分布情况
如下图,LR_IROM1(加载区域)表示程序Flash中的存储位置,包含代码和数据的初始值。ER_IROM1(执行区域)是执行区域,属于LR_IROM1的子区域,表示代码在Flash中原地执行。
- Base: 0x08000000 : Flash起始地址。
- Size: 0x00000c4c : 表示已占用空间大小。
- Max: 0x00100000 : 允许的最大占用空间。
根据上图我们可以看到在Flash区域中分布的相关情况,接下俩我们来看内存中的分布情况。
内存区域分布情况
如下图所示,Exec base: 0x20000000 表示该区域在RAM中的起始地址,所有运行的全局变量堆栈等都在此区域分配,Load base: 0x08000bd0表示该区域的初始化数据在Flash中的存储地址,程序启动时,这些地址会从Flash区域的0x08000bd0复制RAM的0x20000000,大小为0x000012e0。同时也可以查看堆栈分布情况,堆(HEAP)的起始地址0x200000e0,大小为0x00000200。栈(STACK)的存放地址0x200002e0,大小为0x00001000。我们可以通过startup_stm32f40_41xxx.s文件中修改堆栈大小。
修改堆栈大小,如下图。
5、Image component sizes:映像组件大小
该模块用于详细统计程序中每个目标文件(Object)和库(Library)的内存占用情况,包括代码段、只读数据、可读写数据等的大小。
-
- 代码段(Code):统计每个目标文件或库成员的机器指令大小(
.text
段)。 - 只读数据(RO Data):常量数据(如字符串、全局常量)的存储占用(
.rodata
段)。 - 可读写数据(RW Data):已初始化的全局变量(
.data
段)的存储占用,需在启动时从Flash复制到RAM。 - 未初始化数据(ZI Data):未初始化的全局变量(
.bss
段)的RAM占用,启动时被清零。 - 调试信息(Debug):调试符号和元数据的大小,影响调试效率但不影响运行时内存。
- 代码段(Code):统计每个目标文件或库成员的机器指令大小(
如下图startup_stm32f40_41xxx.o文件中,Zi Data 为4608字节,通常为堆栈的预留空间。关于Library Member Name(库函数)统计,对于不需要的库函数可以通过优化选项移除来节约空间。
最后来到.map文件末尾,如下图。
- Total RO Size:
Code + RO Data = 3024字节
,决定Flash的代码和常量存储需求。 - Total RW Size:
RW Data + ZI Data = 4832字节
,决定RAM的运行时需求。 - Total ROM Size : Code + RO Data +RW Data = 3148字节,与bin文件大小一样,也是烧录到芯片中的实际数据量,和Load Region LR_IROM1中的大小一样。不包含ZI Data数据段,是因为在运行时由启动代码进行初始化为零操作。
四、map文件实战
1、通过.map文件查看变量是否被优化
在开发过程中经常会发现一个变量被重新赋值后,其值并没有变化,这是因为很可能被优化掉了,如下例子不是最好的例子,但是检查原理一样。
如下图所示,明明已经定义了,但是在map文件中并没有找到,我们可以在Removing Unused input sections from the image段中找到被移除的数据。
被移除的数据段。
这个例子是因为编译器进行了优化,常量传播中,当ro_data2未被取地址,且仅用于读取值时,编译器会将直接替换为0x11,无需分配存储空间。但是输出数据并不影响。在开发过程中,如果不确定一个变量是否被优化掉,我可以通过加上volatile防止被编译器优化。如下图所示。加上之后,就可以在map文件中找到ro_data2,可以通过查询对应变量的方式确定是否被优化。
2、通过.map文件检查内存分配
在初始化过程中,启动代码把Flash中的data段和bss段复制到RAM中,随后分配堆栈的空间。我们基本可以根据RW Data + ZI Data 小于总内存,可以根据这个判定是否判断溢出。
查看Size是否小于总内存量,有的单片机有多块内存,如下图所示。当然,关于内存溢出问题,尽量避免使用大字节的局部变量,同时尽量减少函数的深层次调用等。
3、通过.map文件查看堆栈分配
在前面的例子中有查看堆栈配置的方法,如下图所示,map文件中相关信息,也可以通过startup_stm32f40_41xxx.s文件中修改堆栈大小。
修改方法
4、通过.map文件查看优化存储段
如果在keil中设置了优化代码,如下图所示,那么最后生成的bin文件会将Removing Unused input sections from the image段中代码不参与烧写,通俗一点讲,在这个段内的代码都被优化掉了,极大的节约了存储开销。
这个段中所有移除的都不参与,不占用内存。
我们在main中未调用demo_global__fun函数,这里在map文件中查看已经被优化掉了,我们查看bin大小为2.57KB,我们再对比查看调用的大小。
我们在main中调用demo_global__fun函数,此时demo_global__fun在全局符号区,此时bin文件的大小为2.85KB,说明未使用的时候确实被优化掉了。
5、通过.map文件判断中断向量表覆盖
在 .map
文件的 Memory Map 部分,确认以下内容是否与向量表地址范围重叠:
- 代码段(.text):若代码段起始地址早于向量表结束地址,可能覆盖向量表。
- 初始化数据段(.data):若
.data
段被错误链接到 Flash 区域(如未正确分离加载域与执行域),可能覆盖向量表。 - 其他自定义段:如用户定义的常量表或代码块可能占用向量表空间。
此时,代码段 .text
与向量表 .vectors
地址重叠,导致向量表被覆盖
.text 0x08000000 - 0x08001000(覆盖向量表)
.vectors 0x08000000 - 0x08000100(被代码段覆盖)
6、通过.map文件的检测栈溢出
通过map文件可以查看到堆栈边界,我们就可以编写简单的测试软件达到测试栈溢出问题。如下图,我们确定栈边界后,编写简单的测试程序。
uint32_t *stackGuard = (uint32_t*)0x2000C1F8; // 栈顶地址
if (*stackGuard != 0xDEADBEEF) { /* 溢出处理 */ }
五、总结
本文介绍了.map的生成、使用与实战,通过map文件,能够更快的定位错误。