一. MDK 编译生成文件简介
MDK 编译 STM32 工程时,会在我是HAL库生成的路径大概是MDK-ARM\01_LED在这个文件夹,这是我之间一个freertos的代码,主要实现一个智能旅行箱。这里生成一堆中间文件,最终才生成能下载进单片机的 .hex。
大概流程是:
- 编译 :先把
.c/.s变成.o - 链接 : 把所有
.o合成.axf - 转换 :把
.axf转成.hex供下载

对于MDK工程来说,基本上任何工程在编译过程中都会有这11类文件,常见的MDK 编译过程生产文件类型如表1.1所示:
| 文件类型 | 作用(小白版) |
|---|---|
| .o | 每个 C / 汇编文件编译后的中间对象文件 |
| .axf | 最终可调试、可仿真的完整程序文件 |
| .hex | 能直接下载到 STM32 的固件文件 |
| .crf | 记录函数 / 变量在哪里被引用 |
| .d | 记录每个 .o 依赖哪些文件 |
| .dep | 整个工程的总依赖文件 |
| .lnp | 给链接器用的命令输入文件 |
| .lst | 编译器生成的汇编列表文件 |
| .htm | 查看栈深度、函数调用关系 |
| .build_log.htm | 最近一次编译的日志 |
| .map | 查看内存占用、函数地址、代码大小 |
想知道其他文件类型及说明,请大家参考:



二、.htm文件解析
1、基本文件构成
| 组成部分 | 核心含义 | 工程作用 |
|---|---|---|
| 最大栈使用量 | 静态分析得出的最小必需栈大小 | 确定栈空间最低配置值 |
| 最大栈深调用链 | 栈消耗最高的函数调用路径 | 定位栈开销大户、优化方向 |
| 互斥递归函数 | 自递归 / 相互递归的函数 | 排查死循环、栈溢出风险 |
| 函数指针引用 | 被向量表 / 指针调用的函数 | 验证中断、回调函数绑定 |
| 全局符号栈信息 | 全局函数的栈大小、调用关系 | 查看业务函数栈占用 |
| 本地符号栈信息 | 静态函数的栈大小、调用关系 | 查看内部函数栈占用 |
2、最大栈深和最大栈深调用链
点开.htm这个文件可知静态可追踪的最大栈深。主栈(MSP)、任务栈(PSP)配置不能低于这个值。

最大栈深调用链:
StartDefaultTask ⇒ parseGpsBuffer ⇒ atof ⇒ __strtod_int ⇒ _local_sscanf ⇒ _scanf_real
什么是最大栈深调用链,这是编译器逐行遍历代码,找到的函数嵌套最深的一条路;函数每嵌套一层,就会在栈(Stack) 里存数据(返回地址、局部变量等)。
找到startup_stm32f103xb.s文件,打开可知默认栈深 (Stack_Size) = 0x400 字节,0x400 = 1024 字节 = 1KB,计算为4*16^2=1024

3、全局符号栈信息和本地符号栈信息
Global Symbols(全局符号栈信息)代表片段

parseGpsBuffer (Thumb, 284 bytes, Stack size 32 bytes, gps.o(i.parseGpsBuffer))
[Stack]
Max Depth = 256
Call Chain = parseGpsBuffer ⇒ atof ⇒ __strtod_int ⇒ _local_sscanf ⇒ _scanf_real
[Calls]
>> __aeabi_d2f
>> strchr
>> strncpy
>> atof
[Called By]
>> StartDefaultTask
>> USART1_IRQHandler
Local Symbols(本地符号栈信息)代表片段

prvCopyDataFromQueue (Thumb, 38 bytes, Stack size 8 bytes, queue.o(i.prvCopyDataFromQueue))
[Stack]
Max Depth = 8
Call Chain = prvCopyDataFromQueue
[Calls]
>> __aeabi_memcpy
[Called By]
>> xQueueReceive
格式如下:
| 字段 | 含义 | 示例 |
|---|---|---|
| 函数名 | 符号名称 | Get_Weight, prvInitialiseNewTask |
| Thumb | 指令集模式(Thumb/ARM) | (Thumb, 122 bytes) |
| 字节数 | 函数代码占用空间 | 122 bytes |
| Stack size | 函数自身栈占用 | 24 bytes |
| 所属文件 | 定义该符号的目标文件 | hx711.o(i.Get_Weight) |
| [Stack] | 栈深度与调用链 | Max Depth = 112 |
| Call Chain | 函数调用路径 | Get_Weight ⇒ __aeabi_dmul ⇒ ... |
| [Calls] | 该函数调用的其他函数 | >> __aeabi_f2d, >> Read_HX711 |
| [Called By] | 调用该函数的其他函数 | >> StartTask03 |
| [Address Reference Count] | 地址引用次数 | [Address Reference Count : 1] |
学习这个 .htm ,能快速查到最大栈深与调用链,安全配置栈大小、避免栈溢出死机;还能定位递归、自调用等风险代码,快速排查 HardFault 等死机问题;同时可看清每个函数的栈占用与调用关系,精准优化内存与代码体积。
三、.map文件解析
.map 文件是编译器链接时生成的一个文件,它主要包含了交叉链接信息。通过.map 文件,我们可以知道整个工程的函数调用关系、FLASH 和 RAM 占用情况及其详细汇总信息,能具体到单个源文件(.c/.s)的占用情况,根据这些信息,我们可以对代码进行优化。.map 文件可以分为以下 5 个组成部分:
| 组成部分 | 简介 |
|---|---|
| 程序段交叉引用关系 | 描述各文件之间函数调用关系 |
| 删除映像未使用的程序段 | 描述工程中未用到而被删除的冗余程序段 (函数 / 数据) |
| 映像符号表 | 描述各符号(程序段 / 数据)在存储器中的地址、类型、 |
| 映像内存分布图 | 描述各个程序段(函数)在存储器中的地址及占用大小 |
| 映像组件大小 | 给出整个映像代码(.o)占用空间信息 |
1、map 文件的 MDK 设置
在模式棒找到如下配置,默认情况下,MDK 这部分设置就是 全勾选的,如果我们想取消掉一些信息的输出,则取消相关勾选即可(一般不建议)。

编译后双击工程即可打开,

若不能打开,打开魔术棒找到listing电机Select Folder for Listings在当前工程新建一个Listings文件夹,然后选中就可以重复上一步操作。

2、 map 文件的基础概念
为了更好的分析 map 文件,我们先对需要用到的一些基础概念进行一个简单介绍,相关概念如下:
- Section:描述映像文件的代码或数据块,我们简称程序段
- RO:Read Only 的缩写,包括只读数据(RO data)和代码(RO code)两部分内容,占用 FLASH 空间
- RW:Read Write 的缩写,包含可读写数据(RW data,有初值,且不为 0),占用 FLASH(存储初值)和 RAM(读写操作)
- ZI:Zero initialized 的缩写,包含初始化为 0 的数据(ZI data),占用 RAM 空间。
- .text:相当于 RO code
- .constdata:相当于 RO data
- .bss:相当于 ZI data
- .data:相当于 RW data
3、map 文件的组成部分说明
1、程序段交叉引用关系(Section Cross References)
这是 .map 文件里的 程序段交叉引用 (Section Cross References),是链接器记录的符号依赖关系,不只是函数调用,还包括函数引用全局变量、启动文件引用中断向量,是编译器把所有文件链接成可执行文件时,标记谁需要用到谁的清单。

这一段main函数主要意思是,完成了 HAL 库初始化、系统时钟配置、GPIO / 串口 / 定时器等外设初始化、硬件驱动初始化,最终实现 FreeRTOS 内核的初始化与调度启动,同时引用了定时器、串口的全局变量,同时主要有相互引用的关系,如:freertos.o(i.MX_FREERTOS_Init) refers to freertos.o(i.StartDefaultTask) for StartDefaultTasky意思是目标文件 freertos.o 内的 MX_FREERTOS_Init 函数段,引用并调用了该目标文件中的 StartDefaultTask 任务函数符号。
2、删除映像未使用的程序段
搜索:Removing Unused input sections from the image.
这部分内容描述了工程中由于未被调用而被删除的冗余程序段(函数/数据)。

上图中,列出了所有被移除的程序段。最下面统计出625 unused section(s) (total 43669 bytes) removed from the image.即一共清理了 625 处、约 42KB 的无用内容。
精准地删掉没用到的函数
打开魔术棒

勾选后One ELF Section per Function后,每个函数都在自己的段里。链接器扫描时,如果发现某个段里的函数没有被任何地方调用,就可以直接把这个段整个删掉,只保留用到的代码,没用的函数全被清掉,固件体积大幅减小。
4、映像符号表
映像符号表(Image Symbol Table)描述了被引用的各个符号(程序段/数据)在存储器 中的存储地址、类型、大小等信息。映像符号表分为两类:本地符号(Local Symbols)和全 局符号(Global Symbols)。
(1)本地符号
本地符号(Local Symbols)记录了用static声明的全局变量地址和大小,c文件中函数的地址和用 static 声明的函数代码大小,汇编文件中的标号地址,作用域:限本文件。

任意举一个例子:i.Get_Weight 函数
i.Get_Weight 0x08000ef4 Section 0 hx711.o(i.Get_Weight)
表示hx711文件中的 Get_Weight函数的入口地址为:0x08000fe,类型为:Section(程序段),大小为0。因为:i. Get_Weight仅仅表示Get_Weight函数入口地址,并不是指令,所以没有大小。在全局符号段,会列出 Get_Weight函数的大小。
(2)全局符号
全局符号(Global Symbols)记录了全局变量的地址和大小,C文件中函数的地址及其 代码大小,汇编文件中的标号地址(作用域:全工程)。


找到Get_Weight函数,表示 hx711.c 文件中的 Get_Weight函数的入口地址为: 0x08000ef5,类型为:Thumb Code(程序段),大小为122字节。
Get_Weight 0x08000ef5 Thumb Code 122 hx711.o(i.Get_Weight)
可以发现Get_Weight地址不一样,这是因为 ARM规定Thumb指令集的所有指令,其最低位必须为0x08000ef5= 0x08000ef4+ 1, 所以才会有2个不同的地址,且总是差1,实际上就是同一个函数。
5、映像内存分布图
映像文件分为加载域(Load Region)和运行域(Execution Region),一个加载域必须有 至少一个运行域(可以有多个运行域),而一个程序又可以有多个加载域。加载域为映像程序的实际存储区域,而运行域则是MCU上电后的运行状态。加载域和运行域的简化关系(这里仅表示一个加载域的情况)。

RW区也是存放在ROM(FLASH)里面的,在执行main函数之前,RW(有初值且不为0的变量)数据会被拷贝到RAM区,同时还会在RAM里面创建ZI区(初始化为0的变量)。

6、映像组件大小
映像组件大小(Image component sizes)给出了整个映像所有代码(.o)占用空间的汇总信息,这是用得最多的。
这部分记录工程内所有目标文件(.o)、驱动库、启动代码、自定义函数与全局变量的分配信息,精准标注各程序段、数据段的物理存储地址、字节占用容量及所属源文件;同时汇总生成映像组件占用统计,统计固件整体 Flash、RAM 资源消耗。

其中FLASH放的是Code、RO Date以及RW Date;SRAM放的是RW Date和ZI Date两项。

482

被折叠的 条评论
为什么被折叠?



