1.1 编译过程简介
首先简单了解下MDK的编译过程,它与其它编译器的工作过程是类似的,该过程见下图:
编译过程生成的不同文件将在后面的小节详细说明,此处先抓住主要流程来理解。
1.2 具体工程中的编译过程
打开一个工程,点击MDK的“rebuild”按钮,它会重新构建整个工程,构建的过程会在MDK下方的“Build Output”窗口输出提示信息。
构建工程的提示输出主要分6个部分,说明如下:
2.1 CODE 、RO 、RW、ZI Data域及堆栈空间
在工程的编译提示输出信息中有一个语句“Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx”,说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据(包括代码)被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:
图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是RAM存储器区域,下方是ROM存储器区域。
程序在存储状态时。RO节(RO section)及RW节都被保存在ROM区。当程序开始运行时,内核直接从ROM中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把RW节数据从ROM复制到RAM,并且在RAM加入ZI节,ZI节的数据都被初始化为0,加载完后RAM区准备完毕,正式开始执行主体程序。
编译生成的RW-data的数据属于图中的RW节,ZI-data的数据属于图中的ZI节。是否需要掉电保存,这就是把RW-data与ZI-data区别开来的原因,因为在RAM创建数据的时候,默认值为0,但如果有的数据要求初值非0,那就需要使用ROM记录该初始值,运行时再复制到RAM。
STM32的RO区域不需要加载到SRAM,内核直接从flash读取指令运行。计算机系统的应用程序运行过程很类似,不过计算机系统的程序在存储状态时位于硬盘,执行的时候甚至会把上述的RO区域(代码、只读数据)加载带内存,加快运行速度,还有虚拟内存管理单元(MMU)辅助加载数据,使得可以运行比物理内存还大的应用程序。而STM32没有MMU,所以无法支持Linux和Windows系统。
当程序存储到STM32芯片的内部flash时(即ROM区),它占用的空间是Code、RO-data及RW-data的总和,所以如果这些内容比STM32芯片的flash空间大,程序就无法被正常保存了。当程序在执行的时候,需要占用内部SRAM空间(即RAM区),占用的空间包括RW-data和ZI-data。应用程序在各个状态时各区域的组成见表:
在MDK中,我们建立的工程一般会选择芯片型号,选择后就有确定的flash及SRAM大小,若代码超出了芯片的存储区的极限,编译器会提示错误,这时就需要裁减程序了,裁减时可针对超出的区域来优化。
在前面编译过程中,MDK调用了各种编译工具,平时我们直接配置MDK,不需要学习如何使用他们,但了解他们是非常有好处的。例如,若希望使用MDK编译生成bin文件的,需要在MDK中输入指令控制fromelf工具;在本章后面讲解AXF及O文件的时候,需要利用fromelf工具查看其文件信息。这都是无法直接通过MDK做到的。关于这些工具链的说明,在MDK的帮助手册《ARM Development Tools》都有详细讲解,点击MDK界面的“help->uVision Help”菜单可打开该文件。
3.1 设置环境变量
调用这些编译工具,需要用到Windows的“命令行提示符工具”,为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。查看本机工具链所在的具体目录可根据上一小节讲解的工程编译提示输出信息中找到,如本机的路径为“D:\work\keil5\ARM\ARMCC\bin”。
添加路径到PATH环境变量
本文以win7系统为例添加工具链的路径到PATH环境变量,其他系统是类似的。
3.2 armcc 、 armasm 及 armlink
接下来我们看看各个工具链的具体用法,主要以armcc为例。
armcc
armcc用于把c/c++文件编译成ARM指令代码,编译后会输出ELF格式的O文件(对象、目标文件),在命令行中输入“armcc”回车可调用该工具,它会打印帮助说明,见图:
帮助提示中分三部分,第一部分是armcc版本信息,第二部分是命令的用法,第三部分是主要命令选项。
根据命令用法:armcc[options] file1 file2 … filen,在[option]位置可输入下面的“- -arm”、“- -cpu list”选项,若选项带文件输入,则把文件名填充在file1 file2…的位置,这些文件一般是c/c++文件。
例如根据它的帮助说明,“- -cpu list”可列出编译器支持的所有cpu,我们在命令行中输入“armcc - -cpu list”,可查看图中的cpu列表:
打开MDK的Option for Target->c/c++菜单,可看到MDK对编译器的控制命令,见下图:
从该图中的命令可看到,它调用了-c、-cpu -D -g -O1等编译选项,当我们修改MDK的编译选项时,可看到该控制命令也会有相应的变化。然而我们无法在该编译选项框中输入命令,只能通过MDK提供的选项修改。
了解这些,我们就可以查询具体的MDK编译选项的具体信息了,如c/c++选项中的“Optimization: Level 1(-O1)”是什么功能呢?首先可了解到它是“-O”命令,命令后还带个数字,查看MDK的帮助手册,在armcc编译器说明章节,可详细了解,如下图:
利用MDK,我们一般不需要自己调用armcc工具,但经过这样的过程我们就会对MDK有更深入的认识,面对它的各种编译选项,就不会那么头疼了。
armasm
armasm是汇编器,它把汇编文件编译成O文件。与armcc类似,MDK对armasm的调用选项可在“Option for Target->Asm”页面进行配置,如下图:
armlink
armlink是链接器,它把各个O文件链接组合在一起生成ELF格式的AXF文件,AXF文件是可执行的,下载器把该文件中的指令代码下载到芯片后,该芯片就能运行程序了;利用armlink还可以控制程序存储到指定的ROM或RAM地址。在MDK中可在“Option for Target->Linker”页面配置armlink选项,如下图:
链接器默认是根据芯片类型的存储器分布来生成程序的,该存储器分布被记录在工程里的sct后缀的文件中,有特殊需要的话可自行编辑该文件,改变链接器的链接方式,具体后面我们会详细讲解。
3.3 armar、fromelf及用户指令
armar工具用于把工程打包成库文件,fromelf可根据axf文件生成hex、bin文件,hex和bin文件是大多数下载器支持的下载文件格式。
在MDK中,针对armar和fromelf工具的选项几乎没有,仅集成了生成HEX或Lib的选项,如下图:
例如如果我们想利用fromelf生成bin文件,可以在MDK的“Option for Target->User”页中添加调用fromelf的指令,如下图:
在User配置页面中,提供了三种类型的用户指令输入框,在不同组的框输入指令,可控制指令的执行时间,分别是编译前(Before Compile c/c++ file)、构建前(Before Build/Rebuild)及构建后(After Build/Rebuild)执行。这些指令并没有限制必须是arm的编译工具链。例如如果您自己编写了Python脚本,也可以在这里输入用户指令执行该脚本。
图中的生成bin文件指令调用了fromelf工具,紧跟后面的是工具的选项及输出文件名、输入文件名。由于fromelf是根据axf文件生成bin的,而axf文件又是构建(build)工程后才生成,所以我们把该指令放到“After Build/Rebuild”一栏。
除了上述编译过程生成的文件,MDK工程中还包含了各种各样的文件,下面我们统一介绍,MDK工程的常见文件类型见下图:
这些文件主要分为MDK相关文件、源文件以及编译、链接器生成的文件。
4.1 uvprojx、uvoptx、uvguix 及 ini工程文件
在工程的“Project”目录下主要是MDK工程相关的文件,如图:
uvprojx、uvoptx 及 uvguix都是使用XML格式记录的文件,若使用记事本打开可以看到XML代码,见下图。而当使用MDK软件打开时,它根据这些文件的XML记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。
这些工程参数都是当MDK正常退出时才会被写入保存,所以若MDK错误退出时(如使用Windows的任务管理器强制关闭),工程配置参数的最新更改是不会被记录的,重新打开工程时要再次配置。根据这几个文件的记录类型,可以知道uvprojx文件是最重要的,删掉它我们就无法再正常打开工程了,而uvoptx及uvguix文件并不是必须的,可以删除,重新使用MDK打开uvprojx工程文件后,会以默认参数重新创建uvoptx及uvguix文件。(所以当使用Git/SVN等代码管理的时候,往往只保留uvprojx文件)
4.2 源文件
源文件是工程中我们最熟悉的内容了,它们就是我们编写的各种源代码,MDK支持c、cpp、h、s、inc类型的源代码文件,其中c、cpp分别是c/c++语言的源代码,h是它们的头文件,s是汇编文件,inc是汇编文件的头文件,可使用“$include”语法包含。编译器根据工程的源文件最终生成机器码。
4.3 output目录下生成的文件
点击MDK中编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的“Option for Target->Output->Select Folder for Objects”和“Option for Target->Listing->Select Folder for Listings”选项配置它们的输出路径,如下图:
接下来我们讲解Output路径下的文件。
ELF文件头:接下来我们看看具体文件的内容,使用fromelf文件可以查看*.o、.axf及.lib文件的ELF信息。
使用命令行,切换到文件所在的命令,输入“fromelf -text -v bsp_led.o”命令,可控制输出bsp_led.o的详细信息,见下图。利用“-c 、 -z”等选项还可输出反汇编指令文件、代码及是数据文件等信息,请亲手尝试一下。
为了便于阅读,我已使用fromelf指令生成了“多彩流水灯.axf”、“bsp_led”及“多彩流水灯.lib”的ELF信息,并已把这些信息保存在独立的文件中,在配套资料的“elf信息输出”文件夹下可查看,见下图。
分散加载代码
前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在FLASH中的RW-data数据拷贝至SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?
通过查看“多彩流水灯_axf_elfInfo_c.txt”的反汇编信息,了解到程序中具有一段名为“__scatterload”的分散加载代码,见下图,它是有armlink链接器自动生成的。
这段分散加载代码包含了拷贝过程(LDM复制指令),而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据。而“__scatterload”的代码会被“__main”函数调用,见下图,__main在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。
生成hex文件
生成hex文件的配置比较简单,在“Option for Target ->Output ->Create Hex File”中勾选该选项,然后编译工程即可,见下图。
生成bin文件
使用MDK生成bin文件需要使用fromelf命令,在MDK的|“Option for Target -> Users”中加入下图中的命令。
图中的指令内容为:“fromelf --bin --output …\Output\多彩流水灯.bin …\Output\多彩流水灯.axf”
该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看fromelf的帮助,见下图。
我们在MDK输入的指令格式是遵守fromelf帮助里的指令格式说明的,其格式为:“fromelf [options] input_file”
其中options是指令选项,一个指令支持输入多个选项,每个选项之间使用空格隔开,我们的实例中使用“–bin”选项设置输出bin文件,使用“–output file”选项设置输出文件的名字为“…\Output\多彩流水灯.bin”,这个名字是一个相对路径格式,如果不了解如何使用“…\”表示路径,可使用MDK命令输入框后面的文件夹图标打开文件浏览器选择文件,在命令的最后使用“…\Output\多彩流水灯.axf”作为命令的输入文件。具体的格式分解见下图。
fromelf需要根据工程的*.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的*.axf文件,在MDK命令输入栏中,我们把fromelf指令放置在“After Build/Rebuild”(工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。
设置完成生成hex的选项或添加了生成bin的用户指令后,点击工程的编译按钮,重新编译工程,成功后可看到下图中的输出。打开相应的目录即可找到文件,若找不到bin文件,请查看提示输出栏执行指令的信息,根据信息改正fromelf指令。
其中bin文件是纯二进制数据,无特殊格式,接下来我们了解一下hex文件格式。
hex文件格式
hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。
一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如“:11aaaatt[dd…]cc”,例如本“多彩流水灯”工程生成的hex文件前几条记录见如下代码。
记录的各个部分介绍如下:
例如,上图中的第一条记录解释如下:
再来看第二条记录:
为了更清楚地对比bin、hex及axf文件的差异,我们来查看这些文件内部记录的信息来进行对比。
hex、bin及axf文件的区别与联系
bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。
同一个工程生成的bin、hex及axf文件的大小见下图。
实际上,这个工程要烧写到FLASH的内容总大小为1456字节,然而在Windows中查看的bin文件却比它大(bin文件是FLASH的代码映像,大小应一致),这是因为Windows文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小,见下图。
接下来我们打开本工程的“多彩流水灯.bin”、“多彩流水灯.hex”及由“多彩流水灯.axf”使用fromelf工具输出的反汇编文件“多彩流水灯_axf_elfInfo_c.txt”文件。清晰地对比他们的差异,见下图。如果您想要亲自阅读自己电脑 上的bin文件,推荐使用sublime软件打开,它可以把二进制数以ASCII码呈现出来,便于阅读。
在“多彩流水灯_axf_elfInfo_c.txt”文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是*.axf文件的原始内容,但因为它是通过*.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。
在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三者之间的区别与联系。
由于文件中存储的都是机器码,见下图,该图是我根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。
如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品。所以在实际的生产中,一定要注意做好加密措施。由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。
注意:查看了各个工程的静态调用图文件统计后,我们发现本书提供的一些比较大规模的工程例子,静态栈调用最大深度都已超出STM32启动文件默认的栈空间大小0x00000400,即1024字节,但在当时的调试过程中却没有发现错误,因此我们也没有修改栈的默认大小(有一些工程调试时已发现问题,它们的栈空间就已经被我们改大了),虽然这些工程实际运行并没有错误,但这可能只是因为它使用的栈溢出RAM空间恰好没被程序其他部分修改而已。所以,建议您在实际的大型工程应用中(特别是使用了各种外部库时,如Lwip/emWin/Fatfs等),要查看本静态调用图文件,了解程序的栈使用情况,给程序分配合适的栈空间。
4.4 Listing目录下的文件
在Listing目录下包含了*.map及*.lst文件,它们都是文本格式的,可使用Windows的记事本软件打开。其中lst文件仅包含了一些汇编符号的链接信息,我们重点分析map文件。
1.map文件说明
map文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的Code、RO-data、RW-data以及ZI-data的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”,各部分介绍如下:
节区的跨文件引用
打开“多彩流水灯.map”文件,可看到它的第一部分——节区的跨文件引用(Section Cross References),见下图。
在这部分中,详细列出了各个*.o文件之间的符号引用。由于*.o文件是由asm或c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“Section Cross References”一一列出。
例如,开头部分说明的是startup_stm32f429_439xx.o文件中的“RESET”节区分为它使用的“__initial_sp”符号引用了同文件“STACK”节区。
也许我们对启动文件不熟悉,不清楚这究竟是什么,那我们继续浏览,可看到main.o文件的引用说明,如说明main.o文件的i.main节区为它使用的LED_GPIO_Config符号引用了bsp_led.o文件的i.LED_GPIO_Config节区。
同样地,下面还有bsp_led.o文件的引用说明,如说明了bsp_led.o文件的i.LED_GPIO_Config节区为它使用的GPIO_Init符号引用了stm32f4xx_gpio.o文件的i.GPIO_Init节区。
可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。有时在构建工程的时候,编译器会输出“undefined symbol xxx (referred from xxx.o)”这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。例如,下图,我们把bsp_led.c文件中定义的函数LED_GPIO_Config改名为LED_GPIO_ConfigABCD,而不修改main.c文件中的调用,就会出现main文件无法找到LED_GPIO_Config符号的提示。
删除无用节区
map文件的第二部分是删除无用节区的说明(removing unused input sections from the image),见下图。
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf文件,不是指在*.o文件删除),这样可以防止这些无用数据占用程序空间。
例如,上面的信息中说明startup_stm32f429_439xx.o中的HEAP(在启动文件中定义的用于动态分配的“堆”区)以及stm32f4xx_adc.o的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何stm32f4xx_adc.c中的内容。由此也可以知道,虽然我们把STM32标准库的各个外设对应的c库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中。
符号映像表
map文件的第三部分是符号映像表(Image Symbol Table),见下图。
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到LED_GPIO_Config符号存储在0x080002a5地址,它属于Thumb Code类型,大小为106字节,它所在的节区为bsp_led.o文件的i.LED_GPIO_Config节区。