6.1 进程虚拟地址空间
程序与进程的区别
- 程序(或者狭义上讲可执行文件)是个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件
- 进程则是一个动态的概念,它是程序运行时的一个过程。
- 两者的关系可以这么比喻:把程序和进程的概念跟做菜相比较,那么程序就是菜谱,计算机的CPU就是人,相关的厨具则是计算机的其他硬件,整个炒菜的过程就是一个进程。
每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),且虚拟地址空间的大小由计算机的硬件平台决定,具体说是由CPU的位数决定的。
从程序的角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。
6.2 装载的方式
覆盖载入(Overlay)和页映射(Paging)(目前的主流方式)
6.3 从操作系统角度看可执行文件的装载
6.3.1 进程的建立
从操作系统的角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。很多时候一个程序被执行同时都伴随着一个新的进程的创建。此过程的最通常的情形如下:
- 创建一个独立的虚拟地址空间。
- 此步骤确定的是虚拟地址空间
=>
物理内存的映射关系- 实际上是创建映射函数所需要的相应的数据结构。
- 在Linux下实际上是分配一个页目录(Page Directory)即可。
- 读取可执行文件头,并且建立虚拟地址空间与可执行文件的映射关系(最为重要)。
- 此步骤确定的是虚拟地址空间
=>
可执行文件(DP,Disk Paging)的映射关系- 此映射关系被保存在进程虚拟地址空间的一个段中,linux下叫做虚拟内存区域(VMA,Virtual Memory Area)。
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
- 涉及到内核堆栈和用户堆栈的切换、CPU运行权限的切换。
6.3.2 页错误
通过进程建立的三个步骤,其实可执行文件的真正指令和数据都没有被装入到内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟地址空间之间的映射关系而已。当CPU开始打算执行某个地址的指令时,发现其所在的页面是空页面,就认为是一个页错误(Page Fault)。CPU将控制权交还给操作系统,操作系统有专门的页错误处理例程来处理这种情况。操作系统根据第二个步骤中建立的映射关系的数据结构,找到空白页所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页(Physical Page),将进程中该虚拟页与分配的物理页之间建立映射关系,然后把CPU控制权再还给进程,进程从刚才错误的位置重新开始执行。随着进程执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页直至满足进程执行的需求。
6.4 进程虚拟地址空间分布
6.4.1 ELF文件链接视图和执行视图
当我们站在操作系统装载可执行文件的角度看问题时,可以发现它实际上值关心一些跟装载相关的问题,主要是段的权限(可读、可写、可执行)。ELF文件中,段的权限组合也就那么几种:
- 以代码段为代表的权限是可读可执行(RX)的段。
- 以数据段和BSS段为代表的权限是可读可写(RW)的段。
- 以只读数据段为代表的权限是只可读(R)的段。
简单的方案是:对于相同权限的段,把它们合并到一起当作一个段(Segment)进行映射。总体上来说,“Segment”和“Section”是从不同的角度来划分同一个ELF文件。这个在ELF中被称为不同的视图(View),从“Section”的角度来看ELF文件就是链接视图(Linking View),从“Segment”的角度来看就是执行视图(Execution View)。当我们谈及ELF装载时,“段”专门指“Segment”;而其他的情况下,“段”指的是“Section”。
ELF可执行文件和共享库文件中都有专门的数据结构来保存“Segment”的信息,叫做程序头表(Program Header Table)。
6.4.2 堆和栈
在操作系统里面,VMA除了被用来映射可执行文件中的各个“Segment”以外,它还可以被用来管理进程的地址空间。进程执行的过程中所需的栈(Stack)、堆(Heap),在进程的虚拟空间中的表现也是以VMA的形式存在的。很多情况下,一个进程中的栈和堆分别有一个对应的VMA。在Linux下,可以通过查看“/proc”来查看进程的虚拟空间分布。
操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟(地址)空间;基本的原则是将相同权限属性的、有相同映像文件的映射成一个VMA。一个进程基本上可以分割成如下几种VMA区域:
- 代码VMA,权限只读、可执行;有映像文件。
- 数据VMA,权限可读写,可执行;有映像文件。
- 堆VMA,权限可读写,可执行;无映像文件,匿名,可向上扩展。
- 栈VMA,权限可读写,不可执行;无映像文件,匿名,可向下扩展。
6.4.3 堆的最大申请数量
32位CPU时,Linux下虚拟地址空间分给进程本身的是3GB(Windows默认是2GB),实际上,Linux分配给程序的最大为2.9G左右(Windows则为1.5G)
6.4.4 段地址对齐
6.4.5 进程栈初始化
常见的做法是操作系统在进程启动前将系统环境变量和进程的运行参数等信息提前保存到进程的虚拟空间的栈中(栈VMA)。
6.5 Linux内核装载ELF过程简介
- 首先,在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。
- 在进入execve系统调用之后,Linux内核就开始进行真正的装载工作。execve()系统调用在内核中相应的入口是
sys_execve()
,它会进行一些参数的检查复制之后,调用do_execve()
。do_execve()
会首先查找被执行的文件,如果找到文件,则读取文件的前128个字节用于判断文件的格式。每种可执行文件的格式的开头几个字节都很特殊,特别是开头4个字节,常常被称作魔数(Magic Number)。判断完格式后,do_execve()
然后调用search_binary_handle()
去搜索和匹配合适的可执行文件装载处理过程,比如:
- ELF可执行文件的装载处理过程叫做 load_elf_binary();
- a.out 可执行文件的装载处理过程叫做 load_aout_binary();
- 可执行脚本文件的装载处理过程叫做 load_script();
以ELF可执行文件的处理过程为例,说明一下装载处理的主要步骤:
- 检查ELF可执行文件格式的有效性。
- 寻找动态链接的“.interp”段,设置动态链接器路径
- 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射。
- 初始化ELF进程环境
- 将系统调用的返回地址修改成ELF可执行文件的入口点。
- 这个入口点取决于程序的链接方式
- 静态文件即是ELF文件的文件头中e_entry所指的地址
- 动态链接的ELF可执行文件,程序入口点是动态链接器。
当处理过程函数结束返回到用户态空间时,即可展开对新程序的执行了。
6.6 Windows PE 的装载
- 相对虚拟地址(RVA,Relative Virtual Address)
- 目标地址(TA,Target Address)
- 基地址(BA,Base Address)