生成 bin 和 hex文件
若编译过程无误,即可把工程生成前面对应的*.axf 文件,而在 MDK 中使用下载器(DAP/JLINK/ULINK 等)下载程序或仿真的时候, MDK 调用的就是*.axf 文件,它解释该文件,然后控制下载器把*.axf 中的代码内容下载到 STM32 芯片对应的存储空间,然后复位后芯片就开始执行代码了。
然而,脱离了 MDK 或 IAR 等工具,下载器就无法直接使用*.axf 文件下载代码了,它们一般仅支持 hex 和 bin 格式的代码数据文件。默认情况下 MDK 都不会生成 hex 及 bin 文件,需要配置工程选项或使用 fromelf 命令。
fromelf的命令描述:
bin 和 hex 文件详解
bin文件就是纯二进制数据,无特殊格式。
hex 是 Intel 公司制定的一种使用 ASCII 文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到 ROM 中的数据,绝大多数下载器支持该格式。
hex格式的具体描述如下所示,非常重要。
5个记录,每部分的具体意义:
接下来,我们分析一下这几条hex文件的内容和意义:
用vscode打开我们生成的hex文件如下,可以看到,与上述hex格式编码要求一致。
为了更清楚地对比 bin、 hex 及 axf 文件的差异,我们来查看这些文件内部记录的信息来进行对比:
注意,bin文件直接用记事本打开会乱码,因为windows记事本默认是ASCII编码格式,而bin文件不是ASCII格式存储,所以记事本打开会是乱七八糟的乱码。
接下来,我们对比看一下bin hex文件的大小:
bin文件:
可以看到,bin文件大小为1440字节,而占用的空间是4096字节。为什么会有这个区别?
因为博主使用的windows系统,一个文件最小是4096字节,文件占用的空间都必须是4096的倍数。再进一步分析,为什么bin文件是1440字节?
实际上,1440 = 1096 + 336 + 8,bin文件的大小,就是实际上要存储在flash的数据大小。所以,bin文件是一种很纯粹的数据格式。
hex文件:
可以看到,hex的实际大小为4101字节(>4096),只比4096字节超出一点点。但windows使用的文件系统,就是会占用到
8192 = 2 * 4096字节的存储空间。
axf文件:
axf文件,可以看到,要比bin文件和hex文件大很多,因为其中还存储了各种MDK文件类型信息。我们可以直接使用fromelf工具,来对axf文件进行反汇编,来得到汇编代码,进而破解别人的程序。
下面是bin hex elf反汇编的比较:
htm静态调用图文件
我们打开对应的流水灯.htm文件,如下所示:
该文件说明了本工程的静态栈空间最大占用 40 字节(Maximum Stack Usage:40bytes),这个占用最深的静态调用为“ main->LED_GPIO_Config->GPIO_Init”。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在 STM32 中可修改启动文件改变堆栈的大小;如果空间不足,可从该文件中了解到调用深度的信息,然后优化该代码。
map文件
如何打开 map 文件?1 直接在Listing目录下打开;2 在 MDK 中双击目标选项,即可打开 map 文件;
注意,在查看 map 文件时,要勾选 MDK 的如下选项配置:
选项One ELF Section per Function的主要功能是对冗余函数的优化。通过这个选项,可以在最后生成的二进制文件中将冗余函数排除掉(虽然其所在的文件已经参与了编译链接),以便最大程度地优化最后生成的二进制代码。
而该选项实现的机制是将每一个函数作为一个优化的单元,而并非整个文件作为参与优化的单元。
选项One ELF Section per Function所具有的这种优化功能特别重要,尤其是在对于生成的二进制文件大小有严格要求的场合。人们习惯将一系列接口函数放在一个文件里,然后将其整个包含在工程中,即使这个文件将只有一个函数被用到。这样,最后生成的二进制文件中就有可能包含众多的冗余函数,造成了宝贵存储空间的浪费。
选项One ELF Section per Function对于一个大工程的优化效果尤其突出,有时候甚至可以达到减半的效果。当然,对于小工程或是少有冗余函数的工程来说,其优化效果就没有那么明显了。
打开“流水灯.map”文件,可看到它的第一部分——节区的跨文件引用(Section Cross References):
我们来看一下 map 文件:
在这部分中,详细列出了各个*.o 文件之间的符号引用。由于*.o 文件是由 asm 或 c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“Section Cross References”一一列出。例如,开头部分说明的是 startup_stm32f10x.o 文件中的“RESET”节区分为它使用的“__initial_sp” 符号引用了同文件“STACK”节区。
也许我们对启动文件不熟悉,不清楚这究竟是什么,那我们继续浏览,可看到 main.o文件的引用说明,如说明 main.o 文件的 i.main 节区为它使用的 LED_GPIO_Config 符号引用了 bsp_led.o 文件的 i.LED_GPIO_Config 节区。
可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。
常见的调试错误:
比如,我们现在改一下函数名(改成LED_GPIO_Config0),如下:
直接编译文件,没有问题,结果如下:
而当点击build(编译链接)时,就会出现错误如下:
其实,我在写代码时,有时会经常遇到这个错误。它提示的是,在main.o中,没有找到引用的函数(符号)LED_GPIO_Config,实际上,main.o是main.c编译后生成的目标文件,这个目标文件引用的LED_GPIO_Config在所有的目标文件中都没有找到,因为我们把函数名修改成了LED_GPIO_Config0。找不到,那么自然就提示错误了。
删除无用节区
map 文件的第二部分是删除无用节区的说明(Removing Unused input sections from the image)
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf 文件,不是指在*.o 文件删除),这样可以防止这些无用数据占用程序空间。
再看一下gpio,因为我们之前使用了gpio_init的初始化函数,进行对比一下:
对比左右两边可以发现,我们在程序中调用过的GPIO_Init函数,没有出现在删除无用节区的列表中。而没有调用到的函数,被直接被删除掉以节省存储空间。
符号映像表
map 文件的第三部分是符号映像表
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到 LED_GPIO_Config 符号存储在 0x08000251 地址,它属于 Thumb Code 类型,大小为 92 字节,它所在的节区为 bsp_led.o 文件的 i.LED_GPIO_Config 节区。
存储器映像索引
本工程的存储器映像索引分为 ER_IROM1 及 RW_IRAM1 部分,它们分别对应 STM32内部 FLASH 及 SRAM 的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分 Code、 RO-data、 RW-data及 ZI-data。
例如,从上面的表中我们可以看到 i.LED_GPIO_Config 节区存储在内部 FLASH 的0x08000250 地址,大小为 0x00000060,类型为 Code,属性为 RO。而程序的 STACK 节区(栈空间)存储在 SRAM 的 0x20000208 地址,大小为 0x00000400,类型为 Zero,属性为RW(即 RW-data) 。
映像组件大小
map 文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常查询的内容。
首先是前面的表格数据,可以明显的看到中断向量表和启动文件栈空间:
这部分包含了各个使用到的*.o 文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的 Code、 RO-data、 RW-data及 ZI-data 的大小,并根据这些大小统计出占据的 ROM 总空间。
数据分析: