可执行文件elf的格式
By WSW
机器执行的是机器指令,而机器指令就是一堆二进制的数字。高级语言编写的程序之所以可以在不同的机器上移植就因为有为不同机器设计的编译器的存在。高级语言的编译器就是把高级语言写的程序转换成某个机器能直接执行的二进制代码。以上的知识在我们学习CS(Computer Science)的初期,老师都会这么对我们讲。但是我就产生疑问了:既然机器都是执行的二进制代码,那么是不是说只要硬件相互兼容,不同操作系统下的可执行文件可以互相运行呢?答案肯定是不行。这就要谈到可执行文件的格式问题。
每个操作系统都会有自己的可执行文件的格式,比如以前的Unix®是用a.out格式的,现代的Unix®类系统使用elf格式, WindowsNT®是使用基于COFF格式的可执行文件。那么最简单的格式应该是DOS的可执行格式,严格来说DOS的可执行文件没有什么格式可言,就是把二进制代码安顺序放在文件里,运行时DOS操作系统就把所有控制计算机的权力都给了这个程序。这种方式的不足之处是显而易见的,所以现代的操作系统都有一种更好的方式来定义可执行文件的格式。一种常见的方法就是为可执行文件分段,一般来说把程序指令的内容放在.text段中,把程序中的数据内容放在. data段中,把程序中未初始化的数据放在.bss段中。这种做法的好处有很多,可以让操作系统内核来检查程序防止有严重错误的程序破坏整个运行环境。比如:某个程序想要修改.text段中的内容,那么操作系统就会认为这段程序有误而立即终止它的运行,因为系统会把.text段的内存标记为只读。在. bss段中的数据还没有初始化,就没有必要在可执行文件中浪费储存空间。在.bss中只是表明某个变量要使用多少的内存空间,等到程序加载的时候在由内核把这段未初始化的内存空间初始化为0。这些就是分段储存可执行文件的内容的好处。
下面谈一下Unix系统里的两种重要的格式:a.out和elf(Executable and Linking Format)。这两种格式中都有符号表(symbol table),其中包括所有的符号(程序的入口点还有变量的地址等等)。在elf格式中符号表的内容会比a.out格式的丰富的多。但是这些符号表可以用 strip工具去除,这样的话这个文件就无法让debug程序跟踪了,但是会生成比较小的可执行文件。a.out文件中的符号表可以被完全去除,但是 elf中的在加载运行是起着重要的作用,所以用strip永远不可能完全去除elf格式文件中的符号表。但是用strip命令不是完全安全的,比如对未连接的目标文件来说如果用strip去掉符号表的话,会导致连接器无法连接。例如:
$:gcc -c hello.c $:ls hello.c hello.o
$:strip hello.o
$:gcc hello.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o: In function `_start': init.c: (.text+0x18) : undefined reference to `main' collect2: ld returned 1 exit status
$:cat hello.c main( ) { printf("Hello Worldn"); }
struct exec { unsigned long a_info; /*Use macros N_MAGIC, etc for access */ unsigned a_text; /* length of text, in bytes */ unsigned a_data; /* length of data, in bytes */ unsigned a_bss; /* length of uninitialized data area for file, in bytes*/ unsigned a_syms; /* length of symbol table data in file, in bytes */ unsigned a_entry; /* start address */ unsigned a_trsize; /*length of relocation info for text, in bytes */ unsigned a_drsize; /*length of relocation info for data, in bytes */ };
$:cat hello.c main() { printf("Hello Worldn"); } $:gcc-elf -c hello.c
$:readelf -h hello.o ELF Header: Magic: 7f 45 4c 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 Type: REL (Relocatable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 256 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 11 Section header string table index: 8
$:readelf -S hello.o There are 11 section headers, starting at offset 0x100: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00002a 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000370 000010 08 9 1 4 [ 3] .data PROGBITS 00000000 000060 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000060 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000060 00000e 00 A 0 0 1 [ 6] .note.GNU-stack PROGBITS 00000000 00006e 000000 00 0 0 1 [ 7] .comment PROGBITS 00000000 00006e 00003e 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 0000ac 000051 00 0 0 1 [ 9] .symtab SYMTAB 00000000 0002b8 0000a0 10 10 8 4 [10] .strtab STRTAB 00000000 000358 000015 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
$:readelf -r hello.o Relocation section '.rel.text' at offset 0x370 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0000001f 00000501 R_386_32 00000000 .rodata 00000024 00000902 R_386_PC32 00000000 printf
$:gcc hello.o $:readelf -S a.out There are 32 section headers, starting at offset 0xbc4: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4 [ 3] .hash HASH 08048168 000168 00002c 04 A 4 0 4 [ 4] .dynsym DYNSYM 08048194 000194 000060 10 A 5 1 4 [ 5] .dynstr STRTAB 080481f4 0001f4 000060 00 A 0 0 1 [ 6] .gnu.version VERSYM 08048254 000254 00000c 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 08048260 000260 000020 00 A 5 1 4 [ 8] .rel.dyn REL 08048280 000280 000008 08 A 4 0 4 [ 9] .rel.plt REL 08048288 000288 000010 08 A 4 11 4 [10] .init PROGBITS 08048298 000298 000017 00 AX 0 0 4 [11] .plt PROGBITS 080482b0 0002b0 000030 04 AX 0 0 4 [12] .text PROGBITS 080482e0 0002e0 0001b4 00 AX 0 0 16 [13] .fini PROGBITS 08048494 000494 00001a 00 AX 0 0 4 [14] .rodata PROGBITS 080484b0 0004b0 000016 00 A 0 0 4 [15] .eh_frame PROGBITS 080484c8 0004c8 000004 00 A 0 0 4 [16] .ctors PROGBITS 080494cc 0004cc 000008 00 WA 0 0 4 [17] .dtors PROGBITS 080494d4 0004d4 000008 00 WA 0 0 4 [18] .jcr PROGBITS 080494dc 0004dc 000004 00 WA 0 0 4 [19] .dynamic DYNAMIC 080494e0 0004e0 0000c8 08 WA 5 0 4 [20] .got PROGBITS 080495a8 0005a8 000004 04 WA 0 0 4 [21] .got.plt PROGBITS 080495ac 0005ac 000014 04 WA 0 0 4 [22] .data PROGBITS 080495c0 0005c0 00000c 00 WA 0 0 4 [23] .bss NOBITS 080495cc 0005cc 000004 00 WA 0 0 4 [24] .comment PROGBITS 00000000 0005cc 0001b2 00 0 0 1 [25] .debug_aranges PROGBITS 00000000 000780 000058 00 0 0 8 [26] .debug_info PROGBITS 00000000 0007d8 000164 00 0 0 1 [27] .debug_abbrev PROGBITS 00000000 00093c 000020 00 0 0 1 [28] .debug_line PROGBITS 00000000 00095c 00015a 00 0 0 1 [29] .shstrtab STRTAB 00000000 000ab6 00010c 00 0 0 1 [30] .symtab SYMTAB 00000000 0010c4 000510 10 31 56 4 [31] .strtab STRTAB 00000000 0015d4 000322 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
$:cat Dyn_hello.c int main(void) { hi(); } $:cat hi.c #include <stdio.h> hi() { printf("Hello worldn"); }
$:gcc -fPIC -c hi.c $:gcc -shared -o libhi.so hi.o
$:gcc -c Dyn_hello.c $:gcc -o Dyn_hello Dyn_hello.o -L. -lhi
$:./Dyn_hello ./Dyn_hello: error while loading shared libraries: libhi.so: cannot open shared object file: No such file or directory
$:LD_LIBRARY_PATH=. ./Dyn_hello Hello world
$:cat Dl_hello.c #include <dlfcn.h> int main (int argc, char *argv[]) { void (*hi) (); void *m; if (argc > 2) exit (0); m = dlopen (argv[1], RTLD_LAZY); if (!m) exit (0); hi = dlsym (m, "hi"); if (hi) { (*hi) (); } dlclose (m); }
$:gcc -c Dl_hello.c $:gcc -o Dl_hello Dl_hello.o -ldl
$:./Dl_hello ./libhi.so Hello world
1. 概述
Executable and linking format(ELF)文件是x86 Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
(1)适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
(2)适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
(3)共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
ELF文件格式比较复杂,本文只是简要介绍它的结构,希望能给想了解ELF文件结构的读者以帮助。具体详尽的资料请参阅专门的ELF文档。
2. 文件格式
为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度,如图1所示。
ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),Program header table指出怎样创建进程映像,含有每个program header的入口,Section header table包含每一个section的入口,给出名字、大小等信息。
图1
3. 数据表示
ELF数据编码顺序与机器相关,数据类型有六种,见表1。
4. ELF文件头
象bmp、exe等文件一样,ELF的文件头包含整个文件的控制结构。它的定义如下:
#define EI_NIDENT 16 typedef struct{ unsigned char e_ident[EI_NIDENT]; 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_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; }Elf32_Ehdr; |
其中E_ident的16个字节标明是个ELF文件(7F+'E'+'L'+'F'+class +data+version+pad)。E_type表示文件类型,2表示可执行文件。E_machine说明机器类别,3表示386机器,8表示MIPS机器。E_entry给出进程开始的虚地址,即系统将控制转移的位置。E_phoff指出program header table的文件偏移,e_phentsize表示一个program header表中的入口的长度(字节数表示),e_phnum给出program header表中的入口数目。类似的,e_shoff,e_shentsize,e_shnum 分别表示section header表的文件偏移,表中每个入口的的字节数和入口数目。E_flags给出与处理器相关的标志,e_ehsize给出ELF文件头的长度(字节数表示)。E_shstrndx表示section名表的位置,指出在section header表中的索引。
下面有个elf文件头的例子,可以对照理解,见图2。
图2
5. section header
目标文件的section header table可以定位所有的section,它是一个Elf32_Shdr结构的数组,Section头表的索引是这个数组的下标。有些索引号是保留的,目标文件不能使用这些特殊的索引。
Section包含目标文件除了ELF文件头、程序头表、section头表的所有信息,而且目标文件section满足几个条件:
(1)目标文件中的每个section都只有一个section头项描述,可以存在不指示任何section的section头项。
(2)每个section在文件中占据一块连续的空间。
(3)Section之间不可重叠。
(4)目标文件可以有非活动空间,各种headers和sections没有覆盖目标文件的每一个字节,这些非活动空间是没有定义的。
Section header结构定义如下:
typedef struct{ 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; |
其中sh_name指出section的名字,它的值是后面将会讲到的section header string table中的索引,指出一个以null结尾的字符串。Sh_type是类别,sh_flags指示该section在进程执行时的特性。Sh_addr指出若此section在进程的内存映像中出现,则给出开始的虚地址。Sh_offset给出此section在文件中的偏移。其它字段的意义不太常用,在此不细述。
文件的section含有程序和控制信息,系统使用一些特定的section,并有其固定的类型和属性(由sh_type和sh_info指出)。下面介绍几个常用到的section:“.bss”段含有占据程序内存映像的未初始化数据,当程序开始运行时系统对这段数据初始为零,但这个section并不占文件空间。“.data.”和“data1”段包含占据内存映像的初始化数据。“.rodata”和“.rodata1”段含程序映像中的只读数据。“.shstrtab”段含有每个section的名字,由section入口结构中的sh_name索引。“.strtab”段含有表示符号表(symbol table)名字的字符串。“.symtab”段含有文件的符号表,在后文专门介绍。“.text”段包含程序的可执行指令。
6. symbol table
目标文件的符号表包含定位或重定位程序符号定义和引用时所需要的信息。符号表入口结构定义如下:
typedef struct{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; Unsigned char st_info; Unsigned char st_other; Elf32_Half st_shndx; }Elf32_Sym; |
其中st_name包含指向符号表字符串表(strtab)中的索引,从而可以获得符号名。St_value指出符号的值,可能是一个绝对值、地址等。St_size指出符号相关的内存大小,比如一个数据结构包含的字节数等。St_info规定了符号的类型和绑定属性,指出这个符号是一个数据名、函数名、section名还是源文件名;并且指出该符号的绑定属性是local、global还是weak。