ELF的全称是Executable Linkable Format,简单理解就是Linux平台下一种二进制文件的格式。本文所涉及到的程序均使用main.c, fun.c, head.h这三个文件。他们编译后生成的可执行文件为res。
main.c
#include
#include"head.h"
int main(){
int a = 12;
int b = 23;
int c = add(a, b);
printf("%d\n",c);
return 0;
}
int add(int a, int b);
int add(int a, int b){
return (a + b);
}
编译命令:
gcc main.c fun.c -o res
ELF文件类型 | 说明 | 实例 |
可重定位文件 | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 | Linux的.o文件 |
可执行文件 | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名 | 比如/bin/bash文件 |
共享目标文件 | 这类文件包含了代码和数据,可以在两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux的.so |
核心转储文件 | 当进程意外终止时,系统可以讲该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | Linux的core dump |
ELF Header Sections ... ... ... Section Header Table String Tables
Symbol Tables
ELF的结构是由多个段(也叫作Section)组成,每一部分的内容都保存在一个段(Section)中。记住一点是,ELF本质上是二进制文件(ELF本身仅仅是存了一堆0和1的序列而已),所谓的段,其实是我们根据二进制的内容先进行解析,解析后根据存的内容不同,划分出了不同的段。
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;
成员 | 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
段表是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)
ELF文件被映射到不同的VMA的时候,是以系统页长作为单位的,那么每个段在映射时的长度都是系统页长的整数倍;如果不是,那么多余的页也将占用一个页,我们从上面的res程序的段内容看到,一个res程序,有多达29个段,如果每一个段映射一个一个系统页,内存空间的浪费就会相当严重了。那么能否把ELF可执行程序中的多个段映射到一个VMA中呢?这个方法是可以的,为什么可以多个段合并映射到一个VMA中呢?因为操作系统在装载可执行文件的时候,操作系统实际上并不关心可执行文件各个段所包含的实际内容,操作系统只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行)。ELF文件中,段的权限往往页只有位数不多的几种组合,基本是以下三种:
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)。
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类型才会最终被装载映射到地址空间,其他的都只是在装载时起辅助作用的。