汇编器、链接器和加载器

547 阅读11分钟

在这篇文章中,我们讨论了汇编器、链接器和加载器的任务。我们还讨论了汇编器和链接器所面临的设计问题。

目录.

  1. 导言.
  2. 汇编器的工作原理。
  3. 设计问题:汇编器。
  4. 设计问题:链接器。
  5. 总结。
  6. 参考文献.

简介.

汇编器负责将源代码转换为目标代码,因此就像编译器一样,汇编器也会有诸如词法分析、符号表的管理和回补等阶段。

值得注意的是,汇编器的输出与准备在计算机上运行的可执行程序还有很大的距离。

汇编器的工作原理。

我们将从一个准备执行的程序开始,然后向后进行,以了解汇编器是如何执行其任务的。

一个正在运行的程序。

一个运行中的程序由四个部分组成,即代码段堆栈段数据段寄存器
代码段和数据段通过存储在机器指令和数据段的预填充部分中的位置的地址而相互联系。

操作系统将设置硬件内存管理单元的寄存器,使每个运行中的程序的代码和数据段的地址空间从零开始,不管这些段在内存中的位置如何。这是一个问题,我们将在下面的章节中看到。

可执行程序。

为了运行一个程序的内容,这个文件被加载到内存中,加载器是操作系统的一个组成部分,因此它是不可见的。
程序的所有初始化部分都来自可执行代码文件,所有的地址都是基于从零开始的段。

装载器读取这些段并创建一个堆栈段,然后跳转到代码段中的预定位置来启动程序。

除了代码段和数据段之外,可执行代码文件还可以包含其他数据,如初始堆栈大小、执行开始地址等。

链接。

每个对象文件都会有自己的代码段和数据段内容,链接器的任务是将这些内容组合成可执行文件的代码段和数据段。

它通过复制这些段,将它们连接起来,然后将它们写入文件中。

关于代码和数据段内的地址存在一个问题。对象文件中的代码和数据通过地址相互关联,但由于对象文件是在不知道它们将如何被连接的情况下创建的。记住每个对象文件的每个代码或数据段的地址空间从零开始。
要解决这个问题,意味着所有对象文件副本内的地址需要在链接过程中被移到它们的实际位置。

一个例子

假设第一个对象文件a.o中的代码段的长度是1000字节,那么另一个对象文件b.o的第二个代码段将从机器地址1000的位置开始。

为此,链接器必须有重定位信息--关于包含地址的对象段的位置信息和地址信息,即地址是指代码段还是数据段。

重新定位信息可以是位图的形式,其中的位对应于对象代码和数据段中的每个位置,地址可能位于这些位置,或者以链接列表的形式。

另一个问题是对象文件中的代码和数据段包含其他程序对象文件或库对象文件中的地址。

这个问题通过外部符号外部名称来解决,这些符号或名称在对象文件中标记了一个位置L,其地址可以在其他对象文件中使用。
这个位置L
称为
外部入口点。对象文件使用一个外部参考来引用L

对象文件包含它们引用的外部符号和它们提供的外部符号的入口点的信息。所有这些信息都存储在一个外部符号表中

例如,给定一个对象文件a.o,其中包含对位置500的printf例程的调用,这个文件将在外部符号表中有明确的信息,即它引用了位置500的外部符号printf

如果库中的对象文件printf.o在位置100处有printf的主体,该文件将在外部符号表中有明确的信息,即在地址100处有外部入口点的特征。

链接器负责结合这两个信息,一旦确定了这个副本相对于其他副本的位置,就把文件a.o的代码段副本中位置500的地址更新为printf.o副本中位置100的地址。

三个代码段的连接
assembly-1

从上面来看,这些段来自对象文件a.ob.oprintf.o。代码段b.o的长度被假定为3000字节,printf.o的长度为500字节。

b.o包含三个内部地址,指的是位置1600、250和400。
a.o包含一个外部符号的外部地址printf
.o包含一个外部入口点--printf的位置。
虽然这里没有包括,但a.oprintf.o的代码段将包含许多内部地址。

与链接列表相比,重定位位图是有效的,因为段将包含高比例的内部地址。

在链接过程中,连接段之后,更新a.ob,oprintf.o副本中的内部地址,将这些段的位置加入其中。
位置是通过扫描重定位图找到的,重定位图也会显示地址是指代码段还是数据段。

最后,它将printf的外部地址存储在位置100处,其计算结果为(1000+3000+100)=4100。

我们还看到,一个对象文件将有四个组成部分,即代码段和数据段、重定位位图和外部符号表。

设计问题:汇编器

构建汇编器的主要问题是如何处理内部地址--指同一段中的位置和外部地址--指其他对象文件段中的位置。

处理内部地址

对同一代码段或数据段中的位置的引用在汇编代码中采取标识符的形式。

一个例子

.data
    ...
    .align 8
var1:
    .long 666
    ...
.code
    ...
    addl var1,%eax
    ...
    jmp label1
    ...
label1:
    ...
    ..

这个片段从*.data数据段的材料开始,它包含一个4字节的位置.long*,在一个8字节的边界上对齐,填充有666,并标有var1标识符。

在这之后,我们有*.代码段的材料,包含了从位置var1到寄存器%eax的4字节加法,跳转到标签label1和其定义(label1*)等指令。

汇编器读取代码并在两个不同的数组中为这些段汇编字节。

当上述片段被读取时,首先遇到的是*.data*指令,它指示它开始组装到数据数组中。
数据段的源材料被翻译成二进制,结果被存储在数据数组中。

现在遇到了*.code指令,汇编器转向汇编到代码数组中,在翻译这一段时,它遇到了指令addl var1, %eax*,为此它汇编了适当的二进制模式和寄存器指示,包括数据段标签var1的值*,400*。

汇编器在重定位位图中把这个位置标记为 "可重定位到数据段"
,结果存储在正在汇编的代码段的阵列中。

当遇到jmp label1指令时,由于不知道label1的值,汇编器不能执行类似的动作。

回补可以解决这个问题,也就是说,汇编器为每一个数值未知的标签保留一个回补列表。

例如,标签L的回补列表将包含所有地址 A1,...An因此,当遇到标签L的应用情况,并且汇编器决定它的值必须被汇编到位置A时,地址A被插入到L的回补列表中,并且位置A被清零。

由此产生的安排如下。

assembly1-3

上面的图片显示了汇编代码、汇编后的二进制代码和标签label1的回补列表。

当找到L的定义出现时,它的标签位置的地址被确定并分配给L作为它的值。
回补列表被处理,对于每个条目

L的值被存储在Ak中。

两次扫描装配也是一种解决方案,装配器将对一个输入文件进行两次处理。第一次扫描确定了标签的值。
在第二次扫描中,所有标签的值都是已知的,因此将进行翻译。

处理外部地址。

一个对象文件的外部符号和地址的例子总结在它的外部符号表中。

外部符号类型地址
选项entry_point50个数据
entry_point100 代码
printf参考500代码
atoi参考600代码
printf参考文献650代码
退出参考700代码
msg_list进入点300 数据
记忆外输入点800代码
fprintf参考900代码
退出参考950代码
file_list参考4个数据

上表规定,数据段在位置50有一个名为options的入口点,代码段的入口点名为main,位置100,该段在位置500引用了一个外部入口点printf
在位置4还有一个名为file_list

外部入口点引用。
看一下地址栏中的数字,对于入口点来说,这个数字代表入口点的一个值,对于引用来说,它代表被引用的入口点的值必须被存储的地址。
这个表在翻译过程中很容易构建,然后汇编器产生一个二进制版本,并将它与代码和数据段、重定位位图和其他头尾材料放在对象文件中的适当位置。

链接器也可以通过使用来自编译器的信息来创建用于调试翻译程序的表格,这足以让调试器找到源自代码片段的精确变量和语句。

设计问题。链接器

链接器读取对象文件并将四个组件中的每一个附加到四个列表中适当的一个。

它还保留了关于组件的长度和位置的信息。
重新定位是很直接的,可以重新定位内部地址和链接外部地址。

然后,它将代码和数据段写入可执行代码文件,它还可以选择添加外部符号表和调试信息。
到这个阶段,翻译已经完成,然而,现实世界的链接器比这复杂得多。

首先,围绕对象模块的情况要复杂得多,因为,许多对象文件格式具有重复初始化数据、可重定位地址中的特殊算术运算、有条件的外部符号解析等特点。

其次,链接器必须通过大型库来寻找所需的外部入口点。先进的符号表技术加速了这一过程。
第三,链接器编写者面临着制作快速链接器的压力,然而,在处理外部符号表时发现了效率低下的根源,即对于每个入口点,要扫描整个表以找到具有相同符号的条目,然后可以进行处理。

这个过程需要O(n2)的时间复杂度,其中n代表组合外部符号表的条目数。

这可以通过排序来解决,从而将具有相同符号的条目放在一起。

总结

汇编器将为源代码模块生成的符号指令翻译成可重定位的二进制对象文件。
链接器将一些可重定位的二进制文件,可能是库对象文件组合成可执行的二进制程序文件。
装载器将可执行的二进制程序文件加载到内存中执行。
可重定位对象文件的代码和数据段由从符号指令衍生的二进制代码组成。
可重定位二进制对象文件包含代码和数据段、重定位信息和外部链接信息。