ARM ELF 镜像结构

来源:互联网 发布:linux个人目录 编辑:程序博客网 时间:2024/06/06 01:45

参考链接:The structure of an ARM ELF image

*注:虽然其它书籍或者文章把 “section” 翻译成“段”,但是这里为了不和 “segment” 相冲突,本文把 “section” 翻译成“节”。本文把 “link” 翻译成“连接”,也可理解为“链接”。

ELF 是 Executable and Linkable Format 的缩写,意为可执行与可连接格式。一个 ARM ELF 镜像包括节(section)、域(region)和段(segment),镜像的各个连接阶段都有不同的视图。

一个镜像的结构由以下三点决定:
● 组成镜像的域和输出节的数量;
● 加载镜像时这些域和节在存储器中的位置;
● 执行镜像时这些域和节在存储器中的位置。

一、镜像处于各个连接阶段的视图

镜像的各个连接阶段都有不同的视图,ELF 镜像有如下三个视图:

1、ELF 目标文件视图(连接器的输入)
ELF 目标文件视图包含输入节。 ELF 目标文件可以是:
● 一个包含代码和数据的重定位文件,这些代码和数据可以和其它目标文件相连接后生成一个可执行文件或者一个共享目标文件;
● 一个包含代码和数据的共享目标文件。

2、连接器视图
因为一个程序存储和运行时的地址是不同的,所以连接器有两个视图——与位置无关的程序段(program fragment)和重定位的程序段(程序段包含代码或者数据):
● 程序段的加载地址是指连接器希望程序加载器、动态连接器或者调试器从 ELF 文件复制程序段的目标地址,这可能不是程序段的执行地址;
● 程序段的执行地址是指连接器希望程序段在程序执行的时候所在的目标地址。
如果程序段是与位置无关的或者重定位的,那么它的执行地址将会在程序执行期间发生变化。

3、ELF 镜像文件视图(连接器的输出)
ELF 镜像文件视图包含程序段和输出节:
● 一个加载域对应于一个程序段;
● 一个可执行域包含一个或多个如下输出节:RO 节(只读)、RW 节(可读写)、XO 节(仅可执行)、ZI 节(未初始化)
一个或多个可执行域组成一个加载域。

*注:armlink 连接器所允许的程序段的最大大小为 2 GB 。

描述存储器视图需要注意一下两点:
● 术语“根域(root region)”的意思是加载和执行地址相同的域;
● 加载域等价于 ELF 段。

下面这张图表述了不同连接阶段的视图间的关系:
不同连接阶段的视图间的关系

二、输入节、输出节、域和程序段

一个目标文件或者镜像文件是由输入节、输出节、域和程序段组成的。

1、输入节
输入节是从输入目标文件中分离出来的。它包含代码、已初始化的数据、一个未初始化的存储器段或者一个执行镜像前必须被初始化为 0 的存储器段。这些属性表示为 RO、 RW、 XO 和 ZI 。 armlink 连接器根据这些属性把输入节分组到更大的输出节和域中。

2、输出节
一个输出节是由一组输入节构成的,输出节同样拥有 RO、 RW、 XO 或者 ZI 属性,连接器将输出节相邻放置在存储器中。一个输出节和和它的输入节具有相同的属性。在一个输出节内,输入节的分组由节排列规则决定。

3、域
一个域最多包含 4 个输出节,这取决于输出节的数量和内容,这些输出节可以是不同属性的。默认情况下,一个域中的输出节是根据它们的属性分类排列的。任何 XO 属性的输出节排在最前面,然后接着 RO 输出节,然后是 RW 输出节,排在最后面的是 ZI 输出节。一个域通常映射到一个物理存储设备,比如 ROM、 RAM 或者外设。你可以使用 scatter-loading 更改输出节的顺序。

4、程序段
程序段对应于加载域,并且包含可执行域。程序段包含代码和数据等信息。

*注:XO 属性的存储器仅仅在 ARMv7-M 和 ARMv8-M指令集架构中被支持。armlink 连接器所允许的程序段的最大大小为 2 GB 。

使用 execute-only (XO) 节需要注意的两点
● 你可以在可执行域内混合 XO 节和非 XO 节。当然,这将导致输出节的属性为 RO。
● 如果输入文件有一个或者多个 XO 节,那么连接器将生成一个分立的 XO ELF 段。在最终的镜像中,XO 段将放在 RO 段之前,除非在 scatter 文件中特别指定或者使用 --xo-base 连接选项。

三、镜像的加载视图和执行视图

加载程序时,镜像域被放置在系统存储器映射中。在程序执行的时候,域在存储器中的位置可能发生改变。

在执行镜像之前,你可能需要其中一些域移动到它们的执行地址,并且创建 ZI 输出节。例如,已经初始化的 RW 数据可能需要从位于 ROM 中的加载地址复制到位于 RAM 中的执行地址。

镜像的存储器映射有以下两个不同的视图:

1、加载视图
描述每个镜像域和节在镜像加载进存储器的时候所在的地址,也就是说在镜像开始执行之前的位置。

2、执行视图
描述每个镜像域和节在镜像执行时0所在的地址。

下面这张图表述了一个不包含仅可执行 (XO) 节的镜像的这些视图:
不包含仅可执行 (XO) 节的镜像的加载视图和执行视图

下面这张图表述了一个包含仅可执行 (XO) 节的镜像的加载视图和执行视图:
包含仅可执行 (XO) 节的镜像的加载视图和执行视图

*注:XO 存储器仅仅被 ARMv7-M 和 ARMv8-M 指令集架构所支持。

下面这张表格比较了加载视图和执行视图的区别:

加载 说明 执行 说明 加载地址 在包含着一个段或者域的镜像开始执行之前,那个段或者域被加载进存储器的地址。一个段或者一个非根域的加载地址可能和它的执行地址不同。 执行地址 当包含着一个段或者域的镜像正在被执行的时候,那个段或者域所在的地址。 加载域 一个加载域描述了存储器中一块连续的加载地址空间的布局 执行域 一个执行域描述了存储器中一块连续的执行地址空间的布局

四、用连接器指定一个镜像存储器映射的方法

一个镜像可以包含任意数量的域和输出节。域可以有不同的加载和执行地址。

当组织一个镜像的存储器映射时,ARM 连接器必须知道以下信息:
● 输入节如何被分组到输出节和域;
● 域应该被放到存储器映射中的什么位置。

根据镜像的存储器映射复杂度,有两种向 ARM 连接器传递这些信息的方式:
1、对于简单的存储器映射,可以使用命令行选项
对于一个仅仅包含一个或者两个加载域和至多三个执行域的镜像来说,你可以使用如下命令:
--first
--last
--ro_base
--rw_base
● [BETA] --ropi
● [BETA] --rwpi
--split
--rosplit
--xo_base
--zi_base
对于简单的镜像来说,这些选项提供了和使用 scatter 文件效果相同但是简单许多的方式向 ARM 连接器传递信息。当然,使用这些选项的时候没有对域进行界限检查,以确定域是可用的。

*注:--xo_base 不可以和 --ropi 或者 --rwpi 同时使用

2、对于复杂的存储器映射,可以使用 scatter 文件
scatter 文件是一个以文本形式描述存储器布局和代码、数据存放位置的文件。它被用在你想完全控制镜像各个部分的分组和放置等更加复杂的情况下。使用 scatter 文件请在命令行中添加 --scatter=filename 选项。

*注:--scatter=filename 选项不能和其它与存储器映射相关的命令行选项同时使用。

下表说明了 scatter 文件和与其等效的命令行选项的对比:
scatter 文件和与其等效的命令行选项的对比

*注:如果存在 XO 节,那么仅仅当你指定了 --xo_base 选项时,分离的加载和执行域才会被创建。如果你不指定 --xo_base 选项,那么 ER_XO 域将会被放在 --ro_base 选项指定的 LR1 域所在的地址。

五、镜像入口点

镜像中的一个入口点是指被加载进 PC 寄存器中的镜像中的那个位置。它是程序开始执行的位置。虽然在一个镜像中可以有不止一个的入口点,但是在连接时你仅可以指定一个镜像入口点。

不是每个 ELF 文件必须包含一个镜像入口点。在单个 ELF 文件中不允许存在多个入口点。

*注:对于嵌入式 Cortex-M 程序,程序开始于被加载进 PC 寄存器的复位向量。一般地,复位向量指向 CMSIS 中的 Reset_Handler 函数。

入口点的类型

入口点有两种不同的类型:
1、初始化入口点
一个镜像的初始化入口点是存储在 ELF 头文件中的单个值。因为一个操作系统或者引导程序将程序加载进了 RAM 中,所以加载器转而通过控制镜像中的初始化入口点来启动镜像的执行。
一个镜像只能包含一个初始化入口点。初始化入口点可以(但不是必须)用 ENTRY 指令来设置一个入口点。
2、用 ENTRY 指令设置入口点
你可以为一个镜像从很多个可能的入口点中选择一个入口点。一个镜像只能有一个入口点。
你在汇编文件中使用 ENTRY 指令以在目标文件中创建入口点。在嵌入式系统中,一般使用这条指令去标记通过处理器的中断向量进入执行的代码,比如 RESET、 IRQ 和 FIQ。使用 ENTRY 关键字告诉连接器不要在清除无用节的时候移除该指令所标记的输出代码节。

对于 C 和 C++ 程序,__main() 函数在 C 库中也是一个入口点。
如果一个加载器使用一个嵌入的镜像,镜像必须在文件头中指定一个初始化入口点。使用 --entry 命令行选项选择一个入口点。

一个镜像的初始化入口点

一个镜像只能含有一个初始化入口点,除非连接器输出 L6305W 的警告。

初始化入口点必须满足如下条件:
● 镜像的入口点必须位于执行域内;
● 一个执行域不能覆盖另一个执行域,并且必须是一个根执行域。也就是说,加载地址与执行地址相同。

如果你没有使用 --entry 选项指定一个初始化入口点,那么:
● 如果输入目标文件仅包含一个由 ENTRY 指令设置的入口点,则连接器使用那个入口点作为镜像的初始化入口点;
● 连接器在以下两种情况下生成不包含初始化入口点的镜像:有超过一个以上的由 ENTRY 指令指定的入口点,没有由 ENTRY 指令指定的入口点。

对于位于 ROM 零地址上的嵌入式应用程序而言,可以使用 --entry 0x0,或者在使用高端向量 (high vectors) 的处理器上使用 0xFFFF0000

*注:高端向量 (high vectors) 在 AArch64 状态下不被支持。

*注:一些处理器,比如 Cortex-M7,在某些配置下能够从不同的地址上启动。

六、镜像结构的限制

在编译 AArch64 架构的目标时,一条指令只能存取相对于 PC 寄存器内所存地址的 4GB 地址范围内的数据。

比如,思考如下 scatter 文件:

LOAD_REGION 0x0000000000 0x200000{    ROOT_REGION +0    {        *(Init, +FIRST)        * (+RO)        * (+RW, +ZI)    }    STACKHEAP 0x1FFFF0 EMPTY -0x18000    {    }}LOAD_REGION2 0x4000000000 0x200000{    ROOT_REGION2 +0    {        *(high_mem)    }}

LOAD_REGION2LOAD_REGION 有16 GB 远,所以 LOAD_REGION 中的代码无法存取 high_mem 中的数据。这将会在连接过程中产生一个超出范围的错误。

0 0
原创粉丝点击