《程序员的自我修养》p6 可执行文件的装载与进程
进程虚拟地址空间
每个程序被运行起来以后,都将拥有自己独立的虚拟地址空间,这个虚拟地址空间的大小由计算机的硬件平台决定,即由CPU的位数决定。
一般来说,C语言指针大小的位数与虚拟空间的位数相同
我们下文以32位的地址空间为主进行讨论:
整个4GB被划分为两部分,其中操作系统本身用去了一部分:从地址0xC0000000到地址0xFFFFFFFF,共1GB;剩下的0x00000000到0xBFFFFFFF共3GB都是留给进程使用的;也就是说整个进程在执行的时候,所有的代码、数据包括通过C语言malloc()等方法申请的虚拟空间之和不可以超过3GB。
装载的方式
覆盖装入和页映射是两种很典型的动态装载方法,都是利用了程序局部性原理。
动态装入的思想就是用到哪个模块,就将哪个模块装入内存,如果不用就暂时不装入,存放在磁盘中。
1、覆盖装入
由覆盖管理器来管理模块代码何时应该驻留在内存而何时应该被替换掉;
当存在多个模块时,程序员需要手工将模块按照他们之间的依赖关系组织成树状结构
有两点需要保证:
1) 这个树状结构中从任何一个模块到树根(也就是main)模块叫做调用路径。当该模块被调用时,整个调用路径上的模块都必须在内存中。
2) 禁止跨树间调用
2、页映射
与覆盖装入原理相似,页映射也不是一下子就把程序的所有数据和指令都装入内存,而是将内存和所有磁盘中的数据和指令按照“页”为单位划分成若干个页,以后所有的装载和操作的单位都是页。
这就是现代操作系统中的存储管理器,几乎所有主流操作系统都是按照这种方式装载可执行文件的。
从操作系统角度看可执行文件的装载
进程的建立
从OS角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。很多时候一个程序被执行同时都伴随着一个新的进程被创建,那么我们来看一下这种最通常的情形:创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只需要做三件事情:
1) 创建一个独立的虚拟地址空间;
2) 读取可执行文件头,并且建立虚拟文件空间与可执行文件的映射关系;
3) 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
首先是创建虚拟地址空间.我们知道虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构;
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。页映射关系函数是虚拟空间到物理内存的映射关系,这一步做的是虚拟空间和可执行文件的映射关系。(当操作系统捕捉到缺页错误时,应该知道程序当前所需要的页在可执行文件的哪一个位置,这就是虚拟空间与可执行文件之间的映射关系)
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件
很明显,这种映射关系只是保存在操作系统内部的一个数据结构。Linux空间中的一个段叫做虚拟内存区域(VMA),在Windows中将这个叫做虚拟段。
在上例中,当操作系统创建进程后,会在进程相应的数据结构中设置一个.text段的VMA:它在虚拟空间中的地址为0x08048000~0x08049000,它对应ELF文件中偏移为0的.text;
将CPU指令寄存器设置成可执行文件入口,启动运行。在进程角度看就是执行了一条跳转指令,直接跳转到可执行文件的入口地址;
页错误
进程虚存空间分布
ELF文件链接视图和执行视图
ELF被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度应该都是系统页长度的整数倍;如果不是,那么多余的部分也将占用一个页,这会造成许多内存浪费。
操作系统装载可执行文件时,它不关心可执行文件各个段所包含的实际内容,只关心一些跟装载相关的问题,最主要的是段的权限:
1) 以代码段为代表的权限是可读可执行的段;
2) 以数据段和BSS段为代表的权限是可读可写的段;
3) 以只读数据段为代表的权限为只读的段
我们选择的方案是:对于相同权限的段,把它们合并到一起当做一个段进行映射
ELF可执行文件引入了一个概念叫做“segment”,一个“segment”包含一个或多个属性类似的“sectioon”,这样映射以后在进程虚存空间中就只有一个相对应的VMA,可以很明显减少页面内部碎片,从而节省了内存空间。
“Segment”实际上是从装载的角度重新划分了ELF各个段,把那些属性相似的、又连在一起的段叫做一个“Segemnt”,而系统正是按照“Segment”而不是“section”来映射可执行文件的。
描述“section”属性的结构叫做段表,描述“segment”的结构叫做程序头,它描述了ELF文件该如何被操作系统映射到进程的虚拟空间
堆和栈
在操作系统中,VMA除了被用来映射可执行文件的各个“Segment”以外,操作系统也可以通过使用VMA来对进程的地址空间进行管理。进程在执行时候的堆、栈等空间,它们在进程的虚拟地址中的表现也是以VMA的形式存在的,很多情况下,一个进程中的栈和堆分别都有一个对应的VMA。