这里,我们从OS的角度来阐述一个可执行文件如何被装载,并且同时在进程中执行。一个可执行文件从装载到执行,最开始只需要做三件事情:
- 创建一个独立的虚拟地址空间
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
- 将CPU指令寄存器设置成可执行文件的入口地址,启动运行
Linux下,创建虚拟地址空间只是分配一个页目录就可以了,甚至不设置页映射关系。当程序发生页错误时,OS将从物理内存中分配一个物理页,然后将“缺页”读取到内存中,在设置缺页的虚拟页和物理页的映射关系。显然,当OS捕获到缺页错误时,它应知道程序目前所需要的页在可执行文件中的哪个位置。这就是虚拟空间与可执行文件之间的映射关系,也是传统意义上的“装载”过程。这种映射关系只是保存在操作系统内部的一个数据结构。Linux将进程虚拟空间中的一个段叫做虚拟内存区域。将CPU指令寄存器设置为可执行文件入口,从进程角度来看,这一步可以简单地认为OS执行了一条跳转指令。
上述步骤执行完以后,其实可执行文件的真正指令和数据都没有被转载到内存中,操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟之间的数据结构而已。当CPU开始执行这个地址的指令时,发现该页面是个空页面,于是它就认为这是一个页错误。CPU将控制权交给OS,OS根据映射关系找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权交给进程继续执行。
OS在装载时,只关心一些跟装载相关的问题,最主要的是段的权限。在ELF文件中,段的权限往往只有为数不多的几种组合:
- 以代码段为代表的权限为可读可执行的段
- 以数据段和BSS段为代表的权限为可读可写的段
- 以只读数据段为代表的权限为只读的段
//SectionMapping
#include <stdlib.h>
int main()
{
while(1)
sleep(1000);
return 0;
}
使用静态链接的方式将其编译成可执行文件,得到一个elf文件
$ gcc -static SectionMapping.c -o SectionMapping.elf
使用readelf查看该elf文件的section:
$ readelf -S SectionMapping.elf
也可以使用readelf命令查看ELF的“Segment”,描述“segment”的结构叫程序头(
Program Header),它描述了ELF文件该如何被OS映射到进程的虚拟空间:
$ readelf -l SectionMapping.elf
从装载的角度,我们只关心“LOAD”类型的Segment。Segment[00]是可读可执行的,统一被映射到VMA0;Segment[01]是可读写的,统一被映射到VMA1。
操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为如下几种VMA区域:
- 代码VMA,权限可读、可执行;有映像文件
- 数据VMA,权限可读写、不可执行;有映像文件
- 堆VMA,权限可读写、不可执行;无映像文件,匿名,可向上扩展
- 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展
$ ./SectionMapping.elf &
[1] 3061
$ cat /proc/3061/maps
下面简介下Linux内核装载ELF过程:
首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚启动的新进程结束,然后继续等待用户输入命令。
在进入execve()系统调用之后,Linux内核就开始进行真正的装在工作。在内核中,execve()系统调用相应的入口时sys_execve(),内部调用do_execve()。do_execve()会首先查找被执行的文件,如果找到文件,则读取文件的前128字节,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装在处理过程。Linux中所有被支持的可执行文件格式都有相应的装在处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并调用相应的装载处理过程;这里我们只关心ELF可执行文件的装载,load_elf_binary(),它的主要套路是:
- 检查ELF可执行文件的有效性;
- 寻找动态链接的“.interp”段,设置动态链接路径
- 根据ELF的程序头表的描述,对ELF文件进行映射
- 初始化ELF进程环境
- 将系统调用的返回地址修改为ELF文件的入口点;入口点取决于程序的链接方式,静态链接ELF文件的头文件中e_entry所指的地址,动态链接ELF文件则入口点为动态链接器