ELF简述

       ELF的全称是Executable Linkable Format,简单理解就是Linux平台下一种二进制文件的格式。本文所涉及到的程序均使用main.c, fun.c, head.h这三个文件。他们编译后生成的可执行文件为res。

main.c

#include<stdio.h>
#include"head.h"
int main(){
    int a = 12;
    int b = 23;
    int c = add(a, b);
    printf("%d\n",c);
    return 0;
}

head.h

int add(int a, int b);

fun.c

int add(int a, int b){
    return (a + b);
}

编译命令:

gcc main.c fun.c -o res


1、ELF常见格式

ELF文件类型 说明 实例
可重定位文件 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 Linux的.o文件
可执行文件 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名 比如/bin/bash文件
共享目标文件 这类文件包含了代码和数据,可以在两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 Linux的.so
核心转储文件 当进程意外终止时,系统可以讲该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 Linux的core dump

2、ELF文件结构

    首先我们看一下ELF文件的总体结构,先有一个大致的了解。

ELF Header
Sections
...
...
...
Section Header Table
String Tables
Symbol Tables

 ELF的结构是由多个段(也叫作Section)组成,每一部分的内容都保存在一个段(Section)中。记住一点是,ELF本质上是二进制文件(ELF本身仅仅是存了一堆0和1的序列而已),所谓的段,其实是我们根据二进制的内容先进行解析,解析后根据存的内容不同,划分出了不同的段。

2.1 ELF Header

ELF Header描述了整个文件的基本属性,ELF Header的数据保存在Elf32_Ehdr结构体中
typedef struct{
	unsigned char e_ident[16];
	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_ephsize;
	Elf32_Half e_phentsize;
	Elf32_Half e_phnum;
	Elf32_Half e_shentsize;
	Elf32_Half e_shnum;
	Elf32_Half e_shstrndex;
} Elf32_Ehdr;

具体这个结构体的成员含义如下表所示(某些值来源于本文所用的res程序):
成员 readelf输出结果与含义
e_ident Magic:7f 454c 46 01 01 01 00 00 00 00 00 00 00 00 00 
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX-System V
ABI Version: 0
e_type Type: EXEC (executable file)
ELF文件类型
e_machine Machine: Intel 80386
ELF文件的CPU平台属性
e_version Version:0x1
ELF版本号。一般为常数1
e_entry Entry point address: 0x8048320
入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完成该程序后,从这个地址开始执行进程的指令。可重定位文件一般没有入口地址,则这个值为0
e_phoff Start of program headers: 52(bytes into file)
程序头表的偏移。参考下文的“连接视图和执行视图”
e_shoff Start of section headers: 4472(bytes into file)
段表在文件中的偏移,也就是从文件的4473个字节开始是段表内容
e_word Flags: 0x0
ELF标志位,用来标志一些ELF文件平台相关的属性。
e_ehsize Size of this header: 52(bytes)
ELF文件头本身的大小
e_phentsize Size of program headers: 32(bytes)
程序头的大小
e_phnum Number of program headers:9
在执行视图中,Segments的数量
e_shentsize Size of section headers:40(bytes)
段表描述符的大小
e_shnum Number of section headers:30
段表描述符的数量。这个值等于ELF文件中拥有的段(section)的数量。
e_shstrndx Section header string table index:27
段表字符串表所在的段在段表中的下标。
   

  以main.c和fun.c生成的res可执行文件为例,我们用linux下的命令

readelf -h res

可以查看具体的ELF Header内容

  1 ELF Header:
  2   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00              //ELF魔数 
  3   Class:                             ELF32                              //文件机器字节长度
  4   Data:                              2's complement, little endian      //数据存储方式
  5   Version:                           1 (current)                        //版本
  6   OS/ABI:                            UNIX - System V                    //运行平台
  7   ABI Version:                       0                                  //ABI版本
  8   Type:                              EXEC (Executable file)             //ELF类型
  9   Machine:                           Intel 80386                        //硬件平台
 10   Version:                           0x1                                //硬件平台版本
 11   Entry point address:               0x8048320                          
 12   Start of program headers:          52 (bytes into file)               //程序头入口和长度
 13   Start of section headers:          4472 (bytes into file)             //段表在文件中的偏移。也就是段表文件从文件的4473个字节开始
 14   Flags:                             0x0
 15   Size of this header:               52 (bytes)
 16   Size of program headers:           32 (bytes)
 17   Number of program headers:         9
 18   Size of section headers:           40 (bytes)
 19   Number of section headers:         30 
 20   Section header string table index: 27

针对魔数(Magic)的16字节数据,用下图大概说明一下各部分含义:



2.2、段表

        段表是ELF文件中除了头文件以外最重要的结构。上面说到ELF文件的内容是放在不同的段中,那么我们的程序在运行的时候,如何知道一个ELF的不同的段是在文件的哪部分呢?这些内容就是由段表来保存的。段表描述了ELF各个段的信息,比如每个段的段命、段的长度、在文件中的偏移、读写权限以及其他属性。ELF文件的段结构就是由段表决定的,诸如编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。段表的位置在ELF文件中由ELF文件头中的e_shoff成员决定,也就是我们用readelf -h res命令看到的结果中的地13行的内容。我们用

readelf -S res
命令可以看到res文件具体的段表信息

  1 There are 30 section headers, starting at offset 0x1178:
  2 
  3 Section Headers:
  4   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  5   [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  6   [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  7   [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  8   [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  9   [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020 04   A  5   0  4
 10   [ 5] .dynsym           DYNSYM          080481cc 0001cc 000050 10   A  6   1  4
 11   [ 6] .dynstr           STRTAB          0804821c 00021c 00004c 00   A  0   0  1
 12   [ 7] .gnu.version      VERSYM          08048268 000268 00000a 02   A  5   0  2
 13   [ 8] .gnu.version_r    VERNEED         08048274 000274 000020 00   A  6   1  4
 14   [ 9] .rel.dyn          REL             08048294 000294 000008 08   A  5   0  4
 15   [10] .rel.plt          REL             0804829c 00029c 000018 08   A  5  12  4
 16   [11] .init             PROGBITS        080482b4 0002b4 000023 00  AX  0   0  4
 17   [12] .plt              PROGBITS        080482e0 0002e0 000040 04  AX  0   0 16
 18   [13] .text             PROGBITS        08048320 000320 0001d2 00  AX  0   0 16
 19   [14] .fini             PROGBITS        080484f4 0004f4 000014 00  AX  0   0  4
 20   [15] .rodata           PROGBITS        08048508 000508 00000c 00   A  0   0  4
 21   [16] .eh_frame_hdr     PROGBITS        08048514 000514 000034 00   A  0   0  4
 22   [17] .eh_frame         PROGBITS        08048548 000548 0000d0 00   A  0   0  4
 23   [18] .init_array       INIT_ARRAY      08049f08 000f08 000004 00  WA  0   0  4
 24   [19] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004 00  WA  0   0  4
 25   [20] .jcr              PROGBITS        08049f10 000f10 000004 00  WA  0   0  4
 26   [21] .dynamic          DYNAMIC         08049f14 000f14 0000e8 08  WA  6   0  4
 27   [22] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
 28   [23] .got.plt          PROGBITS        0804a000 001000 000018 04  WA  0   0  4
 29   [24] .data             PROGBITS        0804a018 001018 000008 00  WA  0   0  4
 30   [25] .bss              NOBITS          0804a020 001020 000004 00  WA  0   0  1
 31   [26] .comment          PROGBITS        00000000 001020 00004f 01  MS  0   0  1
 32   [27] .shstrtab         STRTAB          00000000 00106f 000106 00      0   0  1
 33   [28] .symtab           SYMTAB          00000000 001628 000450 10     29  46  4
 34   [29] .strtab           STRTAB          00000000 001a78 00025b 00      0   0  1
 35 Key to Flags:
 36   W (write), A (alloc), X (execute), M (merge), S (strings)
 37   I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
 38   O (extra OS processing required) o (OS specific), p (processor specific)

在我们的这个res可执行程序中,可以看到一共有30段。其中第一行说段表的开始偏移量是0x1178,这个十六进制表示的就是4472,这个值和ELF Header中是一致的。

2.3 段表字符串表

段表字符串表保存的起始就是这个ELF文件中段名的字符串的内容。段表字符串表本身就是ELF文件中一个普通的段,它的位置则由ELF Header中的最后一个字段e_shstrndx来指定,从上面的ELF Header中可以看到,这个值是27,然后结合readelf查到的段的内容,我们可以看大27是一个叫做.shstrtab的段,这个也就是段表字符串表。所以只要分析了ELF Header,就可以得到段表和段表字符串表的位置,从而解析出这个ELF文件。

2.4 符号表

链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起。在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名。符号表保存的就是这些内容。具体存在.symtab这个段中。

4 链接视图和执行视图

对于ELF文件,还有一个重要的概念就是链接视图和执行视图。这两种视图,说白了就是对一个ELF二进制文件两种不同的解析方式,产生的两种不同的解析结果,叫做两种视图。也就是对同一个ELF文件,我们用不同的眼观看待它,看到的结果是不同的。那为什么对同一个ELF文件,需要两种不同的视图,或者说不同的解析方式呢?这个就要程序运行时的加载过程有些关系了。对于一个可执行程序,比如我们这个文章中的res这个程序,最终其实还是要作为一个进程来运行,一个二进制格式的可执行程序(我们这个res)能和一个进程相关联起来,是因为我们把这个源代码程序映射到了进程的地址空间当中。这种映射的实现,是操作系统在建立一个进程的task_struct的时候,在task_struct的地址空间mm的的结构中建立一个VMA的过程来完成的。

ELF文件被映射到不同的VMA的时候,是以系统页长作为单位的,那么每个段在映射时的长度都是系统页长的整数倍;如果不是,那么多余的页也将占用一个页,我们从上面的res程序的段内容看到,一个res程序,有多达29个段,如果每一个段映射一个一个系统页,内存空间的浪费就会相当严重了。那么能否把ELF可执行程序中的多个段映射到一个VMA中呢?这个方法是可以的,为什么可以多个段合并映射到一个VMA中呢?因为操作系统在装载可执行文件的时候,操作系统实际上并不关心可执行文件各个段所包含的实际内容,操作系统只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行)。ELF文件中,段的权限往往页只有位数不多的几种组合,基本是以下三种:

  • 以代码段为代表的权限可读可执行的段
  • 以数据段和BSS段为代表的权限可读可写的段
  • 以只读数据段为代表的权限为只读的段
所以我们可以把相同权限的段,合并到一起,作为一个段映射到一个VMA中去。注意,这种多对一的映射,只是换了一种方式来看待我们的ELF可执行程序,把原来ELF文件中的多个段,按照权限的相似性,映射到进程地址空间中的一个VMA中,它并不会改变我们ELF文件的内容,这种看待方式也就是ELF的执行视图。可以简单粗暴的理解执行视图就是把链接视图中的多个段合成一个段来看待的一种解析方式。执行视图其实也只有在ELF文件和进程产生某些关系时才会用到,一般情况下,我们描述的ELF文件都是链接视图。
下面这张图描绘的是执行视图和进程地址空间的映射关系:

因为链接视图和执行视图是两种看待ELF文件的不同方式,他们的描述方式也不同。在链接视图中,ELF文件是由不同的段(Section)组成的;而在执行视图中,ELF文件是由不同的段(Segment)组成的。注意他们的中文含义是相同的(有的地方把section翻译成了“节“),本文默认都是描述链接视图的段,如果是执行视图的段会特别说明。
下面这幅图描绘的是两种视图的格式:

我们在2.2中描述了段表的作用,相类似的,描述执行视图段”Segments“的叫做程序头表,他描述了ELF文件该如何被操作系统映射到进程的地址空间,它在ELF文件中的位置就是由ELF Header中的”Start of program headers"来标志。通过从ELF Header找到不同的视图的头部表的开始地址("Start of program headers"和"Start of sectionheaders"),我们可以分别解析出不同视图的具体内容,下面还以我们的这个res程序看一下实际中这两种视图的内容。
首先是链接视图(r命令:read -S res):
  1 There are 30 section headers, starting at offset 0x1178:
  2 
  3 Section Headers:
  4   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  5   [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  6   [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  7   [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  8   [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  9   [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020 04   A  5   0  4
 10   [ 5] .dynsym           DYNSYM          080481cc 0001cc 000050 10   A  6   1  4
 11   [ 6] .dynstr           STRTAB          0804821c 00021c 00004c 00   A  0   0  1
 12   [ 7] .gnu.version      VERSYM          08048268 000268 00000a 02   A  5   0  2
 13   [ 8] .gnu.version_r    VERNEED         08048274 000274 000020 00   A  6   1  4
 14   [ 9] .rel.dyn          REL             08048294 000294 000008 08   A  5   0  4
 15   [10] .rel.plt          REL             0804829c 00029c 000018 08   A  5  12  4
 16   [11] .init             PROGBITS        080482b4 0002b4 000023 00  AX  0   0  4
 17   [12] .plt              PROGBITS        080482e0 0002e0 000040 04  AX  0   0 16
 18   [13] .text             PROGBITS        08048320 000320 0001d2 00  AX  0   0 16
 19   [14] .fini             PROGBITS        080484f4 0004f4 000014 00  AX  0   0  4
 20   [15] .rodata           PROGBITS        08048508 000508 00000c 00   A  0   0  4
 21   [16] .eh_frame_hdr     PROGBITS        08048514 000514 000034 00   A  0   0  4
 22   [17] .eh_frame         PROGBITS        08048548 000548 0000d0 00   A  0   0  4
 23   [18] .init_array       INIT_ARRAY      08049f08 000f08 000004 00  WA  0   0  4
 24   [19] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004 00  WA  0   0  4
 25   [20] .jcr              PROGBITS        08049f10 000f10 000004 00  WA  0   0  4
 26   [21] .dynamic          DYNAMIC         08049f14 000f14 0000e8 08  WA  6   0  4
 27   [22] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
 28   [23] .got.plt          PROGBITS        0804a000 001000 000018 04  WA  0   0  4
 29   [24] .data             PROGBITS        0804a018 001018 000008 00  WA  0   0  4
 30   [25] .bss              NOBITS          0804a020 001020 000004 00  WA  0   0  1
 31   [26] .comment          PROGBITS        00000000 001020 00004f 01  MS  0   0  1
 32   [27] .shstrtab         STRTAB          00000000 00106f 000106 00      0   0  1
 33   [28] .symtab           SYMTAB          00000000 001628 000450 10     29  46  4
 34   [29] .strtab           STRTAB          00000000 001a78 00025b 00      0   0  1
 35 Key to Flags:
 36   W (write), A (alloc), X (execute), M (merge), S (strings)
 37   I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
 38   O (extra OS processing required) o (OS specific), p (processor specific)
链接视图展示出这个ELF文件共有30个段(Sections)。
然后是执行视图(命令:read -l res):
  1 
  2 Elf file type is EXEC (Executable file)
  3 Entry point 0x8048320
  4 There are 9 program headers, starting at offset 52
  5 
  6 Program Headers:
  7   Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  8   PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  9   INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
 10       [Requesting program interpreter: /lib/ld-linux.so.2]
 11   LOAD           0x000000 0x08048000 0x08048000 0x00618 0x00618 R E 0x1000
 12   LOAD           0x000f08 0x08049f08 0x08049f08 0x00118 0x0011c RW  0x1000
 13   DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
 14   NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
 15   GNU_EH_FRAME   0x000514 0x08048514 0x08048514 0x00034 0x00034 R   0x4
 16   GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
 17   GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1
 18 
 19  Section to Segment mapping:
 20   Segment Sections...
 21    00
 22    01     .interp
 23    02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .    rodata .eh_frame_hdr .eh_frame
 24    03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
 25    04     .dynamic
 26    05     .note.ABI-tag .note.gnu.build-id
 27    06     .eh_frame_hdr
 28    07
 29    08     .init_array .fini_array .jcr .dynamic .got
执行视图展示出这个ELF共有9个段(Segments),程序头表的偏移是52,和ELF Header中的值是一致的,而且从结果的19行开始,readelf命令会告诉我们,一个执行视图中的段(Segment)到底会包含哪些链接视图中的段(Section)。在执行视图的这9个段中,只有Type是LOAD类型才会最终被装载映射到地址空间,其他的都只是在装载时起辅助作用的。

参考书籍:
《程序员的自我修养》
《深入理解自算计系统》

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试

关闭