可执行文件只有装载到内存以后才能被CPU执行
6.1进程虚拟地址空间
程序和进程的区别:
程序:是一个静态概念,它就是一些预先编译好的指令和数据集合的一个文件。
进程:是一个动态概念,它是程序运行时的一个过程,很多时候把动态库叫做运行时。
程序被运行起来以后,它就拥有了独立的虚拟地址空间。虚拟地址空间大小由CPU的位数决定。
下面使用的CUP都是以32位为主。程序运行的时候,进程只能使用那些操作系统分配给进程的地址。
Linux操作系统将进程的虚拟地址做了如下分配:
6.2 装载的方式
程序执行时所需要的指令和数据必须在内存中才能够运行。
最简单的就是静态装入:将程序运行所需要的指令和数据全都装入内存中。
当当程序所需要内存大于物理内存时,这个时候就要使用动态装入了:因为程序运行时是有局部性原理的,所以我们将程序最常用的部分驻留在内存中,不太常用的数据存放在磁盘里面。
动态装入的两种方法:
- 覆盖装入
- 页映射
6.2.1 覆盖装入
覆盖装入在没有发明虚拟存储之前使用比较广泛,现在已经几乎被淘汰。
覆盖装入的方法就是程序员在编写程序的时候必须手工将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块何时应该驻留内存何时应该被替换掉。
如果程序有多个模块,程序员需要手工将模块按照它们之间的调用依赖关系组织成树状结构。
6.2.2 页映射
页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。
页映射就是将内存和所有磁盘中的数据和指令按照页为单位划分成若干个页,以后所有的转载和操作的单位就是页。硬件规定页的大小有4096字节、8192字节、2MB、4MB等。
6.3 从操作系统的角度看可执行文件的装载
在动态装载中,可执行文件中的页可能被装入内存中的任意页。
6.3.1 进程的建立
从操作系统的角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。
程序创建的最通常情况:创建一个进程,然后装载相应的可执行文件并且执行。
在有虚拟存储的情况下,上述过程只需要要做三件事:
- 创建一个独立的虚拟地址空间
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
首先创建虚拟地址空间:创建一个虚拟地址空间并不是创建空间而是创建映射函数所需要的数据结构。
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系:当程序发生页错误的时候,操作系统将从物理内存中分配一个物理页,然后将该”缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系。当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域,在Windows中将这个叫做”虚拟段”。
将CPU指令寄存器设置成可执行文件入口,启动运行:操作系统通过设置CPU的指令寄存器将控制权交给进程,由此进程开始执行。
6.3.2 页错误
当CPU开始打算执行一个地址指令时,发现页面是空页面,于是它认为这是一个页错误。CPU将控制权交给操作系统,操作系统将查询第二步建立的数据结构,然后找到VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权交回给进程,进程从刚才错误页位置重新开始执行。
6.4 进程虚存空间分布
6.4.1 ELF文件链接视图和执行视图
前面的例子的可执行文件只有一个代码段,所有它被操作系统转载至进程地址空间之后,相对应的只有一个VMA。
当段的数量增多时,就会产生空间浪费问题。为了避免这种问题,可以把相同的段合并。
操作系统至关心段的权限(可读,可写,可执行)。
ELF文件中,段的权限基本是三种:
- 以代码段为代表的权限可读可执行的段
- 以数据段和BSS段为代码的权限为可读可写的段
- 以只读数据为代表的权限为只读的段
一个简单的方案:对于相同权限的段,把它们合并到一起当作一个段进行映射。
一个”Segment”包含一个或多个属性类似的”Section”。
从链接的角度看,ELF文件是按”Section”存储的,从装载的角度看ELF文件又可以按照”Segment”划分。
“Section”和”Segment”是从不同角度来划分同一个ELF文件的。这个在ELF种被称为视图,从”Section”的角度来看ELF文件就是链接视图,从”Segment”的角度来看就是执行视图。
ELF可执行文件中有一个专门的数据结构叫做程序头表用来保存”Segment”信息。
数据段和BSS段唯一区别就是:数据段从文件中初始化内容,而BSS段的内容全都初始化为0。
6.4.2 堆和栈
操作系统通过使用VMA来对进程的地址空间进行管理。进程在执行的时候它还需要用到栈、堆等空间。一个进程中的栈和堆分别对应一个VMA。
进程虚拟地址空间的概念:操作系统通过给进程空间划分一个个VMA来管理进程的虚拟空间;基本原则就是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为几种VMA区域:
-
- 代码VMA,权限只读、可执行;有映像文件。
- 数据VMA,权限可读写、可执行;有映像文件。
- 堆VMA,权限可读写、可执行;无映像文件,匿名,可向上扩展。
- 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展。
6.4.3 堆的最大申请数量
理论上Linum下虚拟地址空间分给进程本身的是3GB,win是2GB,但实际上,Linux分配的程序的最大内存是2.9GB,win分配的为1.5GB。
有些操作系统使用了一种叫做随机地址空间分布技术,使得进程的堆空间变小。
6.4.4 段地址对齐
可执行文件最终是要被操作系统装载运行的,这个装载的过程一般是通过虚拟内存页映射机制完成。在映射过程中,页是映射的最小单位。
6.4.5 进程栈初始化
进程刚开始启动的时候,须知道一些进程运行的环境,最基本的就是系统环境变量和进程的运行参数,很常见的一种做法是操作系统在进程启动前将这些信息提前保存到进程的虚拟空间的栈中。
6.5 Linux内核装载ELF过程简介
首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。
6.6 Windows PE的装载
RVA:它表示一个相对虚拟地址,相当于文件中的偏移量的东西。它是相对于PE文件的装载基地址的一个偏移地址。每个PE文件在装载时都会有一个装载目标地址,这个地址就是基地址。
装载一个PE可执行文件的过程:
- 先读取文件的第一个页,这个页包含了DOS头、PE文件头、段表
- 检查进程地址空间中,目标地址是否可用,如果不可用,则另外选一个转载地址
- 使用段表中提供的信息,将PE文件中所有段一一映射到地址空间中相应的位置
- 如果装载地址不是目标地址,则进行Rebasing
- 装载所有PE文件所需要的DLL文件
- 对PE文件中所有导入符号进行解析
- 根据PE头中指定的参数,建立初始化栈和堆
- 建立主线程并且启动进程
PE文件中,与装载相关的主要信息都包含在PE扩展头和段表中。