ELF学习

# ELF学习

[TOC]

### 背景
应用软件的编程不可能是所有的代码都自己写的,程序员不可避免地都会使用一些现成的程序库。
从编译/连接和运行的角度看,**应用程序和库程序的连接有两种方法**。
1. 一种是**固定的、静态**的连接,
**库函数的目标(二进制)代码从程序库中抽取出来,连接进应用软件的目标映像中**
这里所谓的**连接包括两方面**的操作,**一是把库函数的目标代码“定位”在应用软件目标映像中的某个位置上**。由于不同应用软件本身的大小和结构都可能不同,**库函数在目标映像中的位置是无法预先确定的**。为此,**程序库中的代码必须是可以浮动的,即“与位置无关”的**,**在编译时必须加上-fPIC选项**,这里PIC是“Position-Independent Code”的缩写。**一旦一个库函数在映像中的位置确定以后,就要使应用软件中所有对此函数的调用都指向这个函数。**早期的软件都采用这种静态的连接方法,**好处**是连接的过程只发生在编译/连接阶段,而且用到的技术也比较简单。但是也有**缺点**,那就是具体库函数的代码往往重复出现在许多应用软件的目标映像中,从而造成运行时的资源浪费。另一方面,这也不利于软件的发展,因为即使某个程序库有了更新更好的版本,已经与老版本静态连接的应用软件也享受不到好处,而重新连接往往又不现实。再说,这也不利于将程序库作为商品独立发展的前景。
2. 第二种连接方法,动态连接。
所谓动态连接,是指**库函数的代码并不进入应用软件的目标映像**,应用软件在编译/连接阶段并不完成跟库函数的连接;而是把函数库的映像也交给用户,**到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位)、再完成应用软件与库函数的连接。**说到程序库,最基本、最重要的当然是C语言库、即libc或glibc。


**装入/启动ELF映像必需由内核完成,而动态连接的实现则既可以在内核中完成,也可在用户空间完成。**

因此,GNU把对于动态连接ELF映像的支持作了分工:把ELF映像的装入/启动放在Linux内核中;而把动态连接的实现放在用户空间(glibc),并为此提供一个称为“解释器”(ld-linux.so.2)的工具软件,而解释器的装入/启动也由内核负责。


在Linux系统中,目标映像的装入/启动是由系统调用execve()完成的,但是可以在Linux内核上运行的二进制映像有a.out和ELF两种

###  ELF文件格式

链接
http://jzhihui.iteye.com/blog/1447570

**概述**

Executable and Linking Format(ELF)文件是x86 Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:

-  适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。

-  适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。

- 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。


**   文件格式**

为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度,如图所示。
![](c2aba87f-1d21-4896-85ee-08fb8238bcbb_files/6cc27e57-5159-31ee-bc69-8392eeb4731a.png)

ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),Program header table指出怎样创建进程映像,含有每个program header的入口,section header table包含每一个section的入口,给出名字、大小等信息。

**    数据表示**

       ELF数据编码顺序与机器相关,数据类型有六种,见下表:
![](c2aba87f-1d21-4896-85ee-08fb8238bcbb_files/609d893b-f125-31bf-93a7-4a57c06e36de.png)


**ELF****文件头**

象bmp、exe等文件一样,ELF的文件头包含整个文件的控制结构。
```cpp
#define EI_NIDENT       16  
typedef struct elf32_hdr
{  
unsigned char e_ident[EI_NIDENT];   
Elf32_Half    e_type;     /* file type */  
Elf32_Half    e_machine;  /* architecture */  
Elf32_Word e_version;  
Elf32_Addr    e_entry;    /* entry point */  
Elf32_Off e_phoff;        /* PH table offset */  
Elf32_Off e_shoff;        /* SH table offset */  
Elf32_Word    e_flags;  
Elf32_Half    e_ehsize;       /* ELF header size in bytes */  
Elf32_Half    e_phentsize;    /* PH size */  
Elf32_Half    e_phnum;        /* PH number */  
Elf32_Half    e_shentsize;    /* SH size */  
Elf32_Half    e_shnum;        /* SH number */  
Elf32_Half    e_shstrndx; /* SH name string table index */  
} Elf32_Ehdr;
```
 其中E_ident的16个字节标明是个ELF文件(7F+'E'+'L'+'F')。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表中的索引。

**Section Header**
目标文件的section header table可以定位所有的section,它是一个Elf32_Shdr结构的数组,Section头表的索引是这个数组的下标。有些索引号是保留的,目标文件不能使用这些特殊的索引。

Section包含目标文件除了ELF文件头、程序头表、section头表的所有信息,而且目标文件section满足几个条件:

 目标文件中的每个section都只有一个section头项描述,可以存在不指示任何section的section头项。

 每个section在文件中占据一块连续的空间。

 Section之间不可重叠。

 目标文件可以有非活动空间,各种headers和sections没有覆盖目标文件的每一个字节,这些非活动空间是没有定义的。

Section header结构定义如下:

```cpp
288 typedef struct {  
289   Elf32_Word    sh_name;    /* name of section, index */  
290   Elf32_Word    sh_type;      
291   Elf32_Word    sh_flags;  
292   Elf32_Addr     sh_addr;       /* memory address, if any */  
293   Elf32_Off      sh_offset;  
294   Elf32_Word    sh_size;        /* section size in file */  
295   Elf32_Word    sh_link;  
296   Elf32_Word    sh_info;  
297   Elf32_Word    sh_addralign;  
298   Elf32_Word    sh_entsize;     /* fixed entry size, if have */  
299 } 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”段包含程序的可执行指令。

当然一个实际的ELF文件中,会包含很多的section,如.got,.plt等等,我们这里就不一一细述了,需要时再详细的说明。

**Program Header**

目标文件或者共享文件的program header table描述了系统执行一个程序所需要的段或者其它信息。目标文件的一个段(segment)包含一个或者多个section。Program header只对可执行文件和共享目标文件有意义,对于程序的链接没有任何意义。


```c
232 typedef struct elf32_phdr{  
233   Elf32_Word    p_type;   
234   Elf32_Off      p_offset;  
235   Elf32_Addr    p_vaddr;        /* virtual address */  
236   Elf32_Addr    p_paddr;        /* ignore */  
237   Elf32_Word    p_filesz;       /* segment size in file */  
238   Elf32_Word    p_memsz;        /* size in memory */  
239   Elf32_Word    p_flags;  
240   Elf32_Word    p_align;       
241 } Elf32_Phdr;  
```

其中p_type描述段的类型;p_offset给出该段相对于文件开关的偏移量;p_vaddr给出该段所在的虚拟地址;p_paddr给出该段的物理地址,在Linux x86内核中,这项并没有被使用;p_filesz给出该段的大小,在字节为单元,可能为0;p_memsz给出该段在内存中所占的大小,可能为0;p_filesze与p_memsz的值可能会不相等。


**Symbol Table**

目标文件的符号表包含定位或重定位程序符号定义和引用时所需要的信息。

**Section****和Segment****的区别和联系**

可执行文件中,一个program header描述的内容称为一个段(segment)。Segment包含一个或者多个section
![](c2aba87f-1d21-4896-85ee-08fb8238bcbb_files/e4c15bdc-2db0-3e42-8176-081f454e25fc.png)

### ELF文件的加载过程

#### Linux可执行文件类型的注册机制

**为什么Linux可以运行ELF文件?**

**内核对所支持的每种可执行的程序类型都有个struct linux_binfmt的数据结构**

ELF文件格式的定义
```cpp
74 static struct linux_binfmt elf_format = {  
75                 .module      = THIS_MODULE,  
76                 .load_binary = load_elf_binary,  
77                 .load_shlib      = load_elf_library,  
78                 .core_dump       = elf_core_dump,  
79                 .min_coredump    = ELF_EXEC_PAGESIZE,  
80                 .hasvdso     = 1  
81 };  
```
 要支持ELF文件的运行,则必须向内核登记这个数据结构,加入到内核支持的可执行程序的队列中。内核提供两个函数来完成这个功能,一个注册,一个注销。

```c
int register_binfmt(struct linux_binfmt * fmt)  
int unregister_binfmt(struct linux_binfmt * fmt)  
```
 当需要运行一个程序时,则扫描这个队列,让各个数据结构所提供的处理程序,ELF中即为load_elf_binary,逐一前来认领,如果某个格式的处理程序发现相符后,便执行该格式映像的装入和启动。

#### 内核空间的加载过程
内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干(当前Linux内核中是128)字节(实际上就是填充ELF文件头,下面的分析可以看到),然后调用另一个函数search_binary_handler(),在此函数里面,它会搜索我们上面提到的Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。在ELF文件格式中,处理函数是load_elf_binary函数。


LF格式的二进制映像的认领、装入和启动是由load_elf_binary()完成的。而“共享库”、即动态连接库映像的装入则由load_elf_library()完成。实际上共享库的映像也是二进制的,但是一般说“二进制”映像是指带有main()函数的、可以独立运行并构成一个进程主体的可执行程序的二进制映像。另一方面,尽管装入/启动二进制映像的过程中蕴含了共享库的装入(否则无法运行),但是在此过程中却并没有调用load_elf_library(),而是通过别的函数进行,这个函数只是在sys_uselib()、即系统调用uselib()中通过函数指针load_shlib受到调用。所以,load_elf_library()所处理的是应用软件在运行时对于共享库的动态装入,而不是启动进程时的静态装入。



### ELF文件中符号的动态解析过程
上面一节提到,控制权是先交到解释器,由解释器加载动态库,然后控制权才会到用户程序。因为时间原因,动态库的具体加载过程,并没有进行深入分析。大致的过程就是将每一个依赖的动态库都加载到内存,并形成一个链表,后面的符号解析过程主要就是在这个链表中搜索符号的定义。

### GOT和PLT
**Global Offset Table(GO)**

在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是**在运行阶段,符号的地址才会最终确定**。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,**GOT表中每项保存程序中引用其它符号的绝对地址**。这样,程序就可以通过引用GOT表来获得某个符号的地址。

在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址(前面提到加载的共享库会形成一个链表);GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,后面我们会详细分析。

**Procedure Linkage Table(PLT)**

过程链接表(PLT)的**作用就是将位置无关的函数调用转移到绝对地址**。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(如前所说,这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。

```cpp
Disassembly of section .plt:  
  
0804828c <__gmon_start__@plt-0x10>:  
 804828c:       ff 35 68 95 04 08       pushl   0x8049568  
 8048292:       ff 25 6c 95 04 08       jmp     *0x804956c  
 8048298:       00 00  
        ......  
0804829c <__gmon_start__@plt>:  
 804829c:       ff 25 70 95 04 08       jmp     *0x8049570  
 80482a2:       68 00 00 00 00          push        $0x0  
 80482a7:       e9 e0 ff ff ff          jmp     804828c <_init+0x18>  
  
080482ac <__libc_start_main@plt>:  
 80482ac:       ff 25 74 95 04 08       jmp     *0x8049574  
 80482b2:       68 08 00 00 00          push        $0x8  
 80482b7:       e9 d0 ff ff ff          jmp     804828c <_init+0x18>  
080482bc <puts@plt>:  
 80482bc:       ff 25 78 95 04 08       jmp     *0x8049578  
 80482c2:       68 10 00 00 00          push    $0x10  
 80482c7:       e9 c0 ff ff ff          jmp     804828c <_init+0x18>  
```

程序中所有对有puts函数的调用都要先来到这里(Hello World里只有一次)。可以看出,除PLT0以外(就是__gmon_start__@plt-0x10所标记的内容),其它的所有PLT项的形式都是一样的,而且最后的jmp指令都是0x804828c,即PLT0为目标的。所不同的只是第一条jmp指令的目标和push指令中的数据。PLT0则与之不同,但是包括PLT0在内的每个表项都占16个字节,所以整个PLT就像个数组(实际是代码段)。另外,每个PLT表项中的第一条jmp指令是间接寻址的。比如我们的puts函数是以地址0x8049578处的内容为目标地址进行中跳转的。


在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。



### 小结
用户通过shell执行程序,shell通过exceve进入系统调用。(User-Mode)

sys_execve经过一系列过程,并最终通过ELF文件的处理函数load_elf_binary将用户程序和ELF解释器加载进内存,并将控制权交给解释器。(Kernel-Mode)

ELF解释器进行相关库的加载,并最终把控制权交给用户程序。由解释器处理用户程序运行过程中符号的动态解析。(User-Mode)
原文地址:https://www.cnblogs.com/volva/p/11814950.html