第48章 MDK的编译过程及文件类型全解(1)

2020-05-06 15:31:37 浏览数 (1)

第48章     MDK的编译过程及文件类型全解

全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn

野火视频教程优酷观看网址:http://i.youku.com/firege

本章参考资料:MDK的帮助手册《ARM Development Tools》,点击MDK界面的"help->uVision Help"菜单可打开该文件。关于ELF文件格式,参考配套资料里的《ELF文件格式》文件。

在本章中讲解了非常多的文件类型,学习时请跟着教程的节奏,打开实际工程中的文件来了解。

相信您已经非常熟练地使用MDK创建应用程序了,平时使用MDK编写源代码,然后编译生成机器码,再把机器码下载到STM32芯片上运行,但是这个编译、下载的过程MDK究竟做了什么工作?它编译后生成的各种文件又有什么作用?本章节将对这些过程进行讲解,了解编译及下载过程有助于理解芯片的工作原理,这些知识对制作IAP(bootloader)以及读写控制器内部FLASH的应用时非常重要。

48.1 编译过程

48.1.1 编译过程简介

首先我们简单了解下MDK的编译过程,它与其它编译器的工作过程是类似的,该过程见图 481。

图 481 MDK编译过程

编译过程生成的不同文件将在后面的小节详细说明,此处先抓住主要流程来理解。

(1)    编译,MDK软件使用的编译器是armcc和armasm,它们根据每个c/c 和汇编源文件编译成对应的以".o"为后缀名的对象文件(Object Code,也称目标文件),其内容主要是从源文件编译得到的机器码,包含了代码、数据以及调试使用的信息;

(2)    链接,链接器armlink把各个.o文件及库文件链接成一个映像文件".axf"或".elf";

(3)    格式转换,一般来说Windows或Linux系统使用链接器直接生成可执行映像文件elf后,内核根据该文件的信息加载后,就可以运行程序了,但在单片机平台上,需要把该文件的内容加载到芯片上,所以还需要对链接器生成的elf映像文件利用格式转换器fromelf转换成".bin"或".hex"文件,交给下载器下载到芯片的FLASH或ROM中。

48.1.2 具体工程中的编译过程

下面我们打开"多彩流水灯"的工程,以它为例进行讲解,其它工程的编译过程也是一样的,只是文件有差异。打开工程后,点击MDK的"rebuild"按钮,它会重新构建整个工程,构建的过程会在MDK下方的"Build Output"窗口输出提示信息,见图 482。

图 482 编译工程时的编译提示

构建工程的提示输出主要分6个部分,说明如下:

(1)    提示信息的第一部分说明构建过程调用的编译器。图中的编译器名字是"V5.06(build 20)",后面附带了该编译器所在的文件夹。在电脑上打开该路径,可看到该编译器包含图 483中的各个编译工具,如armar、armasm、armcc、armlink及fromelf,后面四个工具已在图 481中已讲解,而armar是用于把.o文件打包成lib文件的。

图 483 编译工具

(2)    使用armasm编译汇编文件。图中列出了编译startup启动文件时的提示,编译后每个汇编源文件都对应有一个独立的.o文件。

(3)    使用armcc编译c/c 文件。图中列出了工程中所有的c/c 文件的提示,同样地,编译后每个c/c 源文件都对应有一个独立的.o文件。

(4)    使用armlink链接对象文件,根据程序的调用把各个.o文件的内容链接起来,最后生成程序的axf映像文件,并附带程序各个域大小的说明,包括Code、RO-data、RW-data及ZI-data的大小。

(5)    使用fromelf生成下载格式文件,它根据axf映像文件转化成hex文件,并列出编译过程出现的错误(Error)和警告(Warning)数量。

(6)    最后一段提示给出了整个构建过程消耗的时间。

构建完成后,可在工程的"Output"及"Listing"目录下找到由以上过程生成的各种文件,见图 484。

图 484 编译后Output及Listing文件夹中的内容

可以看到,每个C源文件都对应生成了.o、.d及.crf后缀的文件,还有一些额外的.dep、.hex、.axf、.htm、.lnp、.sct、.lst及.map文件。

48.2 程序的组成、存储与运行

48.2.1 CODE、RO、RW、ZI Data域及堆栈空间

在工程的编译提示输出信息中有一个语句"Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx",它说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据(包括代码)被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:

    Code:即代码域,它指的是编译器生成的机器指令,这些内容被存储到ROM区。

    RO-data:Read Only data,即只读数据域,它指程序中用到的只读数据,这些数据被存储在ROM区,因而程序不能修改其内容。例如C语言中const关键字定义的变量就是典型的RO-data。

    RW-data:Read Write data,即可读写数据域,它指初始化为"非0值"的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在RAM区,因而应用程序可以修改其内容。例如C语言中使用定义的全局变量,且定义时赋予"非0值"给该变量进行初始化。

    ZI-data:Zero Initialie data,即0初始化数据,它指初始化为"0值"的可读写数据域,它与RW-data的区别是程序刚运行时这些数据初始值全都为0,而后续运行过程与RW-data的性质一样,它们也常驻在RAM区,因而应用程序可以更改其内容。例如C语言中使用定义的全局变量,且定义时赋予"0值"给该变量进行初始化(若定义该变量时没有赋予初始值,编译器会把它当ZI-data来对待,初始化为0);

    ZI-data的栈空间(Stack)及堆空间(Heap):在C语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用malloc动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于ZI-data区域的,这些空间都会被初始值化为0值。编译器给出的ZI-data占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用malloc动态申请堆空间,编译器会优化,不把堆空间计算在内)。

综上所述,以程序的组成构件为例,它们所属的区域类别见表 481。

表 481 程序组件所属的区域

程序组件

所属类别

机器代码指令

Code

常量

RO-data

初值非0的全局变量

RW-data

初值为0的全局变量

ZI-data

局部变量

ZI-data栈空间

使用malloc动态分配的空间

ZI-data堆空间

48.2.2 程序的存储与运行

RW-data和ZI-data它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?这就涉及到程序的存储状态了,应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如STM32的内部FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的,见图 485。

图 485 应用程序的加载视图与执行视图

图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是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。应用程序在各个状态时各区域的组成见表 482。

表 482 程序状态区域的组成

程序状态与区域

组成

程序执行时的只读区域(RO)

Code RO data

程序执行时的可读写区域(RW)

RW data ZI data

程序存储时占用的ROM区

Code RO data RW data

在MDK中,我们建立的工程一般会选择芯片型号,选择后就有确定的FLASH及SRAM大小,若代码超出了芯片的存储器的极限,编译器会提示错误,这时就需要裁剪程序了,裁剪时可针对超出的区域来优化。

48.3 编译工具链

在前面编译过程中,MDK调用了各种编译工具,平时我们直接配置MDK,不需要学习如何使用它们,但了解它们是非常有好处的。例如,若希望使用MDK编译生成bin文件的,需要在MDK中输入指令控制fromelf工具;在本章后面讲解AXF及O文件的时候,需要利用fromelf工具查看其文件信息,这都是无法直接通过MDK做到的。关于这些工具链的说明,在MDK的帮助手册《ARM Development Tools》都有详细讲解,点击MDK界面的"help->uVision Help"菜单可打开该文件。

48.3.1 设置环境变量

调用这些编译工具,需要用到Windows的"命令行提示符工具",为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。查看本机工具链所在的具体目录可根据上一小节讲解的工程编译提示输出信息中找到,如本机的路径为"D:workkeil5ARMARMCCbin"。

1.    添加路径到PATH环境变量

本文以Win7系统为例添加工具链的路径到PATH环境变量,其它系统是类似的。

(1)    右键电脑系统的"计算机图标",在弹出的菜单中选择"属性",见图 486;

图 486 计算机属性页面

(2)    在弹出的属性页面依次点击"高级系统设置"->"环境变量",在用户变量一栏中找到名为"PATH"的变量,若没有该变量,则新建一个。编辑"PATH"变量,在它的变量值中输入工具链的路径,如本机的是";D:workkeil5ARMARMCCbin",注意要使用"分号;"让它与其它路径分隔开,输入完毕后依次点确定,见图 487;

图 487 添加工具链路径到PATH变量

(3)    打开Windows的命令行,点击系统的"开始菜单",在搜索框输入"cmd",在搜索结果中点击"cmd.exe"即可打开命令行,见图 488;

图 488 打开命令行

(4)    在弹出的命令行窗口中输入"fromelf"回车,若窗口打印出formelf的帮助说明,那么路径正常,就可以开始后面的工作了;若提示"不是内部名外部命令,也不是可运行的程序…"信息,说明路径不对,请重新配置环境变量,并确认该工作目录下有编译工具链。

这个过程本质就是让命令行通过"PATH"路径找到"fromelf.exe"程序运行,默认运行"fromelf.exe"时它会输出自己的帮助信息,这就是工具链的调用过程,MDK本质上也是如此调用工具链的,只是它集成为GUI,相对于命令行对用户更友好,毕竟上述配置环境变量的过程已经让新手烦躁了。

48.3.2 armcc、armasm及armlink

接下来我们看看各个工具链的具体用法,主要以armcc为例。

1.    armcc

armcc用于把c/c 文件编译成ARM指令代码,编译后会输出ELF格式的O文件(对象、目标文件),在命令行中输入"armcc"回车可调用该工具,它会打印帮助说明,见图 489

图 489 armcc的帮助提示

帮助提示中分三部分,第一部分是armcc版本信息,第二部分是命令的用法,第三部分是主要命令选项。

根据命令用法: armcc [options] file1 file2 ... filen ,在[option]位置可输入下面的"--arm"、"--cpu list"选项,若选项带文件输入,则把文件名填充在file1 file2…的位置,这些文件一般是c/c 文件。

例如根据它的帮助说明,"--cpu list"可列出编译器支持的所有cpu,我们在命令行中输入"armcc --cpu list",可查看图 4810中的cpu列表。

图 4810 cpulist

打开MDK的Options for Targe->c/c 菜单,可看到MDK对编译器的控制命令,见图 4811。

图 4811 MDK的ARMCC编译选项

从该图中的命令可看到,它调用了-c、-cpu –D –g –O1等编译选项,当我们修改MDK的编译配置时,可看到该控制命令也会有相应的变化。然而我们无法在该编译选项框中输入命令,只能通过MDK提供的选项修改。

了解这些,我们就可以查询具体的MDK编译选项的具体信息了,如c/c 选项中的"Optimization:Leve 1(-O1)"是什么功能呢?首先可了解到它是"-O"命令,命令后还带个数字,查看MDK的帮助手册,在armcc编译器说明章节,可详细了解,如图 489。

图 4812 编译器选项说明

利用MDK,我们一般不需要自己调用armcc工具,但经过这样的过程我们就会对MDK有更深入的认识,面对它的各种编译选项,就不会那么头疼了。

2.    armasm

armasm是汇编器,它把汇编文件编译成O文件。与armcc类似,MDK对armasm的调用选项可在"Option for Target->Asm"页面进行配置,见图 4813。

图 4813 armasm与MDK的编译选项

3.    armlink

armlink是链接器,它把各个O文件链接组合在一起生成ELF格式的AXF文件,AXF文件是可执行的,下载器把该文件中的指令代码下载到芯片后,该芯片就能运行程序了;利用armlink还可以控制程序存储到指定的ROM或RAM地址。在MDK中可在"Option for Target->Linker"页面配置armlink选项,见图 4814。

图 4814 armlink与MDK的配置选项

链接器默认是根据芯片类型的存储器分布来生成程序的,该存储器分布被记录在工程里的sct后缀的文件中,有特殊需要的话可自行编辑该文件,改变链接器的链接方式,具体后面我们会详细讲解。

48.3.3 armar、fromelf及用户指令

armar工具用于把工程打包成库文件,fromelf可根据axf文件生成hex、bin文件,hex和bin文件是大多数下载器支持的下载文件格式。

在MDK中,针对armar和fromelf工具的选项几乎没有,仅集成了生成HEX或Lib的选项,见图 4815。

图 4815 MDK中,控制fromelf生成hex及控制armar生成lib的配置

例如如果我们想利用fromelf生成bin文件,可以在MDK的"Option for Target->User"页中添加调用fromelf的指令,见图 4816。

图 4816 在MDK中添加指令

在User配置页面中,提供了三种类型的用户指令输入框,在不同组的框输入指令,可控制指令的执行时间,分别是编译前(Before Compile c/c file)、构建前(Before Build/Rebuild)及构建后(After Build/Rebuild)执行。这些指令并没有限制必须是arm的编译工具链,例如如果您自己编写了python脚本,也可以在这里输入用户指令执行该脚本。

图中的生成bin文件指令调用了fromelf工具,紧跟后面的是工具的选项及输出文件名、输入文件名。由于fromelf是根据axf文件生成bin的,而axf文件又是构建(build)工程后才生成,所以我们把该指令放到"After Build/Rebuild"一栏。

48.4 MDK工程的文件类型

除了上述编译过程生成的文件,MDK工程中还包含了各种各样的文件,下面我们统一介绍,MDK工程的常见文件类型见表 483。

表 483 MDK常见的文件类型(不分大小写)

后缀

说明

Project目录下的工程文件

*.uvguix

MDK5工程的窗口布局文件,在MDK4中*.UVGUI后缀的文件功能相同

*.uvprojx

MDK5的工程文件,它使用了XML格式记录了工程结构,双击它可以打开整个工程,在MDK4中*.UVPROJ后缀的文件功能相同

*.uvoptx

MDK5的工程配置选项,包含debugger、trace configuration、breakpooints以及当前打开的文件,在MDK4中*.UVOPT后缀的文件功能相同

*.ini

某些下载器的配置记录文件

源文件

*.c

C语言源文件

*.cpp

C 语言源文件

*.h

C/C 的头文件

*.s

汇编语言的源文件

*.inc

汇编语言的头文件(使用"$include"来包含)

Output目录下的文件

*.lib

库文件

*.dep

整个工程的依赖文件

*.d

描述了对应.o的依赖的文件

*.crf

交叉引用文件,包含了浏览信息(定义、引用及标识符)

*.o

可重定位的对象文件(目标文件)

*.bin

二进制格式的映像文件,是纯粹的FLASH映像,不含任何额外信息

*.hex

Intel Hex格式的映像文件,可理解为带存储地址描述格式的bin文件

*.elf

由GCC编译生成的文件,功能跟axf文件一样,该文件不可重定位

*.axf

由ARMCC编译生成的可执行对象文件,可用于调试,该文件不可重定位

*.sct

链接器控制文件(分散加载)

*.scr

链接器产生的分散加载文件

*.lnp

MDK生成的链接输入文件,用于调用链接器时的命令输入

*.htm

链接器生成的静态调用图文件

*.build_log.htm

构建工程的日志记录文件

Listing目录下的文件

*.lst

C及汇编编译器产生的列表文件

*.map

链接器生成的列表文件,包含存储器映像分布

其它

*.ini

仿真、下载器的脚本文件

这些文件主要分为MDK相关文件、源文件以及编译、链接器生成的文件。我们以"多彩流水灯"工程为例讲解各种文件的功能。

48.4.1 uvprojx、uvoptx、uvguix及ini工程文件

在工程的"Project"目录下主要是MDK工程相关的文件,见图 4817。

图 4817 Project目录下的uvprojx、uvoptx、uvguix及ini文件

1.    uvprojx文件

uvprojx文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包含了哪些源文件等内容,见图 4818。

图 4818 工程包含的文件、芯片类型等内容

2.    uvoptx文件

uvoptx文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开的文件等等,见图 4819。

图 4819 代码编辑器中已打开的文件

3.    uvguix文件

uvguix文件记录了MDK软件的GUI布局,如代码编辑区窗口的大小、编译输出提示窗口的位置等等。

图 4820 记录MDK工作环境中各个窗口的大小

uvprojx、uvoptx及uvguix都是使用XML格式记录的文件,若使用记事本打开可以看到XML代码,见图 4817。而当使用MDK软件打开时,它根据这些文件的XML记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。

图 4821 使用记事本打开uvprojx、uvoptx及uvguix文件可看到XML格式的记录

这些工程参数都是当MDK正常退出时才会被写入保存,所以若MDK错误退出时(如使用Windows的任务管理器强制关闭),工程配置参数的最新更改是不会被记录的,重新打开工程时要再次配置。根据这几个文件的记录类型,可以知道uvprojx文件是最重要的,删掉它我们就无法再正常打开工程了,而uvoptx及uvguix文件并不是必须的,可以删除,重新使用MDK打开uvprojx工程文件后,会以默认参数重新创建uvoptx及uvguix文件。(所以当使用Git/SVN等代码管理的时候,往往只保留uvprojx文件)

48.4.2 源文件

源文件是工程中我们最熟悉的内容了,它们就是我们编写的各种源代码,MDK支持c、cpp、h、s、inc类型的源代码文件,其中c、cpp分别是c/c 语言的源代码,h是它们的头文件,s是汇编文件,inc是汇编文件的头文件,可使用"$include"语法包含。编译器根据工程中的源文件最终生成机器码。

48.4.3 Output目录下生成的文件

点击MDK中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的"Options for Targe->Output->Select Folder for Objects"和"Options for Targe->Listing->Select Folder for Listings"选项配置它们的输出路径,见图 4822和图 4823。

图 4822 设置Output输出路径

图 4823设置Listing输出路径

编译后Output和Listing目录下生成的文件见图 4824。

图 4824 编译后Output及Listing文件夹中的内容

接下来我们讲解Output路径下的文件。

1.    lib库文件

在某些场合下我们希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成lib文件(Library file)提供给对方,在MDK中可配置"Options for Target->Create Library"选项把工程编译成库文件,见图 4825。

图 4825 生成库文件或可执行文件

工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行文件即我们下载到芯片上直接运行的机器码。

得到生成的*.lib文件后,可把它像C文件一样添加到其它工程中,并在该工程调用lib提供的函数接口,除了不能看到*.lib文件的源码,在应用方面它跟C源文件没有区别。

2.    dep、d依赖文件

*.dep和*.d文件(Dependency file)记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中*.dep是整个工程的依赖,它以工程名命名,而*.d是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开,见图 4826和图 4827。

图 4826 工程的dep文件内容

图 4827 bsp_led.d文件的内容

3.    crf交叉引用文件

*.crf是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。

我们在代码编辑器中点击"Go To Definition Of 'xxxx'"可实现浏览跳转,见图 4828,跳转的时候,MDK就是通过*.crf文件查找出跳转位置的。

图 4828 浏览信息

通过配置MDK中的"Option for Target->Output->Browse Information"选项可以设置编译时是否生成浏览信息,见图 4829。只有勾选该选项并编译后,才能实现上面的浏览跳转功能。

图 4829 在Options forTarget中设置是否生成浏览信息

*.crf文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码,见图 4830,我们不作深入研究。

图 4830 crf文件内容

4.    o、axf及elf文件

*.o、*.elf、*.axf、*.bin及*.hex文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,它们又有所区别。

ELF文件说明

*.o、*.elf、*.axf以及前面提到的lib文件都是属于目标文件,它们都是使用ELF格式来存储的,关于ELF格式的详细内容请参考配套资料里的《ELF文件格式》文档了解,它讲解的是Linux下的ELF格式,与MDK使用的格式有小区别,但大致相同。在本教程中,仅讲解ELF文件的核心概念。

ELF是Executable and Linking Format的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在Linux及Windows系统下都有使用该格式的文件(或类似格式)用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。

目标文件主要有如下三种类型:

(1)    可重定位的文件(Relocatable File),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。

例如MDK的armcc和armasm生成的*.o文件就是这一类,另外还有Linux的*.o 文件,Windows的 *.obj文件。

(2)    可执行文件(Executable File),它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。

例如MDK的armlink生成的*.elf及*.axf文件,(使用gcc编译工具可生成*.elf文件,用armlink生成的是*.axf文件,*.axf文件在*.elf之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解*.axf文件),另外还有Linux的/bin/bash文件,Windows的*.exe文件。

(3)    共享目标文件(Shared Object File), 它的定义比较难理解,我们直接举例,MDK生成的*.lib文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。另外,Linux的.so,如/lib/ glibc-2.5.so,Windows的DLL都属于这一类。

o文件与axf文件的关系

根据上面的分类,我们了解到,*.axf文件是由多个*.o文件链接而成的,而*.o文件由相应的源文件编译而成,一个源文件对应一个*.o文件。它们的关系见图 4831。

图 4831*.axf文件与*.o文件的关系

图中的中间代表的是armlink链接器,在它的右侧是输入链接器的*.o文件,左侧是它输出的*axf文件。

可以看到,由于都使用ELF文件格式,*.o与*.axf文件的结构是类似的,它们包含ELF文件头、程序头、节区(section)以及节区头部表。各个部分的功能说明如下:

    ELF文件头用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头在文件中的位置等。

    程序头告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。MDK的可重定位文件*.o不包含这部分内容,因为它还不是可执行文件,而armlink输出的*.axf文件就包含该内容了。

    节区是*.o文件的独立数据区域,它包含提供给链接视图使用的大量信息,如指令(Code)、数据(RO、RW、ZI-data)、符号表(函数、变量名等)、重定位信息等,例如每个由C语言定义的函数在*.o文件中都会有一个独立的节区;

    存储在最后的节区头则包含了本文件节区的信息,如节区名称、大小等等。

总的来说,链接器把各个*.o文件的节区归类、排列,根据目标器件的情况编排地址生成输出,汇总到*.axf文件。例如,见图 4832,"多彩流水灯"工程中在"bsp_led.c"文件中有一个LED_GPIO_Config函数,而它内部调用了"stm32f4xx_gpio.c"的GPIO_Init函数,经过armcc编译后,LED_GPIO_Config及GPIO_Iint函数都成了指令代码,分别存储在bsp_led.o及stm32f4xx_gpio.o文件中,这些指令在*.o文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。

图 4832 具体的链接过程

ELF文件头

接下来我们看看具体文件的内容,使用fromelf文件可以查看*.o、*.axf及*.lib文件的ELF信息。

使用命令行,切换到文件所在的目录,输入"fromelf –text –v bsp_led.o"命令,可控制输出bsp_led.o的详细信息,见图 4833。利用"-c、-z"等选项还可输出反汇编指令文件、代码及数据文件等信息,请亲手尝试一下。

图 4833 使用fromelf查看o文件信息

为了便于阅读,我已使用fromelf指令生成了"多彩流水灯.axf"、"bsp_led"及"多彩流水灯.lib"的ELF信息,并已把这些信息保存在独立的文件中,在配套资料的"elf信息输出"文件夹下可查看,见表 484。

表 484 配套资料里使用fromelf生成的文件

fromelf选项

可查看的信息

生成到配套资料里相应的文件

-v

详细信息

bsp_led_o_elfInfo_v.txt/多彩流水灯_axf_elfInfo_v.txt

-a

数据的地址

bsp_led_o_elfInfo_a.txt/多彩流水灯_axf_elfInfo_a.txt

-c

反汇编代码

bsp_led_o_elfInfo_c.txt/多彩流水灯_axf_elfInfo_c.txt

-d

data section的内容

bsp_led_o_elfInfo_d.txt/多彩流水灯_axf_elfInfo_d.txt

-e

异常表

bsp_led_o_elfInfo_e.txt/多彩流水灯_axf_elfInfo_e.txt

-g

调试表

bsp_led_o_elfInfo_g.txt/多彩流水灯_axf_elfInfo_g.txt

-r

重定位信息

bsp_led_o_elfInfo_r.txt/多彩流水灯_axf_elfInfo_r.txt

-s

符号表

bsp_led_o_elfInfo_s.txt/多彩流水灯_axf_elfInfo_s.txt

-t

字符串表

bsp_led_o_elfInfo_t.txt/多彩流水灯_axf_elfInfo_t.txt

-y

动态段内容

bsp_led_o_elfInfo_y.txt/多彩流水灯_axf_elfInfo_y.txt

-z

代码及数据的大小信息

bsp_led_o_elfInfo_z.txt/多彩流水灯_axf_elfInfo_z.txt

直接打开"elf信息输出"目录下的bsp_led_o_elfInfo_v.txt文件,可看到代码清单 481中的内容。

代码清单 481 bsp_led.o文件的ELF文件头(可到"bsp_led_o_elfInfo_v.txt"文件查看)

1 ========================================================================

2

3 ** ELF Header Information

4

5 File Name:

6 .bsp_led.o         //bsp_led.o文件

7

8 Machine class: ELFCLASS32 (32-bit) //32位机

9 Data encoding: ELFDATA2LSB (Little endian) //小端格式

10 Header version: EV_CURRENT (Current version)

11 Operating System ABI: none

12 ABI Version: 0

13 File Type: ET_REL (Relocatable object) (1) //可重定位文件类型

14 Machine: EM_ARM (ARM)

15

16 Entry offset (in SHF_ENTRYSECT section): 0x00000000

17 Flags: None (0x05000000)

18

19 ARM ELF revision: 5 (ABI version 2)

20

21 Built with

22 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]

23 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]

24

25 Header size: 52 bytes (0x34)

26 Program header entry size: 0 bytes (0x0)    //程序头大小

27 Section header entry size: 40 bytes (0x28)

28

29 Program header entries: 0

30 Section header entries: 246

31

32 Program header offset: 0 (0x00000000) //程序头在文件中的位置(没有程序头)

33 Section header offset: 507224 (0x0007bd58) //节区头在文件中的位置

34

35 Section header string table index: 243

36

37 =====================================================================

    在上述代码中已加入了部分注释,解释了相应项的意义,值得一提的是在这个*.o文件中,它的ELF文件头中告诉我们它的程序头(Program header)大小为"0 bytes",且程序头所在的文件位置偏移也为"0",这说明它是没有程序头的。

程序头

    接下来打开"多彩流水灯_axf_elfInfo_v.txt"文件,查看工程的*.axf文件的详细信息,见代码清单 482。

代码清单 482 *.axf文件中的elf文件头及程序头(可到"多彩流水灯_axf_elfInfo_v.txt"文件查看)

1 ===================================================================

2

3 ** ELF Header Information

4

5 File Name:

6 .多彩流水灯.axf //多彩流水灯.axf 文件

7

8 Machine class: ELFCLASS32 (32-bit) //32位机

9 Data encoding: ELFDATA2LSB (Little endian) //小端格式

10 Header version: EV_CURRENT (Current version)

11 Operating System ABI: none

12 ABI Version: 0

13 File Type: ET_EXEC (Executable) (2) //可执行文件类型

14 Machine: EM_ARM (ARM)

15

16 Image Entry point: 0x080001ad

17 Flags: EF_ARM_HASENTRY EF_ARM_ABI_FLOAT_SOFT (0x05000202)

18

19 ARM ELF revision: 5 (ABI version 2)

20

21 Conforms to Soft float procedure-call standard

22

23 Built with

24 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]

25 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]

26

27 Header size: 52 bytes (0x34)

28 Program header entry size: 32 bytes (0x20)

29 Section header entry size: 40 bytes (0x28)

30

31 Program header entries: 1

32 Section header entries: 15

33

34 Program header offset: 335252 (0x00051d94) //程序头在文件中的位置

35 Section header offset: 335284 (0x00051db4) //节区头在文件中的位置

36

37 Section header string table index: 14

38

39 =================================================================

40

41 ** Program header #0

42

43 Type : PT_LOAD (1) //表示这是可加载的内容

44 File Offset : 52 (0x34) //在文件中的偏移

45 Virtual Addr : 0x08000000 //虚拟地址(此处等于物理地址)

46 Physical Addr : 0x08000000 //物理地址

47 Size in file : 1456 bytes (0x5b0) //程序在文件中占据的大小

48 Size in memory: 2480 bytes (0x9b0) //若程序加载到内存,占据的内存空间

49 Flags : PF_X PF_W PF_R PF_ARM_ENTRY (0x80000007)

50 Alignment : 8 //地址对齐

51

52

53 ===============================================================

对比之下,可发现*.axf文件的ELF文件头对程序头的大小说明为非0值,且给出了它在文件的偏移地址,在输出信息之中,包含了程序头的详细信息。可看到,程序头的"Physical Addr"描述了本程序要加载到的内存地址"0x0800 0000",正好是STM32内部FLASH的首地址;"size in file"描述了本程序占据的空间大小为"1456 bytes",它正是程序烧录到FLASH中需要占据的空间。

节区头

在ELF的原文件中,紧接着程序头的一般是节区的主体信息,在节区主体信息之后是描述节区主体信息的节区头,我们先来看看节区头中的信息了解概况。通过对比*.o文件及*.axf文件的节区头部信息,可以清楚地看出这两种文件的区别,见代码清单 483。

代码清单 483 *.o文件的节区信息("bsp_led_o_elfInfo_v.txt"文件)

1 ====================================

2 ** Section #4

3

4 Name : i.LED_GPIO_Config //节区名

6

7 //此节区包含程序定义的信息,其格式和含义都由程序来解释。

8 Type : SHT_PROGBITS (0x00000001)

10

11 //此节区在进程执行过程中占用内存。节区包含可执行的机器指令。

12 Flags :SHF_ALLOC SHF_EXECINSTR (0x00000006)

14 Addr : 0x00000000 //地址

15 File Offset : 68 (0x44)        //在文件中的偏移

16 Size : 116 bytes (0x74) //大小

17 Link : SHN_UNDEF

19 Info : 0

20 Alignment : 4 //字节对齐

21 Entry Size : 0

22 ====================================

这个节区的名称为LED_GPIO_Config,它正好是我们在bsp_led.c文件中定义的函数名,这个节区头描述的是该函数被编译后的节区信息,其中包含了节区的类型(指令类型)、节区应存储到的地址(0x00000000)、它主体信息在文件位置中的偏移(68)以及节区的大小(116 bytes)。

由于*.o文件是可重定位文件,所以它的地址并没有被分配,是0x00000000(假如文件中还有其它函数,该函数生成的节区中,对应的地址描述也都是0)。当链接器链接时,根据这个节区头信息,在文件中找到它的主体内容,并根据它的类型,把它加入到主程序中,并分配实际地址,链接后生成的*.axf文件,我们再来看看它的内容,见代码清单 484。

代码清单 484 *.axf文件的节区信息("多彩流水灯_axf_elfInfo_v.txt"文件)

1 ========================================================================

2 ** Section #1

3

4 Name : ER_IROM1 //节区名

5

6 //此节区包含程序定义的信息,其格式和含义都由程序来解释。

7 Type : SHT_PROGBITS (0x00000001)

8

9 //此节区在进程执行过程中占用内存。节区包含可执行的机器指令

10 Flags : SHF_ALLOC SHF_EXECINSTR (0x00000006)

11 Addr : 0x08000000 //地址

12 File Offset : 52 (0x34)

13 Size : 1456 bytes (0x5b0) //大小

14 Link : SHN_UNDEF

15 Info : 0

16 Alignment : 4

17 Entry Size : 0

18

19 ====================================

20 ** Section #2

21

22 Name : RW_IRAM1 //节区名

23

24 //包含将出现在程序的内存映像中的为初始

25 //化数据。根据定义,当程序开始执行,系统

26 //将把这些数据初始化为 0。

27 Type : SHT_NOBITS (0x00000008)

28

29 //此节区在进程执行过程中占用内存。节区包含进程执行过程中将可写的数据。

30 Flags : SHF_ALLOC SHF_WRITE (0x00000003)

31 Addr : 0x20000000 //地址

32 File Offset : 1508 (0x5e4)

33 Size : 1024 bytes (0x400) //大小

34 Link : SHN_UNDEF

35 Info : 0

36 Alignment : 8

37 Entry Size : 0

38 ====================================

在*.axf文件中,主要包含了两个节区,一个名为ER_IROM1,一个名为RW_IRAM1,这些节区头信息中除了具有*.o文件中节区头描述的节区类型、文件位置偏移、大小之外,更重要的是它们都有具体的地址描述,其中 ER_IROM1的地址为0x08000000,而RW_IRAM1的地址为0x20000000,它们正好是内部FLASH及SRAM的首地址,对应节区的大小就是程序需要占用FLASH及SRAM空间的实际大小。

也就是说,经过链接器后,它生成的*.axf文件已经汇总了其它*.o文件的所有内容,生成的ER_IROM1节区内容可直接写入到STM32内部FLASH的具体位置。例如,前面*.o文件中的i.LED_GPIO_Config节区已经被加入到*.axf文件的ER_IROM1节区的某地址。

节区主体及反汇编代码

使用fromelf的-c选项可以查看部分节区的主体信息,对于指令节区,可根据其内容查看相应的反汇编代码,打开"bsp_led_o_elfInfo_c.txt"文件可查看这些信息,见代码清单 485。

代码清单 485 *.o文件的LED_GPIO_Config节区及反汇编代码(bsp_led_o_elfInfo_c.txt文件)

1 ** Section #4 'i.LED_GPIO_Config' (SHT_PROGBITS) [SHF_ALLOC SHF_EXECINSTR]

2 Size : 116 bytes (alignment 4)

3 Address: 0x00000000

4

5 $t

6 i.LED_GPIO_Config

7 LED_GPIO_Config

8 // 地址内容 (ASCII码) 内容对应的代码

9 // (无意义)

10 0x00000000: e92d41fc -..A PUSH {r2-r8,lr}

11 0x00000004: 2101 .! MOVS r1,#1

12 0x00000006: 2088 . MOVS r0,#0x88

13 0x00000008: f7fffffe .... BL RCC_AHB1PeriphClockCmd

14 0x0000000c: f44f6580 O..e MOV r5,#0x400

15 0x00000010: 9500 .. STR r5,[sp,#0]

16 0x00000012: 2101 .! MOVS r1,#1

17 0x00000014: f88d1004 .... STRB r1,[sp,#4]

18 0x00000018: 2000 . MOVS r0,#0

19 0x0000001a: f88d0006 .... STRB r0,[sp,#6]

20 0x0000001e: f88d1007 .... STRB r1,[sp,#7]

21 0x00000022: f88d0005 .... STRB r0,[sp,#5]

22 0x00000026: 4f11 .O LDR r7,[pc,#68] ;

23 0x00000028: 4669 iF MOV r1,sp

24 0x0000002a: 4638 8F MOV r0,r7

25 0x0000002c: f7fffffe .... BL GPIO_Init

26 0x00000030: 006c l. LSLS r4,r5,#1

27 /*....以下省略**/

可看到,由于这是*.o文件,它的节区地址还是没有分配的,基地址为0x00000000,接着在LED_GPIO_Config标号之后,列出了一个表,表中包含了地址偏移、相应地址中的内容以及根据内容反汇编得到的指令。细看汇编指令,还可看到它包含了跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的语句,而且这两个跳转语句原来的内容都是"f7fffffe",这是因为还*.o文件中并没有RCC_AHB1PeriphClockCmd及GPIO_Init标号的具体地址索引,在*.axf文件中,这是不一样的。

接下来我们打开"多彩流水灯_axf_elfInfo_c.txt"文件,查看*.axf文件中,ER_IROM1节区中对应LED_GPIO_Config的内容,见代码清单 486。

代码清单 486*.axf文件的LED_GPIO_Config反汇编代码(多彩流水灯_axf_elfInfo_c.txt文件)

1 i.LED_GPIO_Config

2 LED_GPIO_Config

3 0x080002a4: e92d41fc -..A PUSH {r2-r8,lr}

4 0x080002a8: 2101 .! MOVS r1,#1

5 0x080002aa: 2088 . MOVS r0,#0x88

6 0x080002ac: f000f838 ..8. BL RCC_AHB1PeriphClockCmd ; 0x8000320

7 0x080002b0: f44f6580 O..e MOV r5,#0x400

8 0x080002b4: 9500 .. STR r5,[sp,#0]

9 0x080002b6: 2101 .! MOVS r1,#1

10 0x080002b8: f88d1004 .... STRB r1,[sp,#4]

11 0x080002bc: 2000 . MOVS r0,#0

12 0x080002be: f88d0006 .... STRB r0,[sp,#6]

13 0x080002c2: f88d1007 .... STRB r1,[sp,#7]

14 0x080002c6: f88d0005 .... STRB r0,[sp,#5]

15 0x080002ca: 4f11 .O LDR r7,[pc,#68] ; [0x8000310] = 0x40021c00

16 0x080002cc: 4669 iF MOV r1,sp

17 0x080002ce: 4638 8F MOV r0,r7

18 0x080002d0: f7ffffa5 .... BL GPIO_Init ; 0x800021e

19 0x080002d4: 006c l. LSLS r4,r5,#1

20 /*....以下省略**/

可看到,除了基地址以及跳转地址不同之外,LED_GPIO_Config中的内容跟*.o文件中的一样。另外,由于*.o是独立的文件,而*.axf是整个工程汇总的文件,所以在*.axf中包含了所有调用到*.o文件节区的内容。例如,在"bsp_led_o_elfInfo_c.txt"(bsp_led.o文件的反汇编信息)中不包含RCC_AHB1PeriphClockCmd及GPIO_Init的内容,而在"多彩流水灯_axf_elfInfo_c.txt" (多彩流水灯.axf文件的反汇编信息)中则可找到它们的具体信息,且它们也有具体的地址空间。

在*.axf文件中,跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的这两个指令后都有注释,分别是"; 0x8000320"及"; 0x800021e",它们是这两个标号所在的具体地址,而且这两个跳转语句的跟*.o中的也有区别,内容分别为"f000f838e"及"f7ffffa5"(*.o中的均为f7fffffe)。这就是链接器链接的含义,它把不同*.o中的内容链接起来了。

分散加载代码

学习至此,还有一个疑问,前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在FLASH中的RW-data数据拷贝至SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?

通过查看"多彩流水灯_axf_elfInfo_c.txt"的反汇编信息,了解到程序中具有一段名为"__scatterload"的分散加载代码,见代码清单 487,它是由armlink链接器自动生成的。

代码清单 487 分散加载代码(多彩流水灯_axf_elfInfo_c.txt文件)

1

2 .text

3 __scatterload

4 __scatterload_rt2

5 0x080001e4: 4c06 .L LDR r4,[pc,#24] ; [0x8000200] = 0x80005a0

6 0x080001e6: 4d07 .M LDR r5,[pc,#28] ; [0x8000204] = 0x80005b0

7 0x080001e8: e006 .. B 0x80001f8 ; __scatterload 20

8 0x080001ea: 68e0 .h LDR r0,[r4,#0xc]

9 0x080001ec: f0400301 @... ORR r3,r0,#1

10 0x080001f0: e8940007 .... LDM r4,{r0-r2}

11 0x080001f4: 4798 .G BLX r3

12 0x080001f6: 3410 .4 ADDS r4,r4,#0x10

13 0x080001f8: 42ac .B CMP r4,r5

14 0x080001fa: d3f6 .. BCC 0x80001ea ; __scatterload 6

15 0x080001fc: f7ffffda .... BL __main_after_scatterload ; 0x80001b4

16 $d

17 0x08000200: 080005a0 .... DCD 134219168

18 0x08000204: 080005b0 .... DCD 134219184

这段分散加载代码包含了拷贝过程(LDM复制指令),而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据。而"__scatterload "的代码会被"__main"函数调用,见代码清单 488,__main在启动文件中的"Reset_Handler"会被调用,因而,在主体程序执行前,已经完成了分散加载过程。

代码清单 488 __main的反汇编代码(部分,多彩流水灯_axf_elfInfo_c.txt文件)

1 __main

2 _main_stk

3 0x080001ac: f8dfd00c .... LDR sp,__lit__00000000 ; [0x80001bc] = 0x20000400

4 .ARM.Collect$$$$00000004

5 _main_scatterload

6 0x080001b0: f000f818 .... BL __scatterload ; 0x80001e4

5.    hex文件及bin文件

若编译过程无误,即可把工程生成前面对应的*.axf文件,而在MDK中使用下载器(DAP/JLINK/ULINK等)下载程序或仿真的时候,MDK调用的就是*.axf文件,它解释该文件,然后控制下载器把*.axf中的代码内容下载到STM32芯片对应的存储空间,然后复位后芯片就开始执行代码了。

然而,脱离了MDK或IAR等工具,下载器就无法直接使用*.axf文件下载代码了,它们一般仅支持hex和bin格式的代码数据文件。默认情况下MDK都不会生成hex及bin文件,需要配置工程选项或使用fromelf命令。

生成hex文件

生成hex文件的配置比较简单,在"Options for Target->Output->Create Hex File"中勾选该选项,然后编译工程即可,见图 4834。

图 4834 生成hex文件的配置

生成bin文件

使用MDK生成bin文件需要使用fromelf命令,在MDK的"Options For Target->Users"中加入图 4835中的命令。

图 4835 使用fromelf指令生成bin文件

图中的指令内容为:

"fromelf --bin --output ....Output多彩流水灯.bin ....Output多彩流水灯.axf"

该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看fromelf的帮助,见图 4836。

图 4836 fromelf的帮助

我们在MDK输入的指令格式是遵守fromelf帮助里的指令格式说明的,其格式为:

"fromelf [options] input_file"

其中optinos是指令选项,一个指令支持输入多个选项,每个选项之间使用空格隔开,我们的实例中使用"--bin"选项设置输出bin文件,使用"--output file"选项设置输出文件的名字为"....Output多彩流水灯.bin",这个名字是一个相对路径格式,如果不了解如何使用".."表示路径,可使用MDK命令输入框后面的文件夹图标打开文件浏览器选择文件,在命令的最后使用"....Output多彩流水灯.axf"作为命令的输入文件。具体的格式分解见图 4837。

图 4837 fromelf命令格式分解

fromelf需要根据工程的*.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的*.axf文件,在MDK命令输入栏中,我们把fromelf指令放置在"After Build/Rebuild"(工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。

设置完成生成hex的选项或添加了生成bin的用户指令后,点击工程的编译(build)按钮,重新编译工程,成功后可看到图 4838中的输出。打开相应的目录即可找到文件,若找不到bin文件,请查看提示输出栏执行指令的信息,根据信息改正fromelf指令。

图 4838 fromelf生成hxe及bin文件的提示

其中bin文件是纯二进制数据,无特殊格式,接下来我们了解一下hex文件格式。

hex文件格式

hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。

一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如":llaaaatt[dd…]cc",例如本"多彩流水灯"工程生成的hex文件前几条记录见代码清单 489。

代码清单 489 Hex文件实例(多彩流水灯.hex文件,可直接用记事本打开)

1 :020000040800F2

2 :1000000000040020C10100081B030008A30200082F

3 :100010001903000809020008690400080000000034

4 :100020000000000000000000000000003D03000888

5 :100030000B020008000000001D0300081504000862

6 :10004000DB010008DB010008DB010008DB01000820

记录的各个部分介绍如下:

    ":":每条记录的开头都使用冒号来表示一条记录的开始;

    ll:以16进制数表示这条记录的主体数据区的长度(即后面[dd…]的长度);

    aaaa:表示这条记录中的内容应存放到FLASH中的起始地址;

    tt:表示这条记录的类型,它包含中的各种类型;

表 485 tt值所代表的类型说明

tt的值

代表的类型

00

数据记录

01

本文件结束记录

02

扩展地址记录

04

扩展线性地址记录(表示后面的记录按个这地址递增)

05

表示一个线性地址记录的起始(只适用于ARM)

    dd:表示一个字节的数据,一条记录中可以有多个字节数据,ll区表示了它有多少个字节的数据;

    cc:表示本条记录的校验和,它是前面所有16进制数据 (除冒号外,两个为一组)的和对256取模运算的结果的补码。

例如,代码清单 489中的第一条记录解释如下:

(1)    02:表示这条记录数据区的长度为2字节;

(2)    0000:表示这条记录要存储到的地址;

(3)    04:表示这是一条扩展线性地址记录;

(4)    0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高16位,与前面的"0000"结合在一起,表示要扩展的线性地址为"0x0800 0000",这正好是STM32内部FLASH的首地址;

(5)    F2:表示校验和,它的值为(0x02 0x00 0x00 0x04 0x08 0x00)%6的值再取补码。

再来看第二条记录:

(1)    10:表示这条记录数据区的长度为2字节;

(2)    0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的FLASH首地址为(0x0800 0000 0x0000);

(3)    00:表示这是一条数据记录,数据区的是地址;

(4)    00040020C10100081B030008A3020008:这是要按地址存储的数据;

(5)    2F:校验和

为了更清楚地对比bin、hex及axf文件的差异,我们来查看这些文件内部记录的信息来进行对比。

hex、bin及axf文件的区别与联系

bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。

    bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据),在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);

    hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;

    axf文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。

同一个工程生成的bin、hex及axf文件的大小见图 4839。

图 4839 同一个工程的bin、bex及axf文件大小

实际上,这个工程要烧写到FLASH的内容总大小为1456字节,然而在Windows中查看的bin文件却比它大( bin文件是FLASH的代码映像,大小应一致),这是因为Windows文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小,见图4840。

图 4840 bin文件大小

接下来我们打开本工程的"多彩流水灯.bin"、"多彩流水灯.hex"及由"多彩流水灯.axf"使用fromelf工具输出的反汇编文件"多彩流水灯_axf_elfInfo_c.txt"文件,清晰地对比它们的差异,见图 4841。如果您想要亲自阅读自己电脑上的bin文件,推荐使用sublime软件打开,它可以把二进制数以ASCII码呈现出来,便于阅读。

图 4841 同一个工程的bin、hex及axf文件对代码的记录

在"多彩流水灯_axf_elfInfo_c.txt"文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是*.axf文件的原始内容,但因为它是通过*.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。

在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。

由于文件中存储的都是机器码,见图 4842,该图是我根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。

图 4842 GPIO_Init函数的代码数据在三个文件中的表示

如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。

6.    htm静态调用图文件

在Output目录下,有一个以工程文件命名的后缀为*.bulid_log.htm及*.htm文件,如"多彩流水灯.bulid_log.htm"及"多彩流水灯.htm",它们都可以使用浏览器打开。其中*.build_log.htm是工程的构建过程日志,而*.htm是链接器生成的静态调用图文件。

在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。

例如图 4843是"多彩流水灯.htm"文件顶部的说明。

图 4843"多彩流水灯.htm"中的静态占用最深的栈空间说明

该文件说明了本工程的静态栈空间最大占用56字节(Maximum Stack Usage:56bytes),这个占用最深的静态调用为"main->LED_GPIO_Config->GPIO_Init"。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。

利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在STM32中可修改启动文件改变堆栈的大小;如果空间不足,可从该文件中了解到调用深度的信息,然后优化该代码。

注意:

查看了各个工程的静态调用图文件统计后,我们发现本书提供的一些比较大规模的工程例子,静态栈调用最大深度都已超出STM32启动文件默认的栈空间大小0x00000400,即1024字节,但在当时的调试过程中却没有发现错误,因此我们也没有修改栈的默认大小(有一些工程调试时已发现问题,它们的栈空间就已经被我们改大了),虽然这些工程实际运行并没有错误,但这可能只是因为它使用的栈溢出RAM空间恰好没被程序其它部分修改而已。所以,建议您在实际的大型工程应用中(特别是使用了各种外部库时,如Lwip/emWin/Fatfs等),要查看本静态调用图文件,了解程序的栈使用情况,给程序分配合适的栈空间。

0 人点赞