计算机原理系列之二 ——– 详解ELF文件

下面我们介绍一种文件格式:ELF格式,全名为可执行和可链接格式(Executable and Linkable Format)。维基百科中这样描述:

在计算机科学中,ELF文件是一种用于可执行文件、目标文件、共享库和核心转储(core dump)的标准文件格式。其中核心转储是指: 操作系统在进程收到某些信号而终止时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试。

一、ELF文件类型

通俗点说由汇编器和链接器生成的文件都属于ELF文件。通常我们接触的ELF文件主要有以下三类:

  • 可重定位文件relocatable file) 它保存了一些可以和其他目标文件链接并生成可执行文件或者共享库的二进制代码和数据。
  • 可执行文件excutable file)它保存了适合直接加载到内存中执行的二进制程序。
  • 共享库文件shared object file)一种特殊的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。

总之,ELF文件是一种文件格式。但凡是一种格式,总要有一些规则,下面我们来介绍ELF文件的格式规则。

二、ELF文件结构

一个典型的ELF文件包括ELF HeaderSectionsSection Header TableProgram Header Table。其位置分布如下图所示:
01-elf
图1 ELF文件构成

1. ELF Header

每个ELF文件都存在一个ELF Header用来描述其结构和组成。ELF Header其实对应的是一个结构体,该结构体定义如下:

#define EI_NIDENT 16

   typedef struct {                     
       unsigned char e_ident[EI_NIDENT];
       uint16_t      e_type;            
       uint16_t      e_machine;         
       uint32_t      e_version;         
       ElfN_Addr     e_entry;           
       ElfN_Off      e_phoff;           
       ElfN_Off      e_shoff;           
       uint32_t      e_flags;           
       uint16_t      e_ehsize;          
       uint16_t      e_phentsize;       
       uint16_t      e_phnum;           
       uint16_t      e_shentsize;       
       uint16_t      e_shnum;           
       uint16_t      e_shstrndx;        
   } ElfN_Ehdr;

其中

ElfN_Addr       Unsigned program address, uintN_t
ElfN_Off        Unsigned file offset, uintN_t

上述结构体的各成员意义如下:

  • e_ident:包含一个magic number、ABI信息,该文件使用的平台、大小端规则
  • e_type:文件类型, 表示该文件属于可执行文件、可重定位文件、core dump文件或者共享库
  • e_machine:机器类型
  • e_version:通常都是1
  • e_entry: 表示程序执行的入口地址
  • e_phoff: 表示Program Header的入口偏移量(以字节为单位)
  • e_shoff: 表示Section Header的入口偏移量(以字节为单位)
  • e_flags: 保存了这个ELF文件相关的特定处理器的flag
  • e_ehsize: 表示ELF Header大小(以字节为单位)
  • e_phentsize: 表示Program Header大小(以字节为单位)
  • e_phnum: 表示Program Header的数量 (十进制数字)
  • e_shentsize: 表示Section Header大小(以字节为单位)
  • e_shnum: 表示Section Header的数量 (十进制数字)
  • e_shstrndx: 表示字符串表的索引,字符串表用来保存ELF文件中的字符串,比如段名、变量名。 然后通过字符串在表中的偏移访问字符串。

例如,machine的值为0x3e, 十进制为62,其代表的意义可以从文件elf-em.h中找到,如下:

#define EM_X86_64  62  /* AMD x86-64 */

表示,这个ELF文件可以运行在x86_64的机器上。

2. Section

在ELF文件中,数据和代码分开存放的,这样可以按照其功能属性分成一些区域,比如程序、数据、符号表等。这些分离存放的区域在ELF文件中反映成section。ELF文件中典型的section如下:

  • .text: 已编译程序的二进制代码
  • .rodata: 只读数据段,比如常量
  • .data: 已初始化的全局变量和静态变量
  • .bss: 未初始化的全局变量和静态变量,所有被初始化成0的全局变量和静态变量
  • .sysmtab: 符号表,它存放了程序中定义和引用的函数和全局变量的信息
  • .debug: 调试符号表,它需要以'-g'选项编译才能得到,里面保存了程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C文件
  • .line: 原始的C文件行号和.text节中机器指令之间的映射
  • .strtab: 字符串表,内容包括.symtab.debug节中的符号表

特殊的,
1)对于可重定位的文件,由于在编译时,并不能确定它引用的外部函数和变量的地址信息,因此,编译器在生成目标文件时,增加了两个section:

  • .rel.text 保存了程序中引用的外部函数的重定位信息,这些信息用于在链接时重定位其对应的符号。
  • .rel.data 保存了被模块引用或定义的所有全局变量的重定位信息,这些信息用于在链接时重定位其对应的全局变量。

2)对于可执行文件,由于它已经全部完成了重定位工作,可以直接加载到内存中执行,所以它不存在.rel.text.rel.data这两个section。但是,它增加了一个section:

  • .init: 这个section里面保存了程序运行前的初始化代码

上述描述的各个文件中包含的这些section是必须存在的,当然除了这些section,每种文件还有一些其他的section用来存放编译器或者链接器所需要的辅助信息,详情请参考参考阅读1,在这里就不过多的讨论了。

3. Section Header Table

上述各个section的大小和位置等具体信息的存放是由Section Header Table来描述的。Section Header Table是一个结构体数组,对应的结构体定义如下:

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

其中各成员的意义如下:

  • sh_name:表示该section的名字相对于.shstrtab section的地址偏移量。
  • sh_type:表示该section中存放的内容类型,比如符号表,可重定位段等。
  • sh_flags: 表示该section的一些属性,比如是否可写,可执行等。
  • sh_addr: 表示该section在程序运行时的内存地址
  • sh_offset: 表示该section相对于ELF文件起始地址的偏移量
  • sh_size: 表示该section的大小
  • sh_link: 配合sh_info保存section的额外信息
  • sh_info:保存该section相关的一些额外信息
  • sh_addralign:表示该section需要的地址对齐信息
  • sh_entsize:有些section里保存的是一些固定长度的条目,比如符号表。对于这些section来讲,sh_entsize里保存的就是条目的长度。

4. Program Header Table

前面讲过了,section基本是按照目标文件内容的功能来划分的一些区域,而根据其内容在内存中是否可读写等属性,又可以将不同的section划分成不同的segment。其中每个segment可以由一个或多个section组成。其关系如图1所示。

在可执行文件中,ELF header下面紧接着就是Program Header Table。它描述了各个segment在ELF文件中的位置以及在程序执行过程中系统需要准备的其他信息。它也是用一个结构体数组来表示的。具体代码如下:

typedef uint64_t  Elf64_Addr;
typedef uint64_t  Elf64_Off;
typedef uint32_t  Elf64_Word;
typedef uint64_t  Elf64_Xword;

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

各个字段的具体含义如下:

  • p_type:描述了当前segment是何种类型的或者如何解释当前segment,比如是动态链接相关的或者可加载类型的等
  • p_flags:保存了该segment的flag
  • p_offset:表示从ELF文件到该segment第一个字节的偏移量
  • p_vaddr:表示该segment的第一个字节在内存中的虚拟地址
  • p_paddr:对于使用物理地址的系统来讲,这个成员表示该segment的物理地址
  • p_filesz:表示该segment的大小,以字节表示
  • p_memsz:表示该segment在内存中的大小,以字节表示
  • p_align:表示该segment在文件中或者内存中需要以多少字节对齐

三、实践

为了进一步加深对ELF文件整体结构的理解,我们取一个64bit的linux上的可执行文件hello来验证。
首先,使用READELF工具查看其ELF Header信息:

$ readelf -h hello
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6616 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

从ELF Header中我们得到如下信息:

  • ELF Header大小(Size of this header)为64 byte
  • Program Header Table的起始地址(Start of program headers)是64 byte,其内部共有9个(Number of program headers)Program Header,每个大小为56 byte(Size of program headers
  • Section Header Table的起始地址(Start of section headers)是6616 byte,其内部共有31个(Number of section headers)Section Header,每个大小为64 byte(Size of this header

其次,使用READELF命令读取section header table信息:

$ readelf -S hello
There are 31 section headers, starting at offset 0x19d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4

    ... snip ...

  [27] .comment          PROGBITS         0000000000000000  00001038
       0000000000000035  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  000018cc
       000000000000010c  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  00001070
       0000000000000648  0000000000000018          30    47     8
  [30] .strtab           STRTAB           0000000000000000  000016b8
       0000000000000214  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

根据上述section信息,我们得到:

  • section的起始地址是0x238(第一个section .interp的起始地址)
  • section的结束地址是.shstrtab的结束地址即:0x18cc + 0x10c = 0x19d8

根据我们上面的结论,我们可以得到该ELF文件的组成图如下:

name start_addr size end_addr
ELF Header 0 0x40
Program Header Table 0x40 0x1F8
Section 0x238 0x19D8
Section Header Table 0x19D8 0x7C0

我们可以计算出各部分的end_addr上述空白部分,得到:

cs02-table

显然,各个section正好和下一个section首尾相接。然后,通过ls -l hello命令查看该二进制文件的大小,得到该文件大小为8600 byte,而Section Header Table的end_addr 0x2198换算成十进制大小正好是8600 byte。从而证明我们上述的分析是没问题的。

四、总结

  1. ELF文件包括可重定位文件、可执行文件、共享库和Core dump文件。其基本结构包括ELF Header,Program Header Table,Sections和Section Header Table。其中不同的sections组成了不同的segment。
  2. ELF Header描述了该ELF文件中Program Header Table的位置、大小,Section Header Table的位置、大小和程序执行的入口地址等信息。
  3. Program Header Table描述了各个segment的类型、虚拟地址、相对文件的偏移地址等信息。
  4. Section Header Table描述了各个section的名字、类型、相对文件的偏移地址等信息。

五、参考阅读

  1. Executable and Linkable Format (ELF)
  2. UNIX/LINUX 平台可执行文件格式分析
  3. ELF man page
  4. 打造史上最小可执行ELF文件
  5. SYSTEM V:APPLICATION BINARY INTERFACE Edition 4.1
  6. Linux内核如何装载和启动一个可执行程序

“计算机原理系列之二 ——– 详解ELF文件”的2个回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注