ELF格式解析

编译过程

工具链把程序源文件翻译成可执行文件一般经理4个步骤:预处理、编译、汇编、链接。本章节关于静态链接和动态链接的过程主要就是在第4个过程。链接器会将输入目标文件(.o)经过加工后合并成一个输出文件,这里的输出文件以linux的ELF为例。链接器会将相同性质的段合并在一起,如将所有输入文件的\".text\"合并到输出文件的\".text\"段,接着是\".data\"段,\".bss\"段等。

ELF格式

ELF一般有3种类型:

  • 可重定位文件(Reloacatable File):xxx.o。
  • 可执行文件(Executable File):xx.out。
  • 共享目标文件(Sharead object File):xxx.so。

ELF提供了两种视图:链接视图和执行视图。

  • 链接视图: 以section为单位,在链接阶段,如将多个模块的.text合并为一个.text。
  • 执行视图:以segment为单位,在执行阶段,对应于linux的VMA,.text和.radata为一个整体。

ELF可以组成可以分为四类:ELF header、program header table、固定的sections、sections header table。

ELF header

typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half    e_type;
  Elf32_Half    e_machine;
  Elf32_Word    e_version;
  Elf32_Addr    e_entry;  /* Entry point */
  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; //描述了有多少个sections。
  Elf32_Half    e_shstrndx;
} Elf32_Ehdr;

这些字段是 ELF 文件头中的一部分,它们包含了 ELF 文件的基本信息,如文件类型、目标机器架构、入口地址、程序头表和节区头表等,为 ELF 文件的加载和执行提供了必要的信息。

使用命令readelf -h xxx可以读取上面的信息。

program header table

typedef struct elf32_phdr{
  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;

ELF 程序头表(Program Header Table)包含了多个程序头(Program Header)的条目,每个程序头描述了一个段(Segment)的信息。段是 ELF 文件中的逻辑组织单元,包含了可执行代码、数据、只读数据等。程序头表通过指定每个段在文件中和内存中的位置、大小、属性等信息,为系统加载器(Loader)提供了必要的信息,以正确加载和执行 ELF 文件。

使用readelf -l 可以查看有哪些segment

上图中Type 为 LOAD的,表示该节区包含可执行文件或共享库的可加载部分。在加载过程中,这些节区的内容将被加载到内存中。

固定的sections

使用readelf -S 可以查看有哪些sections

  • .text:包含可执行代码的节区,通常用于存储程序的指令。
  • .data:包含已初始化的全局和静态变量的节区,用于存储程序的全局数据。
  • .bss:包含未初始化的全局和静态变量的节区,用于存储程序的全局数据,但在文件中不占用实际空间。
  • .rodata:包含只读数据的节区,通常用于存储常量、字符串等不可修改的数据。
  • .comment:包含编译器生成的注释信息的节区,通常用于存储编译器版本、编译时间等信息。
  • .eh_frame:包含异常处理框架信息的节区,用于支持 C++ 异常处理和调用堆栈展开。
  • .dynsym:包含动态链接符号表的节区,用于存储动态链接所需的符号信息。
  • .shstrtab:包含节区名称字符串的节区,用于存储所有节区的名称。
  • .strtab:包含字符串表的节区,用于存储字符串常量。
  • .symtab:包含符号表的节区,用于存储链接器符号信息。
  • .got:全局偏移表(Global Offset Table),用于存储动态链接所需的全局变量和函数地址。
  • .plt:过程链接表(Procedure Linkage Table),用于实现函数调用的延迟绑定。
  • .rel.xxx:用于存储重定位信息的节区,其中 xxx 表示需要进行重定位的节区名称,如.rel.text,.rel.data。
  • .dynamic:包含动态链接器信息的节区,用于存储动态链接器所需的信息。

.bss 节区存储未初始化的全局和静态变量,这些变量在编译时没有明确赋初始值,因此在 ELF 文件中不需要占用实际空间。相反,当程序被加载到内存中时,操作系统会分配一段 BSS 段的内存,大小由 .bss 节区中所有变量的总大小决定,然后将该内存区域初始化为 0。这种方式可以有效地节省文件大小,节约磁盘空间和加载时间。如果 .bss 节区中的变量在文件中被显式地初始化了,那么它们就会被分配到 .data 节区中,该节区会在文件中占用实际空间。需要注意的是,.bss 节区的存在并不是必须的,编译器可以将未初始化的全局和静态变量直接放入 .data 节区中,但这样会导致 .data 节区的大小增加,从而增加了文件大小和加载时间。因此,使用 .bss 节区可以更好地优化程序的性能和空间占用。

sections header table

typedef struct elf32_shdr {
  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;

ELF文件中section head table (SHT)来描述ELF文件中有哪些具体的sections,每个section描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。

使用命令使用readelf -S 可以查看有section head table信息,与上一节固定的sections对应,实际上section head table就是描述上一节的信息。