文章

Object File Format

展示了一个典型的 ELF 可重定位目标文件的格式、典型的 ELF 可执行目标文件的格式、Linux 程序运行时内存映像。

Object File Format

Introduction

目标文件有三种形式:第一种,可重定位目标文件。以一种可以在编译时与其他可重定位目标文件合并起来去创建一个可执行目标文件的格式包含二进制代码和数据;第二种,可执行目标文件。以一种可以被直接复制到内存并执行的格式包含二进制代码和数据;第三种,共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。Windows 使用可移植可执行(Portable Executable, PE)格式。Mac os-x 使用 Mach-0 格式。现代 x86-64 Linux 和 Unix 系统使用可执行可链接格式(Executable and Linkable Format, ELF)。不管是哪种格式,基本的概念是相似的。

Typical ELF relocatable object file

Typical ELF relocatable object file 图-1 典型的 ELF 可重定位目标文件

图-1展示了一个典型的 ELF 可重定位目标文件的格式。ELF header 以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。 ELF header 剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF header 的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如 x86-64)、section header table 的文件偏移,以及 section header table 中条目的大小和数量。不同节的位置和大小是由 section header table 描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。夹在 ELF headersection header table 之间的都是节。一个典型的 ELF 可重定位目标文件包含下面几个节:

  • .text: 已编译程序的机器码。
  • .rodata: 只读数据,比如 printf 语句中的格式化字符串和 switch 语句的跳转表。
  • .data: 已初始化的全局和静态 C 变量。局部 C 变量在运行时被保存在栈中,既不出现在 .data 节中,也不出现在 .bss 节中。
  • .bss: 未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式中区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。在运行时,这些变量会被分配在内存中并初始化为 0。
  • .symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g 选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在 .symtab 中都有一张符号表(除非程序员特意用 STRIP 命令去掉它)。当然,和编译器中的符号表不同,.symtab 符号表不包含局部变量的条目。
  • .rel.text: 一个 .text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要被修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
  • .rel.data: 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
  • .debug: 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的 C 源文件。只有以 -g 选项调用编译器驱动程序时,才会得到这张表。
  • .line: 原始 C 源程序中的行号和 .text 节中机器码之间的映射。只有以 -g 选项调用编译器驱动程序时,才会得到这张表。
  • .strtab: 一个字符串表,其内容包括 .symtab.debug 节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串的序列。

Typical ELF executable object file

图-2展示了一个典型的 ELF 可执行目标文件的格式。可执行目标文件的格式类似于可重定位目标文件的格式。ELF header 描述文件的总体格式,它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。.text.rodata.data 这些节除了已经被重定位到它们最终的运行时内存地址这点以外,其他方面与可重定位目标文件中的节是相似的。.init 节定义了一个小函数,叫做 _init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),所以它不再需要 .rel 节。

Typical ELF executable object file 图-2 典型的 ELF 可执行目标文件

每个 Linux 程序都有一个运行时内存映像,类似于图-3中所示。在 Linux x86-64 系统中,代码段总是从地址 Ox400000 处开始,后面是数据段。运行时堆在数据段之后,通过调用 malloc 库往上增长。堆后面的区域是为共享模块保留的。用户栈总是从最大的合法用户地址($2^{48}-1$)开始,向较小内存地址增长。位于栈之上从地址 $2^{48}$ 开始的区域,是为内核(kernel)中的代码和数据保留的。

Linux x86-64 runtime memory image 图-3 Linux x86-64 运行时内存映像

为了简洁,我们把堆、数据和代码段画得彼此相邻,并且把栈顶放在了最大的合法用户地址处。实际上,由于 .data 段有对齐要求,所以代码段和数据段之间是有间隙的。同时,在给栈、共享库和堆段分配运行时地址的时候,链接器还会使用地址空间布局随机化。虽然每次程序运行时这些区域的地址都会改变,但它们的相对位置是不变的。

References

本文由作者按照 CC BY 4.0 进行授权