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

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

------(在arm9的开发中,这些东西都是我们自己搞定的,但是在windows上,IDE帮我们做好了,了解这些对深入开发是很有帮助的,在有arm9开发的基础上,下面的东西很容易理解,如果看不懂,证明你还没有入门。下面的是从world复制过来的,格式和博客不太兼容,所有开始以字母q的,是world中的 □ 字符)

本章参考资料: MDK 的帮助手册《ARM Development Tools》,点击 MDK 界面的
“help->uVision Help”菜单可打开该文件。关于 ELF 文件格式,参考配套资料里的《ELF
文件格式》文件。
在本章中讲解了非常多的文件类型,学习时请跟着教程的节奏,打开实际工程中的文
件来了解。
相信您已经非常熟练地使用 MDK 创建应用程序了,平时使用 MDK 编写源代码,然
后编译生成机器码,再把机器码下载到 STM32 芯片上运行,但是这个编译、下载的过程
MDK 究竟做了什么工作?它编译后生成的各种文件又有什么作用?本章节将对这些过程进
行讲解,了解编译及下载过程有助于理解芯片的工作原理,这些知识对制作 IAP(bootloader)
以及读写控制器内部 FLASH 的应用时非常重要。

 

 

编译过程生成的不同文件将在后面的小节详细说明,此处先抓住主要流程来理解。
(1) 编译, MDK 软件使用的编译器是 armcc 和 armasm,它们根据每个 c/c++和汇编源文件
编译成对应的以“.o”为后缀名的对象文件(Object Code,也称目标文件),其内容主要
是从源文件编译得到的机器码,包含了代码、数据以及调试使用的信息;
(2) 链接,链接器 armlink 把各个.o 文件及库文件链接成一个映像文件“.axf”或“.elf”;

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

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

 

 

(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”目录下找到由以上过程生成的各种
文件,见图 48-4。

 

 

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

程序的组成、存储与运行
CODE、 RO、 RW、 ZI Data 域及堆栈空间
在工程的编译提示输出信息中有一个语句“Program Size: Code=xx RO-data=xx RWdata=xx ZI-data=xx”,它说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据(包括代码)被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状
态,这些域的意义如下:
q Code:即代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 区。
q RO-data: Read Only data,即只读数据域,它指程序中用到的只读数据,这些数
据被存储在 ROM 区,因而程序不能修改其内容。例如 C 语言中 const 关键字定义
的变量就是典型的 RO-data(注:C语言中,const修饰的变量还是可以通过指针更改,c++中是不允许的)。
q RW-data: Read Write data,即可读写数据域,它指初始化为“非 0 值”的可读写
数据,程序刚运行时,这些数据具有非 0 的初始值,且运行的时候它们会常驻在
RAM 区,因而应用程序可以修改其内容。例如 C 语言中使用定义的全局变量,
且定义时赋予“非 0 值”给该变量进行初始化。
q ZI-data: Zero Initialie data,即 0 初始化数据,它指初始化为“0 值”的可读写数
据域,它与 RW-data 的区别是程序刚运行时这些数据初始值全都为 0,而后续运
行过程与 RW-data 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改
其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“0 值”给该变量进
行初始化(若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初
始化为 0);
q ZI-data 的栈空间(Stack)及堆空间(Heap):在 C 语言中,函数内部定义的局部变量
属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部
变量,归还内存空间。而使用 malloc 动态分配的变量属于堆空间。在程序中的栈
空间和堆空间都是属于 ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器
给出的 ZI-data 占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没
有使用 malloc 动态申请堆空间,编译器会优化,不把堆空间计算在内)。

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

 

 

图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是 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 系统。

 

 

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

设置环境变量
调用这些编译工具,需要用到 Windows 的“命令行提示符工具”,为了让命令行方便
地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。查看本机工具链所在
的具体目录可根据上一小节讲解的工程编译提示输出信息中找到,如本机的路径为
“D:\work\keil5\ARM\ARMCC\bin”。
1. 添加路径到 PATH 环境变量
本文以 Win7 系统为例添加工具链的路径到 PATH 环境变量,其它系统是类似的。
(1) 右键电脑系统的“计算机图标”,在弹出的菜单中选择“属性”,见图 48-6;

 

 

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

 

 

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

 

(4) 在弹出的命令行窗口中输入“fromelf”回车,若窗口打印出 formelf 的帮助说明,
那么路径正常,就可以开始后面的工作了;若提示“不是内部名外部命令,也不
是可运行的程序…”信息, 说明路径不对,请重新配置环境变量,并确认该工作
目录下有编译工具链。
这个过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”程序运行,默认运
行“fromelf.exe”时它会输出自己的帮助信息,这就是工具链的调用过程, MDK 本质上也
是如此调用工具链的,只是它集成为 GUI,相对于命令行对用户更友好,毕竟上述配置环
境变量的过程已经让新手烦躁了。

armcc、 armasm 及 armlink
略,在linux arm开发中已经熟悉过了。

补充MDK生成bin文件:

只能通过命令行:

 

命令行:#K\ARM\ARMCC\bin\fromelf.exe --bin -o #L.bin #L  (#L.bin可以改成@L.bin,主要是生成的路径不同,@生成在工程文件文件夹,#生成在输出文件文件夹里)

这样就可以生成bin文件了。

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

 

 

uvprojx、 uvoptx、 uvguix 及 ini 工程文件
在工程的“Project”目录下主要是 MDK 工程相关的文件,见图 48-17。

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

Output 目录下生成的文件
点击 MDK 中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列
表文件,在工程的“Options for Targe->Output->Select Folder for Objects”和“Options for
Targe->Listing->Select Folder for Listings”选项配置它们的输出路径,见图 48-22 和图 48-23。

 

 

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

 

 

工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行
文件即我们下载到芯片上直接运行的机器码。
得到生成的*.lib 文件后,可把它像 C 文件一样添加到其它工程中,并在该工程调用 lib提供的函数接口,除了不能看到*.lib 文件的源码,在应用方面它跟 C 源文件没有区别。

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

 

 

 

  1. crf 交叉引用文件
    *.crf 是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。我们在代码编辑器中点击“Go To Definition Of ‘xxxx’”可实现浏览跳转,见图 48-28,跳转的时候, MDK 就是通过*.crf 文件查找出跳转位置的。

 

 

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

 

  1. 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 文件。它们的关系见图 48-31。

 

 

图中的中间代表的是 armlink 链接器,在它的右侧是输入链接器的*.o 文件,左侧是它输出的*axf 文件。
可以看到,由于都使用 ELF 文件格式, *.o 与*.axf 文件的结构是类似的,它们包含ELF 文件头、程序头、节区(section)以及节区头部表。各个部分的功能说明如下:
q ELF 文件头用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头
在文件中的位置等。
q 程序头告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。 MDK 的可重定位文件*.o 不包含这部分内容,因为它还不是可执行文件,而 armlink 输出的*.axf 文件就包含该内容了。
q 节区是*.o 文件的独立数据区域,它包含提供给链接视图使用的大量信息,如指令(Code)、数据(RO、 RW、 ZI-data)、符号表(函数、变量名等)、重定位信息等,例如每个由 C 语言定义的函数在*.o 文件中都会有一个独立的节区;
q 存储在最后的节区头则包含了本文件节区的信息,如节区名称、大小等等。
总的来说,链接器把各个*.o 文件的节区归类、排列,根据目标器件的情况编排地址生成输出,汇总到*.axf 文件。例如,见图 48-32,“多彩流水灯”工程中在“bsp_led.c”文件中有一个 LED_GPIO_Config 函数,而它内部调用了“stm32f4xx_gpio.c”的 GPIO_Init 函数,经过 armcc 编译后, LED_GPIO_Config 及 GPIO_Iint 函数都成了指令代码,分别存储在 bsp_led.o 及 stm32f4xx_gpio.o 文件中,这些指令在*.o 文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。

 

ELF 文件头
接下来我们看看具体文件的内容,使用 fromelf 文件可以查看*.o、 *.axf 及*.lib 文件的
ELF 信息。
使用命令行,切换到文件所在的目录,输入“fromelf –text –v bsp_led.o”命令,可控
制输出 bsp_led.o 的详细信息,见图 48-33。 利用“-c、 -z”等选项还可输出反汇编指令文
件、代码及数据文件等信息,请亲手尝试一下。

 

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

 

 

图中的指令内容为:
“fromelf --bin --output ..\..\Output\多彩流水灯.bin ..\..\Output\多彩流水灯.axf”
该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看 fromelf 的帮助,见图 48-36。

 

 

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

 

 

fromelf 需要根据工程的*.axf 文件输入来转换得到 bin 文件,所以在命令的输入文件参数中要选择本工程对应的*.axf 文件,在 MDK 命令输入栏中,我们把 fromelf 指令放置在“After Build/Rebuild” (工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf 文件, MDK 再执行 fromelf 指令,从而得到最新的 bin 文件。设置完成生成 hex 的选项或添加了生成 bin 的用户指令后,点击工程的编译(build)按钮,重新编译工程,成功后可看到图 48-38 中的输出。打开相应的目录即可找到文件,若找不到 bin 文件,请查看提示输出栏执行指令的信息,根据信息改正 fromelf 指令

 

 

hex 文件格式
hex 是 Intel 公司制定的一种使用 ASCII 文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到 ROM 中的数据,绝大多数下载器支持该格式。
一个 hex 文件由多条记录组成,而每条记录由五个部分组成,格式形如
“:llaaaatt[dd…]cc”,例如本“多彩流水灯”工程生成的 hex 文件前几条记录见代码清单48-9。

 

 

 

例如, 代码清单 48-9 中的第一条记录解释如下:
(1) 02:表示这条记录数据区的长度为 2 字节;
(2) 0000:表示这条记录要存储到的地址;
(3) 04:表示这是一条扩展线性地址记录;

(4) 0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高 16 位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x0800 0000”,这正好是
STM32 内部 FLASH 的首地址;
(5) F2:表示校验和,它的值为(0x02+0x00+0x00+0x04+0x08+0x00)%256 的值再取补码。
再来看第二条记录:
(1) 10:表示这条记录数据区的长度为 2 字节;
(2) 0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的 FLASH 首地址为(0x0800 0000+0x0000);
(3) 00:表示这是一条数据记录,数据区的是地址;
(4) 00040020C10100081B030008A3020008:这是要按地址存储的数据;
(5) 2F:校验和为了更清楚地对比 bin、 hex 及 axf 文件的差异,我们来查看这些文件内部记录的信息来进行对比。

 

 

如果您想要亲自阅读自己电脑上的 bin 文件,推荐使用sublime 软件打开,它可以把二进制数以 ASCII 码呈现出来,便于阅读。

htm 静态调用图文件
在 Output 目录下,有一个以工程文件命名的后缀为*.bulid_log.htm 及*.htm 文件,如“多彩流水灯.bulid_log.htm”及“多彩流水灯.htm”,它们都可以使用浏览器打开。其中*.build_log.htm 是工程的构建过程日志,而*.htm 是链接器生成的静态调用图文件。
在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。
例如图 48-43 是“多彩流水灯.htm”文件顶部的说明。

 

 

该文件说明了本工程的静态栈空间最大占用 56 字节(Maximum Stack Usage:56bytes),这个占用最深的静态调用为“main->LED_GPIO_Config->GPIO_Init”。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在 STM32 中可修改启动文件改变堆栈的大小;如果空间不足,可从本文件中了解到调用深度的信息,然后优化该代码。
注意:
查看了各个工程的静态调用图文件统计后,我们发现本书提供的一些比较大规模的工程例子,静态栈调用最大深度都已超出 STM32 启动文件默认的栈空间大小 0x00000400,即 1024 字节,但在当时的调试过程中却没有发现错误,因此我们也没有修改栈的默认大小(有一些工程调试时已发现问题,它们的栈空间就已经被我们改大了),虽然这些工程实际运行并没有错误,但这可能只是因为它使用的栈溢出 RAM 空间恰好没被程序其它部分修改而已。所以,建议您在实际的大型工程应用中(特别是使用了各种外部库时,如Lwip/emWin/Fatfs 等),要查看本静态调用图文件,了解程序的栈使用情况,给程序分配合适的栈空间。

Listing 目录下的文件
在 Listing 目录下包含了*.map 及*.lst 文件,它们都是文本格式的,可使用 Windows 的记事本软件打开。其中 lst 文件仅包含了一些汇编符号的链接信息,我们重点分析 map 文件。

  1. map 文件说明
    map 文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的 Code、 RO-data、 RW-data 以及 ZI-data 的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”,各部分介绍如下:

 

在这部分中,详细列出了各个*.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)” 这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。例如,见图 48-44,我们把 bsp_led.c 文件中定义的函数 LED_GPIO_Config 改名为LED_GPIO_ConfigABCD,而不修改 main.c 文件中的调用,就会出现 main 文件无法找到LED_GPIO_Config 符号的提示。

删除无用节区
map 文件的第二部分是删除无用节区的说明(Removing Unused input sections from theimage.),见代码清单 48-11。

 

 

这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被
删除(指不加入到*.axf 文件,不是指在*.o 文件删除),这样可以防止这些无用数据占用程序空间。

例如,上面的信息中说明 startup_stm32f429_439xx.o 中的 HEAP(在启动文件中定义的用于动态分配的“堆”区)以及 stm32f4xx_adc.o 的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何 stm32f4xx_adc.c 中的内容。由此也可以知道,虽然我们把 STM32 标准库的各个外设对应的 c 库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中。

符号映像表
map 文件的第三部分是符号映像表(Image Symbol Table), 见代码清单 48-12。

 

 

这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如
我们可以查到 LED_GPIO_Config 符号存储在 0x080002a5 地址,它属于 Thumb Code 类型,大小为 106 字节,它所在的节区为 bsp_led.o 文件的 i.LED_GPIO_Config 节区。
存储器映像索引
map 文件的第四部分是存储器映像索引(Memory Map of the image), 见代码清单 48-13。

 

本工程的存储器映像索引分为 ER_IROM1 及 RW_IRAM1 部分,它们分别对应 STM32
内部 FLASH 及 SRAM 的空间。相对于符号映像表,这个索引表描述的单位是节区,而且
它描述的主要信息中包含了节区的类型及属性,由此可以区分 Code、 RO-data、 RW-data
及 ZI-data。
例如,从上面的表中我们可以看到 i.LED_GPIO_Config 节区存储在内部 FLASH 的
0x080002a4 地址,大小为 0x00000074,类型为 Code,属性为 RO。而程序的 STACK 节区(栈空间)存储在 SRAM 的 0x20000000 地址,大小为 0x00000400,类型为 Zero,属性为RW(即 RW-data) 。

映像组件大小

map 文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常
查询的内容,见代码清单 48-14。

 

这部分包含了各个使用到的*.o 文件的空间汇总信息、整个工程的空间汇总信息以及占
用不同类型存储器的空间汇总信息,它们分类描述了具体占据的 Code、 RO-data、 RW-data及 ZI-data 的大小,并根据这些大小统计出占据的 ROM 总空间。
我们仅分析最后两部分信息,如 Grand Totals 一项,它表示整个代码占据的所有空间
信息,其中 Code 类型的数据大小为 1012 字节,这部分包含了 84 字节的指令数据(inc .data)已算在内,另外 RO-data 占 444 字节, RW-data 占 0 字节, ZI-data 占 1024 字节。在它的下面两行有一项 ROM Totals 信息,它列出了各个段所占据的 ROM 空间,除了 ZI-data 不占ROM 空间外,其余项都与 Grand Totals 中相等(RW-data 也占据 ROM 空间,只是本工程中没有 RW-data 类型的数据而已)。
最后一部分列出了只读数据(RO)、可读写数据(RW)及占据的 ROM 大小。其中只读数
据大小为 1456 字节, 它包含 Code 段及 RO-data 段; 可读写数据大小为 1024 字节,它包含RW-data 及 ZI-data 段;占据的 ROM 大小为 1456 字节,它除了 Code 段和 RO-data 段,还包含了运行时需要从 ROM 加载到 RAM 的 RW-data 数据。
综合整个 map 文件的信息,可以分析出,当程序下载到 STM32 的内部 FLASH 时,需
要使用的内部 FLASH 是从 0x0800 0000 地址开始的大小为 1456 字节的空间;当程序运行时,需要使用的内部 SRAM 是从 0x20000000 地址开始的大小为 1024 字节的空间。
粗略一看, 发现这个小程序竟然需要 1024 字节的 SRAM,实在说不过去,但仔细分析
map 文件后,可了解到这 1024 字节都是 STACK 节区的空间(即栈空间),栈空间大小是在
启动文件中定义的,这 1024 字节是默认值(0x00000400)。它是提供给 C 语言程序局部变量申请使用的空间,若我们确认自己的应用程序不需要这么大的栈,完全可以修改启动文件,
把它改小一点,查看前面讲解的 htm 静态调用图文件可了解静态的栈调用情况,可以用它
作为参考。

sct 分散加载文件的格式与应用
1. sct 分散加载文件简介

当工程按默认配置构建时, MDK 会根据我们选择的芯片型号,获知芯片的内部
FLASH 及内部 SRAM 存储器概况,生成一个以工程名命名的后缀为*.sct 的分散加载文件
(Linker Control File, scatter loading),链接器根据该文件的配置分配各个节区地址,生成分
散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
例如可以设置源文件中定义的所有变量自动按地址分配到外部 SDRAM,这样就不需
要再使用关键字“__attribute__”按具体地址来指定了;利用它还可以控制代码的加载区与
执行区的位置,例如可以把程序代码存储到单位容量价格便宜的 NAND-FLASH 中,但在
NAND-FLASH 中的代码是不能像内部 FLASH 的代码那样直接提供给内核运行的,这时可
通过修改分散加载文件,把代码加载区设定为 NAND-FLASH 的程序位置,而程序的执行
区设定为 SDRAM 中的位置,这样链接器就会生成一个配套的分散加载代码,该代码会把
NAND-FLASH 中的代码加载到 SDRAM 中,内核再从 SDRAM 中运行主体代码,大部分
运行 Linux 系统的代码都是这样加载的。

分散加载文件的格式
下面先来看看 MDK 默认使用的 sct 文件,在 Output 目录下可找到“多彩流水灯.sct”,
该文件记录的内容见代码清单 48-15。

 

 

在默认的 sct 文件配置中仅分配了 Code、 RO-data、 RW-data 及 ZI-data 这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。
sct 文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而
一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外
层的是加载域,第二层“{}”内的是执行域,其整体结构见图 48-45。

 

 

加载域
sct 文件的加载域格式见代码清单 48-16。

 

 

q 加载域名:名称,在 map 文件中的描述会使用该名称来标识空间。如本例中只有
一个加载域,该域名为 LR_IROM1。
q 基地址+地址偏移:这部分说明了本加载域的基地址,可以使用+号连接一个地址
偏移,算进基地址中,整个加载域以它们的结果为基地址。如本例中的加载域基
地址为 0x08000000,刚好是 STM32 内部 FLASH 的基地址。
q 属性列表:属性列表说明了加载域的是否为绝对地址、 N 字节对齐等属性,该配
置是可选的。本例中没有描述加载域的属性。
q 最大容量:最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,
如果加上这个配置后,当链接器发现工程要分配到该区域的空间比容量还大,它
会在工程构建过程给出提示。本例中的加载域最大容量为 0x00100000,即 1MB,正是本型号 STM32 内部 FLASH 的空间大小。

 

 

输入节区描述
配合加载域及执行域的配置,在相应的域配置“输入节区描述”即可控制该节区存储
到域中,其格式见代码清单 48-18。

 

 

q 模块选择样式:模块选择样式可用于选择 o 及 lib 目标文件作为输入节区,它可以
直接使用目标文件名或“*”通配符,也可以使用“.ANY”。例如,使用语句“bsp_led.o”可以选择 bsp_led.o 文件,使用语句“*.o”可以选择所有 o 文件,使用“*.lib”可以选择所有 lib 文件,使用“*”或“.ANY”可以选择所有的 o 文件及 lib 文件。其中“.ANY”选择语句的优先级是最低的,所有其它选择语句选择完剩下的数据才会被“.ANY”语句选中。

q 输入节区样式:我们知道在目标文件中会包含多个节区或符号,通过输入节区样
式可以选择要控制的节区。
示例文件中“(RESET, +First)”语句的 RESET 就是输入节区样式,它选择
了名为 RESET 的节区,并使用后面介绍的节区特性控制字“+First”表示它要存
储到本区域的第一个地址。示例文件中的“*(InRoot$$Sections)”是一个链接器支
持的特殊选择符号,它可以选择所有标准库里要求存储到 root 区域的节区,如
__main.o、 __scatter*.o 等内容。

q 输入符号样式:同样地,使用输入符号样式可以选择要控制的符号,符号样式需
要使用“:gdef:”来修饰。例如可以使用“*(:gdef:Value_Test)”来控制选择符号“Value_Test”。
q 输入节区属性:通过在模块选择样式后面加入输入节区属性,可以选择样式中不
同的内容,每个节区属性描述符前要写一个“+”号,使用空格或“,”号分隔开,可以使用的节区属性描述符见表 48-6。

  

例如,示例文件中使用“.ANY(+RO)”选择剩余所有节区 RO 属性的内容都分配
到执行域 ER_IROM1 中,使用“.ANY(+RW +ZI)”选择剩余所有节区 RW 及 ZI 属性
的内容都分配到执行域 RW_IRAM1 中。
q 节区特性:节区特性可以使用“+FIRST”或“+LAST”选项配置它要存储到的位置,
FIRST 存储到区域的头部, LAST 存储到尾部。通常重要的节区会放在头部,而
CheckSum(校验和)之类的数据会放在尾部。
例如示例文件中使用“(RESET,+First)”选择了 RESET 节区,并要求把它放置到
本区域第一个位置,而 RESET 是工程启动代码中定义的向量表,见代码清单 48-19,
该向量表中定义的堆栈顶和复位向量指针必须要存储在内部 FLASH 的前两个地址,
这样 STM32 才能正常启动,所以必须使用 FIRST 控制它们存储到首地址。

 

 

总的来说,我们的 sct 示例文件配置如下:程序的加载域为内部 FLASH 的 0x08000000,
最大空间为 0x00100000;程序的执行基地址与加载基地址相同,其中 RESET 节区定义的
向量表要存储在内部 FLASH 的首地址,且所有 o 文件及 lib 文件的 RO 属性内容都存储在内部 FLASH 中;程序执行时 RW 及 ZI 区域都存储在以 0x20000000 为基地址,大小为
0x00030000 的空间(192KB),这部分正好是 STM32 内部主 SRAM 的大小。
链接器根据 sct 文件链接,链接后各个节区、符号的具体地址信息可以在 map 文件
中查看。

通过 MDK 配置选项来修改 sct 文件
了解 sct 文件的格式后,可以手动编辑该文件控制整个工程的分散加载配置,但 sct 文
件格式比较复杂,所以 MDK 提供了相应的配置选项可以方便地修改该文件,这些选项配
置能满足基本的使用需求,本小节将对这些选项进行说明。
选择 sct 文件的产生方式
首先需要选择 sct 文件产生的方式,选择使用 MDK 生成还是使用用户自定义的 sct 文
件。在 MDK 的“Options for Target->Linker->Use Memory Layout from Target Dialog”选项
即可配置该选择,见图 48-46。

 

 

该选项的译文为“是否使用 Target 对话框中的存储器分布配置”,勾选后,它会根据
“Options for Target”对话框中的选项生成 sct 文件,这种情况下,即使我们手动打开它生
成的 sct 文件编辑也是无效的,因为每次构建工程的时候, MDK 都会生成新的 sct 文件覆盖旧文件。该选项在 MDK 中是默认勾选的,若希望 MDK 使用我们手动编辑的 sct 文件构建工程,需要取消勾选,并通过 Scatter File 框中指定 sct 文件的路径,见图 48-47。

 

 

通过 Target 对话框控制存储器分配
若我们在 Linker 中勾选了“使用 Target 对话框的存储器布局”选项,那么“Options
for Target”对话框中的存储器配置就生效了。主要配置是在 Device 标签页中选择芯片的类
型,设定芯片基本的内部存储器信息以及在 Target 标签页中细化具体的存储器配置(包括外部存储器),见图 48-48 及图 48-49。

 

 

图中 Device 标签页中选定了芯片的型号为 STM32F429IGTx,选中后,在 Target 标签
页中的存储器信息会根据芯片更新。

 

 

在 Target 标签页中存储器信息分成只读存储器(Read/Only Memory Areas)和可读写存储
器(Read/Write Memory Areas)两类,即 ROM 和 RAM,而且它们又细分成了片外存储器
(off-chip)和片内存储器(on-chip)两类。
例如,由于我们已经选定了芯片的型号, MDK 会自动根据芯片型号填充片内的 ROM
及 RAM 信息,其中的 IROM1 起始地址为 0x80000000,大小为 0x100000,正是该 STM32
型号的内部 FLASH 地址及大小;而 IRAM1 起始地址为 0x20000000,大小为 0x30000,正是该 STM32 内部主 SRAM 的地址及大小。图中的 IROM1 及 IRAM1 前面都打上了勾,表示这个配置信息会被采用,若取消勾选,则该存储配置信息是不会被使用的。
在标签页中的 IRAM2 一栏默认也填写了配置信息,它的地址为 0x10000000,大小为0x10000,这是 STM32F4 系列特有的内部 64KB 高速 SRAM(被称为 CCM)。当我们希望使
用这部分存储空间的时候需要勾选该配置,另外要注意这部分高速 SRAM 仅支持 CPU 总
线的访问,不能通过外设访问。
下面我们尝试修改 Target 标签页中的这些存储信息,例如,按照图 48-50 中的 1 配置,
把 IRAM1 的基地址改为 0x20001000,然后编译工程,查看到工程的 sct 文件如代码清单
48-20 所示;当按照图 48-50 中的 2 配置时,同时使用 IRAM1 和 IRAM2,然后编译工程,可查看到工程的 sct 文件如代码清单 48-21 所示。

 

 

可以发现, sct 文件都根据 Target 标签页做出了相应的改变,除了这种修改外,在
Target 标签页上还控制同时使用 IRAM1 和 IRAM2、加入外部 RAM(如外接的 SDRAM),
外部 FLASH 等。
控制文件分配到指定的存储空间
设定好存储器的信息后,可以控制各个源文件定制到哪个部分存储器,在 MDK 的工
程文件栏中,选中要配置的文件,右键,并在弹出的菜单中选择“Options for File xxxx”
即可弹出一个文件配置对话框,在该对话框中进行存储器定制,见图 48-51

 

 

在弹出的对话框中有一个“Memory Assignment”区域(存储器分配),在该区域中可以
针对文件的各种属性内容进行分配,如 Code/Const 内容(RO)、 Zero Initialized Data 内容(ZIdata)以及 Other Data 内容(RW-data),点击下拉菜单可以找到在前面 Target 页面配置的IROM1、 IRAM1、 IRAM2 等存储器。例如图中我们把这个 bsp_led.c 文件的 Other Data 属性的内容分配到了 IRAM2 存储器(在 Target 标签页中我们勾选了 IRAM1 及 IRAM2),当在bsp_led.c 文件定义了一些 RW-data 内容时(如初值非 0 的全局变量),该变量将会被分配到IRAM2 空间,配置完成后点击 OK,然后编译工程,查看到的 sct 文件内容见代码清单48-22。

 

 

可以看到在 sct 文件中的 RW_IRAM2 执行域中增加了一个选择 bsp_led.o 中 RW 内容
的语句。
类似地,我们还可以设置某些文件的代码段被存储到特定的 ROM 中,或者设置某些
文件使用的 ZI-data 或 RW-data 存储到外部 SDRAM 中(控制 ZI-data 到 SDRAM 时注意还需要修改启动文件设置堆栈对应的地址,原启动文件中的地址是指向内部 SRAM 的)。
虽然 MDK 的这些存储器配置选项很方便,但有很多高级的配置还是需要手动编写 sct
文件实现的,例如 MDK 选项中的内部 ROM 选项最多只可以填充两个选项位置,若想把内部 ROM 分成多片地址管理就无法实现了;另外 MDK 配置可控的最小粒度为文件,若想控制特定的节区也需要直接编辑 sct 文件。

posted @ 2017-10-20 09:53  Crystal_Guang  阅读(5485)  评论(3编辑  收藏  举报