计算机那些事(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.out、Java
程序、以 #!
开头的脚本程序。do_execve()
通过读取前 128
个字节来判断文件的格式。每种可执行文件格式的开头几个字节都是很特殊的,尤其是前4个字节,被称为
魔数(Magic Number)。比如:ELF的可执行文件格式的头 4
个字节为
0x7F
、e
、l
、f
;Java的可执行文件格式的头
4 个字节为
c
、a
、f
、e
;如果是解释型语言的脚本,则第一行通常是
#!/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()
为例,其所包含的步骤如下图所示:
- 操作系统读取可执行文件 ELF 的
Header
,检查文件的有效性。 - 操作系统读取可执行文件 ELF的
Program Header Table
中读取每个Segment
的虚拟地址、文件地址、属性等。 - 操作系统根据
Program Header Table
将可执行文件 ELF 映射至内存。 - 如果是静态链接的情况,则直接跳转至第 7
步;如果是动态链接的情况,操作系统将查找
.interp
节,找到 动态链接器(Dynamic Linker) 的位置,并启动动态链接器。在 Linux 下,动态链接器ld.so
是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间。操作系统在加载完后,将控制权交给动态链接器的入口。 - 动态链接器获得控制权后,开始执行一系列初始化操作。
- 动态链接器根据当前的环境参数,对可执行文件进行动态链接工作。
- 控制权被转交到可执行文件的入口地址,程序开始正式执行。
参考
- 《程序员的自我修养——链接、装载与库》
- 《深入理解计算机系统》
(完)