# ELF 文件

## 1. 基本问题

Linux命令行运行一个可执行文件，这个过程发生了什么？可执行文件以什么格式进行组织？操作系统如何加载可执行文件，创建进程，调度运行？

首先需要了解，计算机的操作系统的启动引导程序写在特定存储空间，计算机上电后自动加载启动程序，开启主进程，之后的所有进程都是不断fork主进程得到的。对于可执行文件，也是由shell进程fork一个子进程，然后进行一系列系统调用装载亏歘看文件并执行。

不同系统的可执行文件有不同的格式，**ELF (Executable and Linkable Format)** 是Unix系统实验室作为应用程序二进制接&#x53E3;**（ABI）**&#x800C;发布的可执行文件格式.

ELF文件主要有四种不同类型：

* **可重定位文件（Relocatable File）**： 包含与其他文件链接来创建可执行文件或者共享目标文件的数据，`xxx.o` 文件
* **可执行文件（Executable File）**：包含用于执行的程序的文件，此文件规定了`exec（）` 如何创建一个程序的进程映像， `a.out` 文件
* **共享目标文件（Shared Object File）**: 包含可在两种上下文中链接的代码与数据：
  * 链接编译器可以将它与其他可重定位文件和共享目标文件一起处理，生成另外一个目标文件`*.so`
  * 动态链接器`Dynamic Linker` 可能将其与一个可执行文件、其他`so`文件一起创建程序进程映像
* **内核转储（Core Dumps）**： 存放当前进程的执行上下文，用于Dump信号触发。

## 2. 加载与动态链接

从编译/链接和运行的角度看，库分为动态链接和静态链接。相应的两种不同的ELF格式映像：

* 一种是静态链接的，在装入/启动其运行时无需装入函数库映像、也无需进行动态连接。
* 另一种是动态连接，需要在装入/启动其运行时同时装入函数库映像并进行动态链接。

Linux内核既支持静态链接的ELF映像，也支持动态链接的ELF映像，GNU规定：

* 把ELF映像的装入/启动入在Linux内核中；
* 把动态链接的实现放在用户空间（glibc），并为此提供一个称为”解释器”(ld-linux.so.2)的工具软件，而解释器的装入/启动也由内核负责。

## 3. ELF加载过程

### 3.1 `execve()`

在shell中执行命令后，shell获取桥入的指令，并执行`execve()` 函数，该函数接受可执行文件名以及程序所需参数，同时传入环境变量。**负责对于进程栈进行初始化，压栈环境变量值，压栈参数，压栈可执行文件名。**&#x521D;始化完成后，调用`sys_execuve()`陷入内核, 调用链路如下：

```
sys_execve / sys_execveat
└→ do_execve
      └→ do_execveat_common
       └→ __do_execve_file
            └→ exec_binprm
            └→ search_binary_handler
                         └→ load_elf_binary
```

### 3.2 `sys_execve()`

该函数进行一些参数的检查与复制，而后调用 `do_execve()`

### 3.3 `do_execve`

内核中实际执行`execv()`或`execve()`系统调用的程序是`do_execve()`，这个函数先打开目标映像文件，并从目标文件的头部（第一个字节开始）读入若干（├─systemd─┬─当前Linux内核中是128）字节（实际上就是填充ELF文件头）， 读取文件头后可以判断文件格式，进而调用`serach_binary_handler()`

### 3.4 `search_bianry_handler()`

该函数将去搜索和匹配合适的可执行文件装载处理程序。Linux 中所有被支持的可执行文件格式都有相应的装在处理程序。以Linux 中的ELF 文件为例，接下来将会调用elf 文件的处理程序：`load_elf_binary()`

### 3.5 `load_elf_binary()`

该函数执行以下三个步骤:

a）创建虚拟地址空间：实际上指的是建立从虚拟地址空间到物理内存的映射函数所需要的相应的数据结构。（即创建一个空的页表）

b）读取可执行文件的文件头，建立可执行文件到虚拟地址空间之间的映射关系

c）将CPU指令寄存器设置为可执行文件入口（虚拟空间中的一个地址）

`load_elf_binary()`函数执行完毕，事实上装载函数执行完毕后，可执行文件真正的指令和数据都没有被装入内存中，只是建立了可执行文件与虚拟内存之间的映射关系，以及分配了一个空的页表，用来存储虚拟内存与物理内存之间的映射关系。

### 3.6 程序返回到`execve()`中

此时从内核态返回到用户态，且寄存器的地址被设置为了ELF 的入口地址，于是新的程序开始启动，发现程序入口对应的页面并没有加载（因为初始时是空页面），则此时引发一个缺页错误，操作系统根据可执行文件和虚拟内存之间的映射关系，在磁盘上找到缺的页，并申请物理内存，将其加载到物理内存中，并在页表中填入该虚拟内存页与物理内存页之间的映射关系。之后程序正常运行，直至结束后回到shell 父进程中，结束回到 shell。
