elf文件格式

可执行文件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
用gcc把hello.c编译成目标文件hello.o
代码:
      
      
$:strip 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
再用gcc连接时,连接器ld报错。说明在目标文件中的符号起着很重要的作用,如果要发布二进制的程序的话,在debug后为了减小可执行文件的大小,可以用strip来除去符号信息但是在程序的调试阶段还是不要用strip为好。

在接下去讨论以前,我们还要来讲讲relocations的概念:首先有个简单的程序hello.c
代码:
      
      
$:cat hello.c main( ) { printf("Hello Worldn"); }
当我们把hello.c编译为目标文件时,我们并没有在源文件中定义printf这个函数,所以汇编器也不知道printf这个函数的具体的地址,所以在目标文件中就会留下printf这个符号。以下的工作就交给连接器了,连接器会找到这个函数的入口地址然后传递给这个文件最终形成可执行文件。这个过程就叫做relocations。a.out格式的可执行文件是没有这种relocation的功能的,内核不会执行其中还有未知函数的入口地址的可执行文件的。在目标文件中当然可以relocation,只不过连接器需要把未知函数的入口地址完全找到,生成可执行文件才行。这样就有一个很尴尬的问题,在 a.out格式中极其难以实现动态连接技术。要知道为什么现在的Unix几乎都是用的elf格式的可执行文件就要了解a.out格式的短处。

a.out的符号是极其有限的,在/usr/include/linux/asm/a.out.h中定义了一个结构exec就是:
代码:
      
      
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 */ };
在这个结构中更本没有指示每个段在文件中的开始位置,内核加载器具有一些非正式的方法来加载可执行文件的。明显的,a.out 是不支持动态连接的。(在内部不支持动态连接,用某些技术也是可以实现a.out的动态连接)

要了解elf可执行文件的运行方式,我们有必要讨论一下动态连接技术。很多人对动态连接技术十分熟悉,但是很少有人真正了解动态连接的内部工作方式。回想没有动态连接的日子,程序员写程序时不用什么都从头开始,他们可以调用定义的很好的函数,然后再用连接器与函数库连接。这样的话使得程序员更加有效率,但是一个十分重要的问题出现了:这样产生的可执行文件就会很大。因为连接器把程序需要用的所有函数的代码都复制到了可执行文件中去了。这种连接方式就是所谓的静态连接,与之相对的就是动态连接。连接器在可执行文件中标记出程序调用外部函数的位置,并不把代码复制进去,只是标出函数在动态连接库中的位置。用这样的方式生成的特殊可执行文件就是动态连接的。在运行这种动态程序时,系统在运行时把该程序调用的外部函数地址映射到程序地址,这就是所谓的动态连接,系统就有一个程序叫做动态连接器,在动态连接的程序执行前都要先把地址映射好。很显然的,必须有一种机制保证动态连接的程序中的函数地址正确地指向了动态连接库的某个函数地址。这就需要讨论一下elf可执行文件格式处理动态连接的机制了。

elf的动态连接库是内存位置无关的,就是说你可以把这个库加载到内存的任何位置都没有影响。这就叫做position independent。而a.out的动态连接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。在编译内存位置无关的动态连接库时,要给编译器加上 -fpic选项,让编译器产生的目标文件是内存位置无关的还会尽量减少对变量引用时使用绝对地址。把库编译成内存位置无关会带来一些花费,编译器会保留一个寄存器来指向全局偏移量表(global offset table (or GOT for short)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。

Elf的另一个特点是它的动态连接库是在运行时处理符号的,这是通过用符号表和再布置(relocation)表来实现的。在载入文件时并不能立即执行,要在处理完符号表把所有的地址都relocation完后才可以执行。这个听起来有点复杂而且可能导致文件运行慢,不过对elf做了很大的优化后,这种减慢已经是微不足道的了。理论上说不是用-fpic选项编译出来的目标文件也可以用作动态连接库,但是在运行时会需要做数目极大的 relocation,这是对运行速度有极大影响的。这样的程序性能是很差的,几乎没有可用性。

当从动态连接库中读一个全局变量时与从非-fpic编译的目标文件读是不同的。读动态连接的库中的变量是通过GOT来寻找到目标变量的,GOT已经由某一个寄存器指向了。GOT本生就是一个指针列表,找到GOT中的某一个指针就可以读到所要的全局变量了,有了GOT我们要读出一个变量只要做一次 relocation。

下面我们来看看elf文件中到底有些什么信息:
代码:
      
      
$:cat hello.c main() { printf("Hello Worldn"); } $:gcc-elf -c hello.c
还是这个简单的程序,用gcc把它编译成目标文件hello.o。然后用readelf工具来探测一下elf文件的内容。(readelf是在 binutils软件包里的一个工具,大多数Linux发行版都包含它)
代码:
      
      
$: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
-h选项是列出elf文件的头信息。Magic:字段是一个标识符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,这是一个32位的elf。Machine:字段是指出目标文件的平台信息,这里是 I386兼容平台。其他的字段可以从其字面上看出它的意义,这里就不一一解释了。

下面用-S选项列出段的头信息:
代码:
      
      
$: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)
Name字段显示的是各个段的名字,Type显示段的属性,Addr是每个段载入虚拟内存的位置,Off是每个段在目标文件中的偏移位置,Size是每个段的大小,后面的一些字段是表示段的可写,可读,或者可执行。

用-r可以列出elf文件中的relocation:
代码:
      
      
$: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
在.text段中有两个relocation,其中之一就是printf函数的relcation。Offset指出当relocation时要把 printf函数的入口地址贴到离.text段开头00000024处。

下面我们可以看一下连接过后的可执行文件中的内容:
代码:
      
      
$: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)
这里的段比目标文件hello.o的段要多的多,这是因为这个程序需要elf的一个动态连接库libc.so.1。在这里需要简单的介绍一下内核加载 elf可执行文件。内核先是把整个文件加载到用户的虚拟内存空间,如果程序是与动态连接库连接的,则程序中就会包含动态连接器的名称,可能是 /lib/elf/ld-linux.so.1。(动态连接器本身也是一个动态连接库)

在文件的尾部的一些段的Addr值是00000000,因为这些都是符号表,动态连接器并不把这些段的内容加载到内存中。. interp段中只是储存这一个ASCII的字符串,它就是动态连接器的名字(路径)。.hash, .dynsym, .dynstr这三个段是用于动态连接器执行relocation时的符号表。.hash是一个哈希表,可以让我们很快的从.dynsym中找到所需的符号。

.plt段中储存着我们调用动态连接库中的函数入口地址,在默认状态下,程序初始化时,.plt中的指针并不是指向正确的函数入口地址的而是指向动态连接器本身,当你在程序中调用某个动态连接库中的函数时,连接器会找到那个函数在动态连接库中的位置,再把这个位置连接到.plt段中。这样做的好处是如果在程序中调用了很多动态连接库中的函数,会花费掉连接器很长时间把每个函数的地址连接到.plt段中。所以就可以采用连接器只是把要用的函数地址连接进去,以后要用的再连接。但是也可以设置环境变量LD_BIND_NOW=1让连接器在程序执行前把所有的函数地址都连接好,这主要是方便调试程序。

readelf工具还有很多选项,具体内容可以查看man手册。在文章的开头就说elf文件格式很方便运用动态连接技术,下面我就写一个就简单的动态连接库的例子:
代码:
      
      
$:cat Dyn_hello.c int main(void) { hi(); } $:cat hi.c #include <stdio.h> hi() { printf("Hello worldn"); }
两个简单的文件,在mian函数中调用hi()函数,下面并不是把两个文件一起编译,而是把hi.c编译成动态连接库。(注意Dyn_hello.c中并没有包含任何头文件。)
代码:
      
      
$:gcc -fPIC -c hi.c $:gcc -shared -o libhi.so hi.o
现在在当前目录下有一个名字为libhi.so的文件,这就就是仅含有一个函数的动态连接库。
代码:
      
      
$:gcc -c Dyn_hello.c $:gcc -o Dyn_hello Dyn_hello.o -L. -lhi
在当前目录下有了一个Dyn_hello可执行文件,现在就可以执行它了。
代码:
      
      
$:./Dyn_hello ./Dyn_hello: error while loading shared libraries: libhi.so: cannot open shared object file: No such file or directory
执行不成功,这就表明了这是一个动态连接的程序,连接器找不到libhi.so这个动态连接库。在命令行加上 LD_LIBRARY_PATH=...就行了。像这样运行:
代码:
      
      
$:LD_LIBRARY_PATH=. ./Dyn_hello Hello world
指出当前目录是连接器的搜索目录,就可以了。

Elf可执行文件还有一个a.out很难实现的特点,就是对dlopen()函数的支持,这个函数可以在程序中控制动态的加载动态连接库,看下面的一个小程序:
代码:
      
      
$: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程序加上动态连接库。
代码:
      
      
$:./Dl_hello ./libhi.so Hello world
命令行成功的打印出了Hello world说明我们的动态连接库运用成功了。

在这篇文章中只是讨论了elf可执行文件的执行原理,还有很多方面没有涉及到,要深入了解elf你也许需要对动态连接器hack一下,也要hack一下内核加载程序的loader。但是我想对大多数人来说,这篇文章对elf的介绍已经足够让你可以自己对elf在进行比较深入的研究了。
 

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。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值