本文是作为 Rust 的练手项目 rself(https://github.com/chengzhycn/rself) 的背景知识记录。rself 使用 Rust 重新造了一个 readelf 的轮子。

ELF全称为Executable and Linkable Format,是Linux上可执行文件的通用文件格式。主要有三种类型的ELF文件:

  • Relocatable file: 存储用于和其它对象文件链接的代码和数据,创建一个可执行文件或者共享对象文件
  • Executable file: 存储的是用于执行的程序。它指定了exec如何创建一个程序的进程镜像
  • Shared object file: 存储了在两种语义下用于链接的代码和数据。第一种是让链接器将共享对象文件和其它的可重定位文件/共享对象文件一起创建一个新的对象文件;第二种是动态链接器将它和可执行文件以及其它的共享对象一起创建一个进程镜像。

ELF构成#

ELF主要由如下几个部分组成:

  • ELF Header: 位于文件的起始,描述了整个文件的组织结构
  • Sections/Segments: 存储了指令、数据、符号表、可重定位信息等。sections 是这段数据在链接时的表现;segments 是数据在执行时的表现。
  • Program Header Table: 用于告诉系统如何创建一个进程镜像。对于可执行程序,Program Header Table是必须的,但是对于可重定位文件就不需要了。
  • Section Header Table: 存储描述文件中sections的信息。每个section都在这个表中有一个条目,存储了如section name, section size等信息。用于链接的文件必须得有Section Header Table,其它的对象文件可有可无。

d02c13c1fa4bd228f698cf355ec47377_MD5

上面的图中虽然描述文件各个部分的位置,但实际上只有ELF Header的位置是固定的,其它的部分在文件中的位置都在ELF Header中指定。

ELF Header#

#define EI_NIDENT 16

typedef struct {
        unsigned char   e_ident[EI_NIDENT];
        Elf32_Half      e_type;
        Elf32_Half      e_machine;
        Elf32_Word      e_version;
        Elf32_Addr      e_entry;
        Elf32_Off       e_phoff;
        Elf32_Off       e_shoff;
        Elf32_Word      e_flags;
        Elf32_Half      e_ehsize;
        Elf32_Half      e_phentsize;
        Elf32_Half      e_phnum;
        Elf32_Half      e_shentsize;
        Elf32_Half      e_shnum;
        Elf32_Half      e_shstrndx;
} Elf32_Ehdr;

typedef struct {
        unsigned char   e_ident[EI_NIDENT];
        Elf64_Half      e_type;
        Elf64_Half      e_machine;
        Elf64_Word      e_version;
        Elf64_Addr      e_entry;
        Elf64_Off       e_phoff;
        Elf64_Off       e_shoff;
        Elf64_Word      e_flags;
        Elf64_Half      e_ehsize;
        Elf64_Half      e_phentsize;
        Elf64_Half      e_phnum;
        Elf64_Half      e_shentsize;
        Elf64_Half      e_shnum;
        Elf64_Half      e_shstrndx;
} Elf64_Ehdr;

ELF header通常由一个Elfxx_Ehdr结构体表示。它存储了当前二进制文件的一些元数据:

  • e_ident: 16字节的数组,存储了文件的一些用来解析文件内容的标识符。依次是:
    • EI_MAG0-3: ELF magic,固定字段,用来识别ELF文件
    • EI_CLASS: File class
    • EI_DATA: File’s data encoding
    • EI_VERSION: File’s version
    • EI_OSABI: OS/ABI identification
    • EI_ABIVERSION: ABI version
    • EI_PAD: Start of padding bytes
    • EI_NIDENT: Size of ei_ident
  • e_type: 可执行文件类型,常见的有:
    • ET_REL: 可重定位文件,值为1
    • ET_EXEC: 可执行文件,值为2
    • ET_DYN:共享对象文件,值为3
    • ET_CORE:core 文件,值为4
  • e_machine: 文件的架构
  • e_version: 对象文件版本,固定值为1
  • e_entry: 系统初始转移控制的虚拟地址,启动进程。如果文件没有关联的entry point,值为0。
  • e_phoff: Program Header Table 在文件中的偏移。如果没有Program Header Table,值为0。
  • e_shoff: Section Header Table 在文件中的偏移。如果没有Section Header Table,值为0。
  • e_flags: processor-specific flags
  • e_ehsize: ELF Header 大小
  • e_phentsize: Program Header Table 中的entry的大小
  • e_phnum: Program Headers 的个数
  • e_shentsize: Section Header Table 中的entry的大小
  • e_shnum: Section Headers 的个数
  • e_shstrndx: 存储Section Header Name的section.shstrtab在Section Header Table中的index

我们可以用readelf来查看可执行文件的ELF header:

readelf -h <executable>

Sections#

typedef struct {
        Elf32_Word        sh_name;
        Elf32_Word        sh_type;
        Elf32_Word        sh_flags;
        Elf32_Addr        sh_addr;
        Elf32_Off        sh_offset;
        Elf32_Word        sh_size;
        Elf32_Word        sh_link;
        Elf32_Word        sh_info;
        Elf32_Word        sh_addralign;
        Elf32_Word        sh_entsize;
} Elf32_Shdr;

typedef struct {
        Elf64_Word        sh_name;
        Elf64_Word        sh_type;
        Elf64_Xword        sh_flags;
        Elf64_Addr        sh_addr;
        Elf64_Off        sh_offset;
        Elf64_Xword        sh_size;
        Elf64_Word        sh_link;
        Elf64_Word        sh_info;
        Elf64_Xword        sh_addralign;
        Elf64_Xword        sh_entsize;
} Elf64_Shdr;

Sections中包括了从一个目标对象文件构建一个可执行文件所需要的所有信息(Sections是在链接时需要,在运行时数据以Segments形式呈现)。每个ELF可执行文件中都有一个Section Header Table。这个table是一个Elfxx_Shdr结构体的数组,每个section一个Elfxx_Shdr。Elfxx_Shdr字段的定义如下:

  • sh_name: section名字,用在section header string table中的index表示
  • sh_type: section类型,常见的类型有:
    • SHT_NULL: 表示这个section header是inactive的,没有一个相关联的section。
    • SHT_PROGBITS: 表示该section存储了程序定义的信息,格式和含义完全取决于程序。
    • SHT_SYMTAB和SHT_DYNSYM: 符号表section
    • SHT_STRTAB: string table section
    • SHT_RELA: 携带显式的addends的重定位section
    • SHT_HASH: symbol hash table
    • SHT_DYNAMIC: 存储动态链接相关的信息。
    • SHT_NOTE: 存储以某种方式标记文件的信息。
    • SHT_NOBITS: 表示该section在文件中不占空间,但在其它方面类似于SHT_PROGBITS。尽管该section不占空间,sh_offset字段还是会包含一个概念上的文件偏移量。
    • SHT_REL: 不携带显式的addends的重定位section。
    • SHT_INIT_ARRAY: 存储了指向初始化函数的指针的数组。
    • SHT_FINI_ARRAY: 存储了指向终止函数的指针的数组。
    • SHT_PREINIT_ARRAY: 同上面一样,存储了一个指针数组。每个指针指向的函数会在所有其它初始化函数之前调用。
  • sh_flags: section attributes
  • sh_addr: 如果该section会在进程的内存镜像中存在,该值表示的是这个section的虚拟首地址。否则该值为0。
  • sh_offset: 该值表示从文件开始到该section第一个字节的偏移。SHT_NOBITS类型的section在文件中不占空间,它的sh_offset表示文件中的一个概念位置。
  • sh_size: section的大小
  • sh_link: section link index,释义取决于section类型
  • sh_info: section extra information,释义取决于section类型
  • sh_addralign: section alignment限制
  • sh_entsize: size of entries contained in section。为0表示section没有存储一个固定条目长度的table。

一些常见的sections如下所示:

  • .text: code
  • .data: 初始化过的数据
  • .rodata: 初始化过的只读数据
  • .bss: 未初始化的数据
  • .plt: PLT(Procedure Linkage Table)(IAT equivalent)
  • .got: 专用于动态链接全局变量的GOT条目
  • .got.plt: 专用于动态链接函数的GOT条目
  • .symtab: 全局符号表
  • .dynamic: 存储动态链接所需的所有信息
  • .dynsym: 专用于动态链接符号的符号表
  • .strtab: string table of .symtab section
  • .dynstr: string table of .dynsym section
  • .interp: RTLD embedded string
  • .rel.dyn: 全局变量 relocation table
  • .rel.plt: 函数 relocation table

Segments#

typedef struct {
        Elf32_Word        p_type;
        Elf32_Off        p_offset;
        Elf32_Addr        p_vaddr;
        Elf32_Addr        p_paddr;
        Elf32_Word        p_filesz;
        Elf32_Word        p_memsz;
        Elf32_Word        p_flags;
        Elf32_Word        p_align;
} Elf32_Phdr;

typedef struct {
        Elf64_Word        p_type;
        Elf64_Word        p_flags;
        Elf64_Off        p_offset;
        Elf64_Addr        p_vaddr;
        Elf64_Addr        p_paddr;
        Elf64_Xword        p_filesz;
        Elf64_Xword        p_memsz;
        Elf64_Xword        p_align;
} Elf64_Phdr;

Segments也被称作 Program Headers,将ELF二进制文件的结构分解为适当的块,以准备加载到内存中的可执行文件。和Section Headers相比,Program Headers在链接时是不需要的。

同样,每个ELF二进制文件都有一个Program Header Table,由每个现有segments一个的Elfxx_Phdr结构体组成。Elfxx_Phdr字段的定义如下:

  • p_type: Segment type,常见的类型如下:
    • PT_NULL: 该segment未使用
    • PT_LOAD: 指向一个loadable segment。
    • PT_DYNAMIC: 动态链接信息。
    • PT_INTERP: 指向了一个null-terminated的路径名称的地址和长度,用来引用一个解释器。这个segment仅仅在可执行文件中有意义。如果存在,它必须优先于任何loadable segment entry。
    • PT_NOTE: 指向一些辅助信息的地址和长度。
    • PT_SHLIB: reserved
    • PT_PHDR: 如果存在,指向了Program Header Table自身在文件和内存中的地址和长度。它也是要优先于任何loadable segment entry的。
    • PT_TLS: 指向一个 Thread-Local Storage 模版。
  • p_flags: Segment attributes
  • p_offset: segment在文件中从文件的第一个字节开始的偏移量
  • p_vaddr: segment在内存中第一个字节的虚拟地址
  • p_paddr: segment的物理地址。由于System V 程序的物理寻址,这个字段在可执行文件和共享对象中是未指定的。
  • p_filesz: segment在磁盘上的大小
  • p_memsz: segment在内存中的大小
  • p_align: segment alignment in memory

Segment的类型有很多。下面是一些常见的类型:

  • PT_NULL: 未分配的segment(通常是Program Header Table的第一个entry)
  • PT_LOAD: loadable segment
  • PT_INTERP: Segment holding .interp section
  • PT_TLS: Thread Local Storage segment(常见在静态链接的二进制中)
  • PT_DYNAMIC: Holding .dynamic section

需要注意的是只有PT_LOAD segments会加载进内存。因此,每个其他的segment都会被映射到一个PT_LOAD segments的内存范围中。

dbfe522f4e2dd6731122ccab518043e5_MD5

Another important aspect of segments is that their offsets and virtual addresses must be congruent modulo the page size and their p_align field must be a multiple of the system page size.

The reason for this alignment is to prevent the mapping of two different segments within a single memory page. This is due to the fact that different segments usually have different access attributes, and these cannot be enforced if two segments are mapped within the same memory page. Therefore, the default segment alignment for PT_LOAD segments is usually a system page size.

符号#

什么是符号#

在程序开发过程中,我们会用名称来指代代码中的对象,如函数、变量。这些信息被称作程序的符号信息。在编译过程中,机器码中的符号引用被翻译成地址和偏移。

但是,编译器的作用不仅限于机器码生成,它还会从源代码中导出符号信息。导出符号信息是为了改善对生成的机器码的可解释性。ELF文件格式定义了存储符号信息的方法,并且提供了本地和外部ELF对象访问这些符号信息的接口。

链接器和调试器是常见的使用符号的工具。在链接时,链接器会和符号表交互来匹配、引用和修改一个ELF对象中的符号。如果没有符号,链接器不知道relocation应该在哪个地方起作用,因此也就没有办法将符号引用匹配到相应的值。调试器则从符号中来识别函数和变量。

ELF 符号结构体#

typedef struct {
        Elf32_Word        st_name;
        Elf32_Addr        st_value;
        Elf32_Word        st_size;
        unsigned char        st_info;
        unsigned char        st_other;
        Elf32_Half        st_shndx;
} Elf32_Sym;

typedef struct {
        Elf64_Word        st_name;
        unsigned char        st_info;
        unsigned char        st_other;
        Elf64_Half        st_shndx;
        Elf64_Addr        st_value;
        Elf64_Xword        st_size;
} Elf64_Sym;

ELF文件格式中,每个符号在指定的符号表中用一个Elfxx_Sym结构体表示:

结构体的成员包括:

  • st_name: 在符号表对应的string table中该符号的名称的index。如果这个字段没有初始化,那么该符号没有名称。
  • st_info: contains symbol bind and type attributes。
    • Binding attributes决定了外部对象引用给定符号时链接的可见性和行为。常见的symbol binds有:
      • STB_LOCAL: 在符号定义的ELF对象之外是不可见ƒ的。这也就意味着同名的local符号可以存在在多个文件中,互不影响。
      • STB_GLOBAL: 符号对所有将要组合的对象文件都是可见的。定义在其中一个文件中的global符号可以满足另外一个文件中对其的未定义引用。
      • STB_WEAK: 表示这也是一个全局符号,但是它的定义是低优先级,可以被覆盖,即可以在多个文件中被定义。
    • 常见的符号类型有:
      • STT_NOTYPE: 符号类型未指定
      • STT_OBJECT: 符号是数据对象,比如变量,数组等
      • STT_FUNC: 符号是代码对象(函数)
      • STT_SECTION: 符号是section,通常用于relocation
    • 为了从st_info中解析对象,定义了一些bitmask:
      • ELF64_ST_BIND(info) ((info) » 4)
      • ELF64_ST_TYPE(info) ((info) & 0xf)
  • st_other: 符号可见性的信息。符号可见性定义了当符号成为一个可执行文件或者共享库的一部分时,该符号是否允许被访问。它和前面symbol binding的区别是符号可见性是在符号的宿主对象上实施,并且应用到所有引用这个符号的外部对象上。而symbol binding是由引用该符号的外部对象指定,并且在链接时实施的。常见的symbol visibilities有:
    • STV_DEFAULT: 默认可见性的符号,其属性由符号的binding类型指定。这类情况下,(可执行文件和共享库中的)全局符号和弱符号默认是外部可访问的,本地符号默认外部是无法被访问的。但是,可见性是STV_DEFAULT的全局符号和弱符号是可被覆盖的。什么意思?举个最典型的例子,共享库中的可见性值为STV_DEFAULTD的全局符号和弱符号是可被可执行文件中的同名符号覆盖的。
    • STV_PROTECTED: 当符号的可见性是STV_PROTECTED时,它是外部可见的,这点跟可见性STV_DEFAULT的一样,但不同的是它是不可覆盖的。这样的符号在共享库中比较常见。不可覆盖意味着如果是在该符号所在的共享库中访问这个符号,那么就一定是访问的这个符号,尽管可执行文件中也会存在同样名字的符号也不会被覆盖掉。规定绑定属性为STB_LOCAL的符号的可见性不可以是STV_PROTECTED。
    • STV_HIDDEN: 该符号是外部无法访问的。这个属性主要用来控制共享库对外接口的数量。需要注意的是,一个可见性为STV_HIDDEN的数据对象,如果能获取到该符号的地址,那么依然是可以访问或者修改该数据对象的。在可重定位文件中,如果一个符号的可见性是STV_HIDDEN的话,那么在链接生成可执行文件或者共享库的过程中,该符号要么被删除,要么绑定属性变成STB_LOCAL。
    • STV_INTERNAL: 该可见性属性的含义可以由处理器补充定义,以进一步约束隐藏的符号。 处理器补充程序的定义应使通用工具可以安全地将内部符号视为隐藏符号。当可重定位对象包含在可执行文件或共享对象中时,可重定位对象中包含的内部符号必须被链接编辑器删除或转换为STB_LOCAL绑定。
    • st_other中只有最后3个bits表示可见性信息,其他的bits暂时没有特殊的意义。所以用如下的宏来获取可见性信息:
      • ELF64_ST_VISIBILITY(o) ((o) & 0x3)
  • st_shndx: 符号表中每个符号entry都和一个section相关联。st_shndx表示当前符号与之相关联的section在section header table中的index。
  • st_value: 这个字段表示当前符号表entry的符号值。根据对象类型的不同这个字段有不同的含义:
    • 对于ET_REL文件,该字段的值表示在st_shndx关联的section中的offset。
    • 对于ET_EXEC和ET_DYN文件,该字段的值表示一个虚拟地址。如果该字段的值是0并且st_shndx中指向的section的类型是SHT_UNDEF,这个符号就是一个导入的relocation,它的值会在运行时由RTLD(ld.so)解析。

符号和字符串表#

一个ELF对象最多包含两个符号表,分别是.symtab和.dynsym。

.symtab是二进制的全局符号表,包含了当前对象所有的符号引用。section .strtab是这个符号表的字符串表,存储了对应符号表中引用的null-terminated的字符串。即.symtab中的Elfxx_Sym entry的index值和其引用的字符串在.strtab中分配的index是一样的。

.dynsym符号表仅存储那些在动态链接中使用的符号。.dynstr是其对应的字符串表。

strip一个二进制文件会将.symtab和.strtab移除,但是不会影响.dynsym和.dynstr,因为运行时做动态链接时RTLD需要这个子集中符号的信息。这些符号能够通过解析位于PT_DYNAMIC segment内DT_SYMTAB entry中的.dynsym表来恢复。

2c06437bc43045db7cc7f7e641bd0535_MD5

重定位(Relocation)#

重定位是将前面提到的符号引用和它对应的符号定义连接起来的机制。举个例子,假设我们的程序中有两个全局变量。如果我们想成功地使用这两个变量,它们在程序虚拟地址空间的地址就需要被解析。同样,如果我们的程序从外部依赖中调用一个函数,计算函数地址也必须在将执行控制转移给它之前。这两个场景就是我们说的重定位发生的地方。

程序编译过程中有一个阶段是可重定位对象(relocatable objects)的生成。可重定位对象包含了将符号引用映射到它的定义所需的所有信息。可重定位对象有3种类型:

  • Generic object files(*.o)
  • Kernel object files(*.ko)
  • Shared object files(*.so)

Generic object files用于静态链接,它是编译过程中的产物,其中所有的符号定义都会变成随后生成的主可执行文件的一部分。很显然,静态链接有它的优缺点。优点是它可以不需要任何外部依赖运行在任何主机上。缺点就是静态链接的可执行文件可能会变得非常庞大,所有的依赖都会变成其自身的一部分。不过在今天内存/存储爆炸的时代,这个缺陷似乎可以忽略不计,越来越多的编程语言如Go/Rust选择了静态编译来提供更好的可移植性。

Kernel objects files是用来在Linux内核中加载的一类可重定位对象。它可以在不重启系统的情况下以模块的形式加载到内核中。

Shared objects files是在运行时链接的可重定位对象,它可以在不同的进程中共享。因此,动态依赖的重定位是在运行时进行的,这是我们常说的动态链接。

Relocation entries#

typedef struct {
        Elf32_Addr        r_offset;
        Elf32_Word        r_info;
} Elf32_Rel;

typedef struct {
        Elf32_Addr        r_offset;
        Elf32_Word        r_info;
        Elf32_Sword        r_addend;
} Elf32_Rela;

typedef struct {
        Elf64_Addr        r_offset;
        Elf64_Xword        r_info;
} Elf64_Rel;

typedef struct {
        Elf64_Addr        r_offset;
        Elf64_Xword        r_info;
        Elf64_Sxword        r_addend;
} Elf64_Rela;

重定位信息存储在ELF对象中指定的relocation sections中。sections中的relocatable entries也有两种结构:Elfxx_Rel和Elfxx_Rela。两者唯一的区别是Elfxx_Rela多了一个r_addend字段。同一个ELF对象里面这两个结构体是互斥的,即如果使用了其中一个结构体,另外一个结构体就不会再使用了。

结构体中字段的含义如下:

  • r_offset: 存储重定位发生的地方。根据ELF对象类型的不同,有不同的解释
    • 对于ET_REL文件,值指向的是重定位发生的section header中的offset
    • 对于ET_EXEC文件,值指向的是重定位影响的虚拟地址
  • r_info: 这个字段中有两个含义,一个是需要被重定位的符号在符号表中的index,和这个符号的重定位类型。这两个信息可以用下面的宏来获取:
    • ELF32_R_SYM(info) ((info)»8)
    • ELF32_R_TYPE(info) ((unsigned char)(info))
    • ELF64_R_SYM(info) ((info)»32)
    • ELF64_R_TYPE(info) ((Elf64_Word)(info))
  • r_addend: 这个字段存储了一个固定的addend,用来计算存储在重定位引用中的值,见重定位类型中解释。

每个relocation section可能会引用另外两个sections:

  • 第一个是与其相对应的符号表。符号表的index可以从relocation section的Elfxx_Shdr中的sh_link字段获取。
  • 如果是ET_REL文件,relocation entry的offset指向的是特定的section中的offset。这个section的index就存储在relocation section header的 sh_info字段中。

重定位类型#

不同的重定位类型之间的区别是重定位的值的计算方式。下面是一些计算过程中可能会引用的变量:

  • A: Addend of Elfxx_Rela entries.
  • B: Image base where the shared object was loaded in process virtual address space.
  • G: Offset to the GOT relative to the address of the correspondent relocation entry’s symbol.
  • GOT: Address of the Global Offset Table
  • L: Section offset or address of the procedure linkage table (PLT, .got.plt).
  • P: The section offset or address of the storage unit being relocated. retrieved via r_offset relocation entry’s field.
  • S: Relocation entry’s correspondent symbol value.
  • Z: Size of Relocations entry’s symbol.

下面是一些常见的重定位类型的suffix:

  • *_NONE: Neglected entry.
  • *_64: qword relocation value.
  • *_32: dword relocation value.
  • *_16: word relocation value.
  • *_8: byte relocation value.
  • *_PC: relative to program counter.
  • *_GOT: relative to GOT.
  • *_PLT: relative to PLT (Procedure Linkage Table).
  • *_COPY: value copied directly from shared object at load-time.
  • *_GLOB_DAT: global variable.
  • *_JMP_SLOT: PLT entry.
  • *_RELATIVE: relative to image base of program’s image.
  • *_GOTOFF: absolute address within GOT.
  • *_GOTPC: program counter relative GOT offset.

Reference#