扒一扒ELF文件

ELF文件(Executable Linkable Format)是一种文件存储格式。Linux下的目标文件和可执行文件都按照该格式进行存储,有必要做个总结。

1. 链接举例

  在介绍ELF文件之前,我们先看下,一个.c程序是如何变成可执行目标文件的。下面举个例子。

  该程序由main.c和sum.c两个模块组成。sum.c接收数组和数组长度两个参数,最后将数组求和的结果返回。main.c调用sum函数,并传递一个两元素的int数组array,将计算结果保存在val中。

//main.c
int sum(int *a, int n);

int array[2] = {1, 2};

int main(int argc, char** argv)
{
    int val = sum(array, 2);
    return val;
}
//sum.c
int sum(int *a, int n)
{
    int i, s = 0;

    for (i = 0; i < n; i++) {
        s += a[i];
    }
    return s;
}

  让我们来看看如果我们使用GCC编译两个模块会发生什么?

  main.c和sum.c将分别通过翻译器将源文件处理为可重定位的目标文件main.o和sum.o。翻译器处理的过程包括了预处理(ccp)、编译(ccl)、汇编(as)三个过程。最后,链接器(ld)将可重定位的目标文件main.o和sum.o以及一些必要的系统文件组合起来,创建一个可执行目标文件prog。具体过程如下图所示。

链接过程

  由上面的过程,我们可以看出在经过汇编器后会输出一个.o文件,这个叫做可重定位的目标文件。将main.o和sum.o输入链接器后,链接器输出的prog文件叫做可执行目标文件。那这两个目标文件有什么样的区别呢?

2. ELF文件类型

2.1 可重定位目标文件(.o文件)

  包含二进制代码和数据,其形式可以和其他目标文件进行合并,创建一个可执行目标文件。例如lib*.o文件。

2.2 可执行目标文件(a.out文件)

  包含二进制代码和数据,可直接被加载器加载执行。例如编译好的可执行文件a.out。

2.3 共享对象文件(.so文件)

  用于和其他共享目标文件或者可重定位文件一起生成ELF目标文件或者和执行文件一起创建进程映像,例如lib*.so文件。

3. ELF文件作用

  ELF文件参与程序的连接(建立一个程序)和程序的执行(运行一个程序),所以可以从不同的角度来看待ELF格式的文件:

  1.如果用于编译和链接(可重定位文件),则编译器和链接器将把ELF文件看作是节头表描述的节的集合,程序头表可选。

  2.如果用于加载执行(可执行文件),则加载器则将把ELF文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头表可选。

4. ELF文件格式

4.1 从编译和链接角度看ELF文件(可重定位目标文件)

从编译和链接角度看ELF文件

ELF头

  每个ELF文件都必须存在一个ELF_Header,这里存放了很多重要的信息用来描述整个文件的组织,如: 版本信息,入口信息,偏移信息等。程序执行也必须依靠其提供的信息。

段头表

  段头表。存放的是所有不同段将在内存中的位置

.text section

  代码段。存放已编译程序的机器代码,一般是只读的。

.rodata section

  只读数据段。此段的数据不可修改,存放常量。比如,printf中的格式化语句。

.data section

  数据段。存放已初始化的全局变量、常量。

.bss section

  bss段。未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。目标文件格式区分初始化和非初始化是为了空间效率.

从编译和链接角度看ELF文件

.symtab section

  符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.rel.txt section

  .text节的重定位信息,用于重新修改代码段的指令中的地址信息。

.rel.data section

  .data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息。

.debug section

  调试用的符号表。

.strtab section

  包含 symtab和 debug节中符号及节名。

节头部表

  每个节的节名、偏移和大小。

  以下是32位系统对应的节头表数据结构,说明了每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。

typedef struct {
    Elf32_Word sh_name;   //节名字符串在.strtab节(字符串表)中的偏移
    Elf32_Word sh_type;   //节类型:无效/代码或数据/符号/字符串/...
    Elf32_Word sh_flags;  //节标志:该节在虚拟空间中的访问属性
    Elf32_Addr sh_addr;   //虚拟地址:若可被加载,则对应虚拟地址
    Elf32_Off  sh_offset; //在文件中的偏移地址,对.bss节而言则无意义
    Elf32_Word sh_size;   //节在文件中所占的长度
    Elf32_Word sh_link;   //sh_link和sh_info用于与链接相关的节(如 .rel.text节、.rel.data节、.symtab节等)
    Elf32_Word sh_info;
    Elf32_Word sh_addralign; //节的对齐要求
    Elf32_Word sh_entsize;   //节中每个表项的长度,0表示无固定长度表项
} Elf32_Shdr;

  使用readelf命令命令查看节头表内容

[ubuntu@localhost interpositioning]$ readelf -S main.o
There are 13 section headers, starting at offset 0x3f8:

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
       0000000000000071  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002d0
       0000000000000090  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000b1
       0000000000000049  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  000000b1
       000000000000000c  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  000000b1
       0000000000000019  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000ca
       0000000000000035  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ff
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  00000100
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000360
       0000000000000030  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000390
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  00000158
       0000000000000150  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  000002a8
       0000000000000023  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)

  可重定位目标文件中,每个可装入节的起始地址总是0。

  .bss节应占000000000000000c大小,但只有装入内存时才会分配。

4.2 从程序执行角度看ELF文件(可执行文件)

从程序执行角度看ELF文件

  与可重定位文件的不同

  1.ELF头中字段 e_entry给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0。

  2.多一个init节,用于定义init函数,该函数用来进行可执行目标文件开始执行时的初始化工作。

  3.少两.rel节(无需重定位)。

  4.多一个程序头表,也称段头表,是一个结构数组。

  使用readelf命令查看ELF头的内容:

[ubuntu@localhost interpositioning]$readelf -h main.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:          1064 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           32 (bytes)         //程序头表每项32B
  Number of program headers:         8                  //程序头表共8项
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10                //.strtab在节头表中的索引

  装入内存时,ELF头、程序头表、.init节、.rodata节会被装入只读代码段。.data节和.bss节会被装入读写数据段。

  段头表能够描述可执行文件中的节与虚拟空间中的存储段之间的映射关系。一个表项32B,说明虚拟地址空间中一个连续的片段或一个特殊的节。以下是32位系统对应的段头表数据结构:

typedef struct {
    Elf32_Word p_type;   //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
    Elf32_Off p_offset;  //此成员给出从文件头到该段第一个字节的偏移
    Elf32_Addr p_vaddr;  //此成员给出段的第一个字节将被放到内存中的虚拟地址
    Elf32_Addr p_paddr;  //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
    Elf32_Word p_filesz; //此成员给出段在文件映像中所占的字节数。可以为0。
    Elf32_Word p_memsz;  //此成员给出段在内存映像中占用的字节数。可以为0。
    Elf32_Word p_flags;  //此成员给出与段相关的标志。
    Elf32_Word p_align;  //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;

  使用readelf命令某可执行目标文件的程序头表

[ubuntu@localhost interpositioning]$readelf -l main

Elf file type is EXEC (Executable file)
Entry point 0x400550
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000008ac 0x00000000000008ac  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000240 0x0000000000000248  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000780 0x0000000000400780 0x0000000000400780
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

  程序头表信息有9个表项,其中两个为可装入段(即Type=LOAD):

  第一可装入段(第15,16行):第0x00000~0x0x8ab的长度为0x8ac字节的ELF头、程序头表、.init、.text和.rodata节,映射到虚拟地址0x400000开始长度为0x8ac字节的区域 ,按0x200000=2MB对齐,具有只读/执行权限(Flg=RE),是只读代码段。

  第二可装入段(第17,18行):第0xe10~0x104f的长度为0x240字节的.data节和磁盘中不占存储空间的.bss节,映射到虚拟地址0x600e10开始长度为0x248字节的存储区域,在0x248=584B存储区中,前0x240=576B用.data节内容初始化,后面584-576=8B对应.bss节,初始化为0 ,按0x200000=2MB对齐,具有可读可写权限(Flg=RW),是可读写数据段。

  由此看出.bss节在文件中不占用磁盘空间,但在存储器中需要给它分配相应大小的空间

5.总结

  1.链接处理涉及到三种目标文件格式:可重定位目标文件、可执行目标文件和共享目标文件。共享库文件是一种特殊的可重定位目标。

  2.ELF目标文件格式可以从编译链接角度程序执行角度两个角度看,前者是可重定位目标格式,后者是可执行目标格式。从编译链接角度看,可重定位目标文件中包含ELF头、各个节以及节头表。可执行目标文件中包含ELF头、程序头表(段头表)以及各种节组成的段。

  3.bss段在可执行目标文件中不会有它的空间,只有当可执行目标文件装载运行时,才会被分配内存(并且位于data段内存块之后),并且初始化为0

本文参考

《深入理解计算机系统》

posted @ 2021-01-30 23:03  学习,积累,成长  阅读(866)  评论(0编辑  收藏  举报