Object Files
目标文件基本上由编译和汇编的代码、数据以及使其内容可用所需的所有附加信息组成。 在构建操作系统的过程中,您将使用大量的对象文件。 虽然对于常见的开发任务,您不需要知道它们的确切细节,但是当您想要创建或使用具有各种细节的任务时,细节可能非常重要。
注: 术语“对象文件”与“面向对象编程”的高级概念无关。 对象文件比OOP的最早形式(大约1966年的“参与者模型”)早了十多年,这个术语在1958年或更早的时候在IBM使用。
核心概念
目标文件是包含“目标代码”的三种文件类型之一, 一种机器代码的修改形式,其中包括允许链接和重新定位最终加载的可执行文件的附加信息。
在大多数情况下,编译器或汇编程序将生成目标代码作为其最终结果,而不是真正的可执行二进制文件。 虽然大多数汇编程序和一些编译器都有生成原始二进制映像的选项,但这通常仅适用于引导加载程序、只读内存芯片和其他特殊用途的可执行文件。 实际上,今天几乎所有系统都使用对象文件和可重定位可执行文件。 即使是目前常用的最简单的文件格式[1],不是纯二进制可执行映像; MS-DOS加载器将段的前0x100字节用于[2], 因此,段图像的该部分被排除在文件中。
今天的大多数系统都有更为复杂的对象格式,其中地址信息被某种存根或符号取代, 以及包含有关外部可见函数、变量等的相对位置的信息。 这有助于实现
- 链接,其中两个或多个对象和/或库文件组合形成一个可执行文件,以及
- 加载,其中地址存根由加载程序替换为代码将驻留在进程内存中的实际内存位置。
对象文件、可执行文件和库文件
Wikipedia认为可执行文件是对象文件的一个子集,基于两者都包含对象代码而不是二进制图像,两者存在显著差异。 在某些系统中,它们是完全不同的格式(COFF vs PE),或者具有不同的字段(ELF程序/节标题)。 关键的区别在于,在可执行文件中,存在程序的完整目标代码(除了共享库中可能存在的代码,如下所述),而目标文件只是生成它们的特定模块的目标代码。 这意味着非可执行文件不包含可加载代码。
如前所述,这并不一定意味着“可执行”文件是执行的实际二进制图像; 在大多数现代系统中,这是在加载步骤中产生的。 在许多情况下,可执行文件仍然包含目标代码,而不是纯机器代码,地址位置可能在加载之前无法解析, 但它们“确实”包括工作程序的所有静态链接代码。 某些链接器(例如 LD,Unix/Linux链接器,在生成可执行文件时由GCC隐式调用)具有以下选项:- or even default to, as ld does - 在链接时解析地址,但即使在这种情况下, the executable files generated usually contain additional information to facilitate the loading process - e.g., a separate read-only data section, a definition of the writable data area (sometimes called the .bss section), a section defining the stack area, etc. - 并且可能有使用共享库的链接信息。
第三种类型的目标代码文件是“库文件”,该文件包含多个程序使用的元素,可供一般使用。 大多数程序使用的大多数函数、变量和其他元素都保存在库中。 库与常规对象文件的区别主要在于(在大多数系统上),它们的排列方式使链接器可以从文件中提取库的独立元素, 因此,只有程序使用的元素才会包含在由它们生成的可执行文件中。
在今天的大多数系统上,库有两种类型,“静态库”,它们在链接时直接链接到可执行文件中, 和“共享库”(在Windows世界中也称为“动态链接库”或DLL),它们在运行时加载并链接到使用它们的程序。 主要区别在于,顾名思义,共享库可以由多个程序同时共享,从而降低内存使用率。 但是,当第一次使用可执行文件中的元素时,除了加载可执行文件外,还必须加载共享库, 然后在运行时将其链接到使用它们的程序。 共享库通常是缓存的,以减少加载开销, 并且通常在实际使用它们中的元素之前不会加载,这意味着如果没有调用使用共享库的程序部分,则根本不需要加载库。 然而,权衡是这样的,通常不可能由多个程序共享的代码通常被链接为静态库,而只有非常常见的元素(例如,标准C和C++库)被动态链接。
重新安置
对象文件的大部分包含代码及其相关数据。 在源代码中,代码包含对其他函数和数据存储的引用。 在目标文件中,这样的引用被转换成指令和重定位对,因为编译器无法提前知道代码将在哪里结束。 例如,x86上的函数调用如下所示(在对象文件中):
14: e8 fc ff ff ff call 15 <sprintf+0x15> 15: R_386_PC32 vsnprintf
反汇编包含用于调用的操作码(e8)加上偏移量-4(fc ff)。 如果要执行此命令,它将调用地址15,这看起来像是指令执行了一半。 第二行(重定位条目)列出了位置15(The-4)处的地址应固定为vsnprintf地址的位移。 这意味着它应该得到被调用函数的地址减去重新定位的地址。 但是,空白输入差异将不起作用,因为调用地址是相对于下一条指令的,而不是操作码中间偏移字节的开始。 这就是-4的作用:重新定位的结果被添加到被填充的字段中。 通过从地址中减去4(加-4),位移变为相对于指令末尾的位移,调用在它应该到达的地方结束。 在可执行文件中:
804a1d4: e8 07 00 00 00 call 804a1e0 <vsnprintf> 804a1d5: R_386_PC32 vsnprintf 804a1d9: c9 leave
(...) 0804a1e0 <vsnprintf>:
调用所需的位移是vsnprintf的地址减去下一条指令的地址, i.e. 0x804a1e0 - 0x804a1d9 = 0x7, 这是调用字节中的值(07 00)。 这相当于目标地址减去重新定位地址加上存储的值: 0x804a1e0 - 0x804a1d5 + -4 = 0x7.
Relocating code
When an executable is created, it will be set to use a specific address by default. This can be a problem when you need several object files in the same address space and they may overlap, or you want to perform address space randomization, you might find relocating an executable an option.
Since relocations are only needed to build an executable, but not when you run it, they normally aren't present in a linked file. Instead you need to specifically tell the linker to emit relocations when necessary. For the GCC Cross-Compiler, this can be done with the -q switch. Note that the -i and -r switches have a similar description, but cause the linker to yield an object file rather than an executable.
Relocating is of itself fairly straightforward by finding the differences. Start with loading the sections to the location of your choice, then for each relocation entry:
- compute the original address where the relocation was applied
- compute the address where the relocation applies now (its moved by the same amount you moved the original section from its original location)
- do the same for the destination of the relocation
- compute what the relocation value is - the destination for absolute relocations, and the destination minus the origin for relative relocations.
- compute what the relocation value was using the original location.
- subtract the old value from the new value
- add the result to the original relocation value in memory.
If the sections are moved relatively to each other, then relocating can become as simple as only adding the displacement to the absolute relocations. The relative locations do not get changed as both the source and the target are moved by the same amount.
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.