MDK编译
1、MDK 编译过程
1.1 编译过程简介
首先我们简单了解下 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 中。
1.2 、具体工程中的编译过程
下面我们打开 “多彩流水灯”的工程,以它为例进行讲解,其它工程的编译过程也是一样的,只是文件有差异。打开工程后,点击 MDK 的“ rebuild”按钮,它会重新构建整个工程,构建的过程会在 MDK 下方的“ Build Output”窗口输出提示信息,见下图:

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

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

可以看到,每个 C 源文件都对应生成了.o、 .d 及.crf 后缀的文件,还有一些额外的.dep、 .hex、 .axf、 .htm、 .lnp、 .sct、 .lst 及.map 文件。
2、 程序的组成、存储与运行
2.1、 CODE、 RO、 RW、 ZI Data 域及堆栈空间
在工程的编译提示输出信息中有一个语句“ Program Size: Code=xx RO-data=xx RWdata=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 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“ 0 值”给该变量进行初始化(若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初始化为 0);
ZI-data 的栈空间(Stack)及堆空间(Heap):在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用 malloc 动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于 ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用 malloc 动态申请堆空间,编译器会优化,不把堆空间计算在内)。
综上所述,以程序的组成构件为例,它们所属的区域类别见下表:
2.2 、程序的存储与运行
RW-data 和 ZI-data 它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?这就涉及到程序的存储状态了,应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如 STM32 的内部 FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的,见下图:

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

程序状态区域的组成

浙公网安备 33010602011771号