引言
ARM编译器将各种源文件(汇编文件、C语言程序文件、C++语言程序文件)编译生成ELF格式的目标文件(后缀为.o文件,以下将目标文件简称为.o文件),.o文件经过连接器,和C/C++运行时库一起编译生成ELF格式的映像文件(image,如常用的MKD使用 fromelf.exe生成的bin文件就是image),bin文件可以直接写入到flash中,实际上使用jlink下载的内容就是bin文件的内容(hex文件是包含地址信息的bin文件,可以理解为hex=地址信息+bin)。下文中也将映像文件称为bin文件或者image文件。编译流程如下图。
图片来源于《Introductiontothe Armv8-M Architecture and its ProgrammersModel》
相关文档在后面有链接
1、ARM映像文件的组成
1.1 ARM映像文件的组成
ARM映像文件是一个层次性的结构,有如下三部分组成:域(region)、输出段(output section)、输入段(input section),三者有如下的关系:
-
一个bin文件由一个或多个域组成
-
一个域包含一个或多个输出段
-
一个输出段包含一个或多个输入段
-
各输入段包含了目标文件中的代码和数据
输入段可以理解为我们写的代码具体包含哪些属性。输入段包含4类:代码、已经初始化的数据、未初始化的存储区域、内容初始化为0的区域,即(code+RW)、ZI、ZI,分别为RO段、RW段、ZI段。编译器根据输入段的属性,将这些输入段分组,组成不同的输出段以及域。
一个输出段里面包含了一些列具有相同RO、RW、ZI属性的输入段。输出段的属性于其中包含的输入段的属性相同,在一个输出段内部,各个输入段是按照一定的规律排序的。
一个域包含1到3个输出段,其中各输出段的属性各不相同。各输出段的排列顺序是由其属性决定的,RO属性排在最前面,其次RW,最后ZI段。一个域通常映射到一个物理储存器上,如flash、ram等。
抛开上面较书面的表达,我们已keil为例。在mdk中,域的定义由分散链接文件(sct)决定,随便打开一个keil的sct文件,可以看到:
在sct中,定义了三个域,名字分别为LR_IROM1、ER_IROM1、RW_IRAM1,LR_IROM1是最终的bin文件,ER_IROM1是加载域,RW_IRAM1是执行域。keil生成的bin中,一般包含三个段:RO段(代码+只读数据)、RW段、ZI段。一个域至少包含上述三个段中的一个。
对于一个C文件来说,可能包含代码、全局变量、只读数据、为0的数据、未初始化的数据。在编译的时候,会自动将各个段进行分类,例如led.c文件,会将代码部分放到RO段里面,全局变量放到RW段,未初始化的变量存放到ZI段。
以下进行一些猜测:如果说我们有led1.c和led2.c两个文件,二者分别生成RO、RW、ZI,则可以理解为:led1.c和led2.c是输入段1.1.1和1.1.2,他们生成的RO、RW、ZI为输出段1.1和1.2,最终合并在域1中(实际上我们知道,这个域存放的地方是M4芯片内部的flash)。
1.2 ARM映像文件各组成部分的地址映射
bin文件在储存系统(如内部flash)中的地址有两种:一种是bin文件位于储存器中时的地址,称为加载地址。一种时bin文件运行时的地址,称为运行地址。之所以分成两类,是因为在bin文件中,有些域是可以移动到新的储存区域,比如RW域中的数据在运行的时候会搬运到RAM中。
如下图所示为例,flash的起始地址为0x0800 0000,依次存放RO、RW、ZI段,在运行的时候,ZI和RW会搬运到SRAM中。
一个bin文件一般包含若干个域,一个域包含若干个输出段。ARM链接器在链接时需要知道如下信息才能正确的生成bin文件:
-
分组信息 决定如何将各输入段组织成相应的输出段和域
-
定位信息 决定各域在储存空间中的起始地址
根据bin文件中地址映射的复杂程度,可以使用命令行选项的方式提供或者提供配置文件(如keil的sct文件)
2、ARM映像文件的入口点
2.1 ARM映像文件中的两类入口点
bin文件有两类入口点:一种是映像文件运行时的入口点,称为初始入口点(initial entry point),另一种时普通的入口点(entry point)。
初始入口点时bin文件运行时的入口点,每个bin只有唯一的初始入口点,保存在ELF头文件中。如果bin是被操作系统加载的,则OS正是用过跳转到该初始入口点处执行来加载该映像文件。
普通入口点是在汇编中用ENTRY来定义的,在嵌入式系统中一个典型的用途就是将中断服务程序定义为普通入口点,防止在链接的时候将中断服务程序给删除了。在C库中,__main是入口点。在sct中,可以看到:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x10000000 0x0000F000 { ; load region size_region
ER_IROM1 0x10000000 0x0000F000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x1FFFC000 0x00003000 { ; RW data
.ANY (+RW +ZI)
}
}
其中REST就是初始入口点属性,将初始入口点放到ROM中,在start.s中可以看到如下的汇编写法:
这个就是初始入口点定义的代码部分。
在一个bin文件中,只有一个初始入口点,普通入口点可以由多个,初始入口点可以是普通入口点,也可以不是。
2.2 定义初始入口点
初始入口点需要满足如下两个条件:
-
初始入口点必须位于bin文件的运行时域内
-
包含初始入口点的运行时域不能被覆盖,它的加载地址和运行地址必须相同(这种域称为固定域,root region)
可以使用链接选项-entry address来指定bin的初始入口点,address表示初始入口点的地址。
如果bin被一个加载器加载再运行,如被引导程序或者OS加载,则bin文件必须包含一个初始入口点。比如,一个操作系统的映像文件被一个引导程序加载,这时程序跳转到该映像文件的初始入口点处开始执行,它覆盖了引导程序,称为系统中的操作系统。这种映像文件中通常包含了其它的普通入口点,这些入口点一般为异常中断处理程序的入口地址。
当用户没有指定链接选项-entry指定链接地址,则按照如下规则决定映像文件的初始入口点:
-
如果输入的目标文件中只有一个普通入口点,该普通入口点被链接器当作bin文件的初始入口点
-
如果输入的目标文件没有普通入口点或者普通入口点多余一个,则链接器生成的bin文件不包含初始入口点,且产生警告。
2.3 普通入口点的用法
普通入口点一般用作两种用途:
-
指定中断服务程序的入口,防止链接的时候删除中断服务器程序代码
-
没有指定链接选项-entry address且输入的目标文件中只有一个普通入口点,则该普通入口点被当作初始入口点
3、输入段的排列顺序
链接器根据各输入段的属性来组织这些输入段,具有相同属性的输入段被放到域中一段连续的地址空间中,组成一个输出段。在输出段中,各输入段的起始地址与输出段的起始地址和该输出段中各输入段的排列顺序有关。
通常情况下,输出段中各输入段的排列顺序由如下几个因素决定:
-
输入段的属性
-
输入段的名称
-
各输入段在连接命令行的输入段列表中的排列顺序
按照输入段的属性,排列顺序如下:
-
只读的代码段(RO-code)
-
只读的数据段(RO-data)
-
可读写的代码段(RW-code)
-
其它以及初始化的数据段(RW-data)
-
未初始化的数据(ZI)
对于相同属性的输入段,按照名称顺序来排序,输入段的名称区分大小写,按照ASCII码顺序进行排序。如果输入段的名称也相同,按照其在输入段列表中的顺序进行排序。也就是说,即使各输入段的属性和名称保持不变,若在编译时,各个输入段在列表中的排列属性不同,生成的映像文件也将不同。
可以使用连接选项-first、-last改变输入段的排列顺序,也可以使用配置文件改变输入段的排列顺序。排序规则影响因素有三个:输入段属性、输入段名称、输入段在列表中的顺序。连接选项-first、-last只对输入段的名称和在列表中的顺序有效,无法改变因输入段的属性进行的排序规则。
在各个输入端排好序之后,在最终确定各个输入段的起始地址之前,可以通过填充“补丁”使各个输入段满足地址对齐要求。
4、题外话
本篇文章是博主在学习ARM体系的时候写的笔记性质的文章,先在CSDN上分享,用到的参考文档有:
- 《Introductiontothe Armv8-M Architecture and its ProgrammersModel》
- 《ARM体系结构与编程》
相关文档已经上传到博主的仓库里面,链接:https://gitee.com/zichuanning520/htq_library