Bootstrap

程序的自我修养 第三章ELF文件结构

第三章 ELF文件结构

1、ELF的定义

ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的ELF格式文件包括:可执行文 件、 可重定位文件(.o)、共享目标文件(.so)、核心转储文件等。ELF主要用于Linux平台,Windows下是PE/COFF格式。

2、ELF文件的结构

一个完整的ELF文件一般会包括如下几个内容:ELF头、Section头、Program头和Section。

其中由Section头组成的集合称为Section头表,由Program头组成的集合称为Program头表。注意:数个连续的头称之为头表,头表是虚拟出来的定义,文件中不存在头表,只有头。

一个Section头指向一个Section,Section头中包括所指向Section的名字、类型、其在ELF文件中的偏移地址、大小等信息。

一个Program头指向一个Segment,Program头中包括所指向Segment的类型、其在ELF文件中的偏移地址、大小,映射到内存的虚拟地址等信息。一个Segment由一系列连续的Section构成,连续的Section拥有相同的权限,如只读、读写、可读可执行等;

一个ELF头内包含有:Section头表的在ELF文件中的偏移地址、单个Section头的大小、Section头表中Section头的个数;Program头表的在ELF文件中的偏移地址、单个Program头的大小、Program头表中Program头的个数;该ELF文件的文件属性,包括文件是否可执行是静态链接还是动态链接以及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息

img

 Simpletest.c
 int printf(const char* format,...);        
 int global_init_var = 84;
 int global_uninit_var;
 ​
 void func1(int i){
   printf( "%d\n", i);
 }
 ​
 int main(void){
   static int static_var = 85;
   static int static_var2;//静态整数变量static_var2,但没有初始化。由于是静态变量,所以其值默认初始化为0。
   int a = 1;
   int b;
   func1(static_var + static_var2 + a + b);
   return a;
 }

🏷Readelf命令的使用

通过man readelf指令可以查看 readelf各种参数所代表的含义

 SYNOPSIS
        readelf [-a|--all]
                [-h|--file-header]
                [-l|--program-headers|--segments]
                [-S|--section-headers|--sections]
                [-g|--section-groups]
                [-t|--section-details]
                [-e|--headers]
                [-s|--syms|--symbols]
 $ readelf -h simpletest.o
 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:                              REL (Relocatable file)
   Machine:                           Advanced Micro Devices X86-64
   Version:                           0x1
   Entry point address:               0x0                              
   Start of program headers:          0 (bytes into file)     
   Start of section headers:          1104 (bytes into file)
   Flags:                             0x0
   Size of this header:               64 (bytes)
   Size of program headers:           0 (bytes)
   Number of program headers:         0
   Size of section headers:           64 (bytes)
   Number of section headers:         13
   Section header string table index: 12

从上述输出结果中可得,ELF的文件头包含 ELF魔数、ELF文件类型、文件机器字节长度、数据存储方式、版本、入口地址、程序头入口和长度、段表的位置和长度以及段的数量等参数。

ELF魔数:

ELF文件类型: REL : 可重定位文件,一般为.o文件 ​ EXEC: 可执行文件 ​ DYN: 共享目标文件,一般为.so文件

 $ readelf -S simpletest.o
 There are 13 section headers, starting at offset 0x450:
 ​
 Section Headers:
   [Nr] Name              Type             Address           Offset
        Size              EntSize          Flags  Link  Info  Align
   [ 0]                   NULL             0000000000000000  00000000
        0000000000000000  0000000000000000           0     0     0
   [ 1] .text             PROGBITS         0000000000000000  00000040
        0000000000000057  0000000000000000  AX       0     0     1
   [ 2] .rela.text        RELA             0000000000000000  00000340
        0000000000000078  0000000000000018   I      10     1     8
   [ 3] .data             PROGBITS         0000000000000000  00000098
        0000000000000008  0000000000000000  WA       0     0     4
   [ 4] .bss              NOBITS           0000000000000000  000000a0
        0000000000000004  0000000000000000  WA       0     0     4
   [ 5] .rodata           PROGBITS         0000000000000000  000000a0
        0000000000000004  0000000000000000   A       0     0     1
   [ 6] .comment          PROGBITS         0000000000000000  000000a4
        000000000000002a  0000000000000001  MS       0     0     1
   [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ce
        0000000000000000  0000000000000000           0     0     1
   [ 8] .eh_frame         PROGBITS         0000000000000000  000000d0
        0000000000000058  0000000000000000   A       0     0     8
   [ 9] .rela.eh_frame    RELA             0000000000000000  000003b8
        0000000000000030  0000000000000018   I      10     8     8
   [10] .symtab           SYMTAB           0000000000000000  00000128
        0000000000000198  0000000000000018          11    11     8
   [11] .strtab           STRTAB           0000000000000000  000002c0
        0000000000000079  0000000000000000           0     0     1
   [12] .shstrtab         STRTAB           0000000000000000  000003e8
        0000000000000061  0000000000000000           0     0     1
 Key to Flags:
   W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
   L (link order), O (extra OS processing required), G (group), T (TLS),
   C (compressed), x (unknown), o (OS specific), E (exclude),
   l (large), p (processor specific)
  1. [Nr]: 节的序号(编号),从0开始计数。

  2. Name: 节的名称。标准的节名称包括.text(代码段),.data(已初始化的数据段),.bss(未初始化的数据段),.rodata(只读数据段)等。

  3. Type: 节的类型。常见的类型有:

    • PROGBITS: 程序所需的信息,如代码、数据等。

    • NOBITS: 表示该节在文件中不占用空间,如未初始化的数据段.bss

    • RELA: 重定位表,包含修正代码和数据引用到正确内存地址的信息。

    • SYMTAB: 符号表,包含程序中定义和引用的符号信息。

    • STRTAB: 字符串表,包含符号名称的字符串。

  4. Address: 如果这个ELF文件被加载到内存中,这个字段表示该节在内存中的起始地址。对于未链接的目标文件(如.o文件),这个字段通常是0。

  5. Offset: 该节在ELF文件中的偏移量,即从文件开头到该节开始位置的字节数。

  6. Size: 该节的大小,以字节为单位。

  7. EntSize: 如果节中包含固定大小的项目(如符号表),则此字段表示每个项目的大小。对于不包含固定大小项目的节,此字段为0。

  8. Flags: 节的标志位,用于描述节的属性和权限。常见的标志包括:

    • W: 可写(Writeable)

    • A: 可分配(Allocatable,即加载到内存中)

    • X: 可执行(Executable)

    • M: 可合并(Mergeable,相同值的字符串可以合并)

    • S: 包含字符串数据

    • I: 该节包含的信息对于链接器和其他工具可能很重要

    • L: 保留节在文件中的顺序

    • 其他标志可能表示特定于处理器或操作系统的属性

  9. Link: 如果该节是某种类型的表(如符号表或重定位表),此字段表示相关联的另一个节的索引。例如,对于重定位表,这个字段可能指向符号表。

  10. Info: 提供额外的信息,其含义取决于节的类型。例如,在重定位表中,它可能表示要修正的符号的索引。

  11. Align: 一些节有地址对齐约束,即它们在内存中的地址必须是某个特定数的倍数。这个字段表示对齐的约束,即节在内存中的地址应该是Align的倍数。

注: .rela.text 段类型为”SHT_REL“,也就是重定位表(Relocation Table),其功能为指导链接器如何修正程序中的地址引用。 实际上.bss section并不占据实际的空间,仅仅作为一个占位符。程序运行时,会在内存中分配这些变量,并把初始值设为0。

$ readelf -s simpletest.o

 Symbol table '.symtab' contains 17 entries:
    Num:    Value          Size Type    Bind   Vis      Ndx Name
      0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
      1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS simpletest.c
      2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
      3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
      4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
      5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
      6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1802
      7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1803
      8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
      9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
     13: 0000000000000000    36 FUNC    GLOBAL DEFAULT    1 func1
     14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
     15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
     16: 0000000000000024    51 FUNC    GLOBAL DEFAULT    1 main

注:初始化且初始值为0的静态变量,会因为现代编译器的优化机制存放在.bss段中。

问题1: 为什么未初始化的全局变量不存放在.bss段中

因为Fortran中的COMMON块机制,其中COMMON块用于不同程序之间共享数据,COMMON块中的变量不会立即分配空间, 在编译时候单个单元不知道该全局变量在其他单元的使用情况,例如其是否需要更大的空间,若直接放入.bss段,那么在链接的时候可能出现空间不足或者浪费。

问题2: 为什么局部变量没有出现在符号表中

因为局部变量在运行时栈中被管理,链接器对此类符号不感兴趣,所以局部变量不会出现符号表中。

符号的类型:

其中区别全局符号和局部符号的关键就是static属性,带有static属性的函数以及变量是不能被其他模块引用的。 ​ 对于c语言来说,static属性的功能就是隐藏模块内部的变量以及函数声明。

链接器如何进行符号解析

当编译器遇到一个不是在当前模块中定义的符号时,会假设其在其他模块中有定义,会在符号表中为其生成相应的符号。 ​ 若链接器在其他输入模块中都找不到这个被引用符号的定义就会输出一条错误信息(linkerror),并终止链接操作。 ​ 若可以在其他输入模块找到定义分为三种情况: ​ 1、多个同名的强符号,会报告符号重复定义的错误。 ​ 2、一个强符号和多个同名的弱符号,连接器会选择强符号进行链接,不会出现警告和错误。 ​ 3、多个同名的弱符号,会选择所需空间最大的弱符号进行符号解析,这种情况下可能会导致一些错误

🏷Objdump指令参数

 -a 或 --archive-header:显示档案库的头部信息。
 -d 或 --disassemble:反汇编可执行节。
 -D 或 --disassemble-all:与 -d 类似,但反汇编所有节,而不仅仅是可执行节。
 -f:显示文件头部信息。
 -h:显示节头部信息,包括每个节的名字、大小等。
 -r 或 --reloc:显示重定位条目。
 -s :将所有段的内容以十六进制的方式打印出来。
 -t 或 --syms-only:仅显示符号表(与 -S 类似,但输出格式不同)。
 -x:显示所有头部信息,包括文件头部、节头部和符号表。

🏷Objdump指令具体使用

 $ objdump -h simpletest.o     -h参数用于显示各个Section的头部信息
 simpletest.o:     file format elf64-x86-64
 Sections:
 Idx Name          Size      VMA               LMA               File off  Algn
   0 .text         00000057  0000000000000000  0000000000000000  00000040  2**0
                   CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
   1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                   CONTENTS, ALLOC, LOAD, DATA
   2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                   ALLOC
   3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                   CONTENTS, ALLOC, LOAD, READONLY, DATA
   4 .comment      0000002a  0000000000000000  0000000000000000  000000a4  2**0
                   CONTENTS, READONLY
   5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ce  2**0
                   CONTENTS, READONLY
   6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                   CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  1. Size:段在内存中的大小。

  2. VMA (Virtual Memory Address):段在虚拟内存中的起始地址。

  3. LMA (Load Memory Address):段被加载到内存中的地址。

  4. File off (File Offset):段在文件中的起始位置。

  5. Algn (Alignment):段在内存中的对齐要求。

  6. CONTENTS : 表示该段在文件中存在。

🏷高级语言代码中的各类数据存储在二进制文件的哪个section中

  1. 全局变量和静态变量:

    • 存储位置:已初始化的全局变量和静态变量存储在.data段,未初始化的全局变量和静态变量存储在.bss段。

    • 原因:全局变量和静态变量在整个程序执行期间都存在,因此它们需要存储在可执行文件的某个段中。.data段用于存储已初始化的数据,而.bss段用于存储未初始化的数据,后者在程序加载时会被初始化为零。

  2. 局部变量:

    • 存储位置:不直接存储在ELF文件中,而是在程序运行时存储在栈(stack)上。

    • 原因:局部变量在函数被调用时创建,并在函数返回时销毁。它们的存储位置是在运行时由栈动态分配的,因此不会存储在ELF文件中。

  3. 指向全局变量、静态变量和局部变量的指针:

    • 存储位置:指针变量本身根据其作用域和生命周期存储在.data段、.bss段或栈上。

    • 原因:指针是一种特殊类型的变量,它存储的是内存地址。如果指针是指向全局变量或静态变量的,那么指针变量本身可能存储在.data段或.bss段(取决于它是否被初始化)。如果指针是指向局部变量的,那么指针变量本身将存储在栈上。

  4. 字符串常量:

    • 存储位置:存储在.rodata段(只读数据段)。

    • 原因:字符串常量在程序运行期间不应被修改,因此将它们存储在只读段中可以保护它们不被意外修改。

  5. 代码:

    • 存储位置:存储在.text段。

    • 原因:.text段包含程序的执行代码。这是程序运行时由CPU执行的指令序列。

;