计算机那些事(6)——可执行文件的装载与运行

当我们在 Linux 的 bash 中输入命令执行某个 ELF 可执行文件时,如下所示。

1
$ ./hello.out

那么,Linux 系统是如何装载该 ELF 文件并执行的呢?这个过程可以分为以下这些步骤:

  • 创建新进程
  • 检查可执行文件类型
  • 搜索匹配装载处理过程
  • 装载执行可执行文件

创建新进程

首先在用户层面,bash 进程会调用 fork() 系统调用创建一个新的进程。其次,新的进程通过调用 execve() 系统调用来执行指定的 ELF 文件。原先的 bash 进程继续返回并等待刚才启动的新进程结束,之后继续等待用户输入命令。

execve() 系统调用被定义在 unistd.h,其原型如下所示。其中的三个参数分别对应被执行程序的 程序文件名执行参数环境变量

1
int execve(const char *filename, char *const argv[], char *const envp[]);

检查可执行文件类型

当进入 execve() 系统调用之后,Linux 内核就开始进行真正的装载工作。在内核中,execve() 系统调用相应的入口是 sys_execve()sys_execve() 进行一些参数的检查复制之后,调用 do_execve()do_execve() 会首先查找被执行的文件,如果找到文件,则读取文件的前 128 个字节。

为什么要先读取文件的前 128 个字节?这是因为Linux支持的可执行文件不止 ELF 一种,还包括 a.outJava 程序#! 开头的脚本程序do_execve()通过读取前 128 个字节来判断文件的格式。每种可执行文件格式的开头几个字节都是很特殊的,尤其是前4个字节,被称为 魔数(Magic Number)。比如:ELF的可执行文件格式的头 4 个字节为 0x7Felf;Java的可执行文件格式的头 4 个字节为 cafe;如果是解释型语言的脚本,则第一行通常是 #!/bin/sh#!/user/bin/python,其中 #! 构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序的路径。

搜索匹配装载处理过程

do_execve() 读取了128个字节的文件头部之后,调用 search_binary_handle() 去搜索和匹配合适的可执行文件装载处理过程。Linux 中所有被支持的可执行文件格式都有相应的装载处理过程search_binary_handler() 会通过判断头部的魔术确定文件的格式,并且调用相应的装载处理过程。常见的可执行程序及其装载处理过程的对应关系如下所示.

  • ELF 可执行文件:load_elf_binary()
  • a.out 可执行文件:load_aout_binary()
  • 可执行脚本程序:load_script()

装载执行可执行文件

以 ELF 的装载处理过程 load_elf_binary() 为例,其所包含的步骤如下图所示:

  1. 操作系统读取可执行文件 ELF 的 Header,检查文件的有效性。
  2. 操作系统读取可执行文件 ELF的 Program Header Table 中读取每个 Segment 的虚拟地址、文件地址、属性等。
  3. 操作系统根据 Program Header Table 将可执行文件 ELF 映射至内存。
  4. 如果是静态链接的情况,则直接跳转至第 7 步;如果是动态链接的情况,操作系统将查找 .interp 节,找到 动态链接器(Dynamic Linker) 的位置,并启动动态链接器。在 Linux 下,动态链接器 ld.so 是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间。操作系统在加载完后,将控制权交给动态链接器的入口。
  5. 动态链接器获得控制权后,开始执行一系列初始化操作。
  6. 动态链接器根据当前的环境参数,对可执行文件进行动态链接工作。
  7. 控制权被转交到可执行文件的入口地址,程序开始正式执行。

参考

  1. 《程序员的自我修养——链接、装载与库》
  2. 《深入理解计算机系统》

(完)