type
status
slug
summary
tags
category
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM
目标文件基本上由编译和汇编的代码、数据以及使其内容可用所需的所有附加信息组成。在构建操作系统的过程中,你会用到很多目标文件。虽然对于常见的开发任务,您不需要知道它们的确切细节,但当您想要创建或使用具有各种细节的任务时,细节可能非常重要。
注意:术语“目标文件”与“面向对象编程”的高级概念无关。对象文件这个术语在 1958 年或更早的时候就在 IBM 使用了,比最早的 OOP 形式(“Actor 模型”,大约 1966 年)早了十多年。

1 Core Concepts

目标文件是三种包含目标代码的文件之一,目标代码是一种经过修改的机器代码,其中包含允许链接和重定位最终加载的可执行文件的附加信息。
对于大多数用途,编译器或汇编器将生成目标代码作为其最终结果,而不是真正的可执行二进制文件。虽然大多数汇编器和一些编译器都可以选择生成原始二进制映像,但这通常只应用于引导加载程序、只读存储器芯片和其他特殊用途的可执行文件。实际上,今天几乎所有系统都使用目标文件和可重定位的可执行文件。即使是当今常用的最简单的文件格式,MS-DOS .COM 格式,也不是纯粹的二进制可执行映像; MS-DOS 加载程序使用段的前 0x100 个字节作为程序段前缀,因此文件中排除了段映像的一部分。
今天的大多数系统都有非常复杂的目标文件格式,其中地址信息被替换为某种存根或符号,并且包含有关外部可见函数、变量等的相对位置的信息。这有利于以下过程
  • 链接,其中两个或多个目标文件和/或库文件组合在一起形成一个可执行文件,以及
  • 加载,其中地址存根被加载程序替换为代码将驻留在进程内存中的实际内存位置。

2 Object files, executable files and library files

尽管维基百科基于两者都包含目标代码而不是二进制图像,认为可执行文件是目标文件的一个子集,但两者之间存在显著差异。在某些系统中,它们是完全不同的格式(COFF vs PE),或者它们具有不同的字段(ELF 程序/节 头)。关键的区别在于,在可执行文件中,程序的完整目标代码是存在的(除了共享库中可能存在的代码,如下所述),而目标文件只是生成它们的特定模块的目标代码。这意味着不可执行文件不包含可加载代码。
如前所述,这并不一定意味着“可执行”文件是实际执行的二进制映像;在大多数现代系统中,这是在加载步骤中产生的。在许多情况下,可执行文件仍然包含目标代码,而不是纯粹的机器代码,并且地址位置可能在加载之前无法解析,但它们确实包含工作程序的所有静态链接代码。某些链接器(例如 ld,Unix/Linux 链接器,在生成可执行文件时由 GCC 隐式调用)可以选择 - 甚至像 ld 一样默认为 - 在链接时解析地址,但即使在这种情况下,生成的可执行文件通常包含附加信息以促进加载过程,例如,一个单独的只读数据节、一个可写数据区域的定义(有时称为 .bss 节)、一个定义堆栈区域的节等——并且可能有使用共享库的链接信息。
第三种类型的目标代码文件是库文件,该文件包含多个程序使用的元素,并可用于一般用途。大多数程序使用的大多数函数、变量和其他元素都保存在库中。库与常规目标文件的不同之处主要在于(在大多数系统上)它们的排列方式使得链接器可以从文件中提取库的独立元素,因此只有程序使用的元素包含在由它们生成的可执行文件中。
在当今的大多数系统上,库有两种类型,一种是在链接时直接链接到可执行文件的静态库,另一种是在程序运行的时候进行加载和链接的共享库(在 Windows 世界中也称为动态链接库或 DLL)。主要区别在于共享库,顾名思义,可以同时由多个程序共享,从而降低内存使用量。但是,这样做的代价是,在第一次使用共享库中的元素时,除了加载可执行文件外,还必须加载共享库,然后在运行时将其链接到使用它们的程序。共享库一般会被缓存,以减少加载开销,并且通常在实际使用其中的元素时才加载,这意味着如果使用共享库的程序部分没有被调用,则根本不需要加载库。尽管如此,最终权衡的结果是,不太可能同时被几个程序共享的代码通常被链接为静态库,只有非常常见的元素(例如,标准C和c++库)被动态链接。

3 Relocations

目标文件的很大一部分包含代码及其相关数据。在源代码中,代码包含对其他函数和数据存储的引用。在目标文件中,此类引用被转换为指令和重定位对,因为编译器无法提前知道代码将在哪里结束。例如,x86 上的函数调用看起来像这样(在目标文件中):
反汇编包含调用的操作码 (e8) 加上偏移量 -4 (fc ff ff ff)。如果要执行它,它将调用地址 15,这看起来像是指令的一半。第二行(重定位条目)列出了位置 15(-4)的地址应该固定为 vsnprintf 的地址。这意味着它的地址应该是被调用函数的地址减去重定位的地址。然而,直接输入差异是行不通的,因为调用地址是相对于下一条指令的,而不是操作码中途偏移字节的开始。这就是为什么会有 -4 出现的原因:重定位的结果被添加到正在填充的字段中。通过从地址中减去 4(加 -4),位移变成是相对于指令的结尾的相对位移,并且在它应该去的地方调用结束。在可执行文件中:
调用所需的位移是 vsnprintf 的地址减去下一条指令的地址,即 0x804a1e0 - 0x804a1d9 = 0x7,这是在调用字节中看到的值 (07 00 00 00)。这相当于目标地址减去重定位地址加上存储的值:0x804a1e0 - 0x804a1d5 + -4 = 0x7

3.1 Relocating code

当一个可执行文件被创建时,它将被默认设置为使用一个特定的地址。当您需要同一地址空间中的多个目标文件并且它们可能重叠时,或者您想要执行地址空间随机化时,这可能是一个问题,您可能会发现重新定位可执行文件是一个不错的选择。
由于重定位仅在构建可执行文件时需要,但在运行时不需要,因此它们通常不存在于链接文件中。相反,您需要明确告诉链接器在必要时发出重定位。对于 GCC 交叉编译器,这可以通过 -q 开关来完成。请注意,-i-r 开关具有类似的描述,但会导致链接器生成一个目标文件而不是可执行文件。
当找到差异时,重新定位本身就相当简单。首先将节加载到您选择的位置,然后为每个重定位条目:
  • 计算应用重定位的原始地址
  • 计算现在应用重定位的地址(其移动量与您将原始部分从其原始位置移动的量相同)
  • 对重定位的目的地执行相同的操作
  • 计算重定位值是什么 - 绝对重定位的目的地,以及相对重定位的目的地减去原点。
  • 使用原始位置计算重定位值。
  • 从新值中减去旧值
  • 将结果添加到内存中的原始重定位值。
如果这些部分彼此相对移动,那么重定位就可以变得简单,只需将位移添加到绝对重定位中即可。相对位置不会改变,因为源和目标都移动了相同的量。

4 Common errors

  • Passing -i or -r to ld. It does not work except for some limited cases, as it generates a file where relocations have not been applied at all.
  • Assuming code and data are continuous. A pitfall when trying to make a PE file multiboot-compatible. A section is generally page-aligned (4k), but a PE file is sector-aligned (512b). So if a section is not multiple of 4k in size, relative addresses to the data section will be off by a multiple of 512 bytes as the gap has been removed from the binary. Worse, it is perfectly valid to have metadata sections between the various loadable sections, which can put addresses off.
  • Loading as a flat binary. All executables that aren't flat binaries have a header up front. Blatantly loading a file and starting at the start will execute the header instead of your code. Again, there is a tutorial that tries to get away with this.
  • Assuming the entry point is at the start. The linker has a certain amount of freedom in what order it loads the object files, and so does the compiler. That means that main doesn't need to be at the start of the code section.
wiki.osdev.org 系列之 - Executable Formatswiki.osdev.org 系列之 - PE