进程的装载

程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的方法就是将所需要的指令和数据全部装入内存,这就最简单的静态装载。
熟悉计算机原理的同学可能知道,程序运行是有时间以及空间局部性的,,所以我们一般将程序最常用的部分驻留内存,将一些不太常用的放在磁盘上。这就是动态装载的基本原理。

动态装载的思想就是用哪个就把哪个装载入内存,覆盖装入和页映射就是两种很典型的动态装载方法。

装载方式

覆盖装载

覆盖装载虽然已经被淘汰了,但是作为高手,最基本的三路长拳怎么也要会点。

假设有个main主程序,main会调用A、B两个模块,A、B之间不会互相调用。其中main 1024字节,A 512字节,B 256字节。

那么它们的覆盖装载方式如图:

当main调用A时,覆盖管理器将模块A读入内存;当调用模块B的时候,覆盖管理器会把模块B读入内存,这是A不会用了,所以B会把A覆盖掉

页映射

页映射是将内存和所有磁盘中的指令和数据按照页为单位划分。Intel采用的页大小就是4096字节。
页映射图解:


页置换算法:
页置换算法–百度百科

从操作系统角度看进程的装载

进程的创建

创建一个独立的虚拟地址空间
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
将CPU指令寄存器设置成可执行文件的入口地址,启动运行。

<mark>创建虚拟地址空间</mark>:虚拟空间实际是由一组页映射函数将虚拟空间的各个页映射至相应的物理空间。所以,创建虚拟空间实际上是创建映射函数所需要的相应的数据结构

<mark>读取可执行文件头,创建虚拟空间与可执行文件的映射关系</mark>:创建虚拟地址空间的页映射函数是虚拟空间到物理内存的映射关系,而这一步做的是虚拟空间与可执行文件的映射关系。当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中,在设置缺页的虚拟页和物理页的映射关系。显然,当操作系统捕获到缺页错误时,它应该知道程序当前所需页在可执行文件中的位置。这就是虚拟空间和可执行文件之间的映射关系。

<mark>将CPU指令寄存器设置成可执行文件的入口,启动运行</mark>:操作系统通过设置CPU的指令寄存器将控制权转交给进程,由此进程开始执行。从进程的角度看这一步可以简单的认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址(保存着ELF文件头)。

页错误

进程创建之后,其实可执行文件的真正指令和数据都没被装入到内存中,操作系统只是建立起可执行文件和进程虚拟内存的映射关系。

假设在上面的例子中,程序的入口地址为0x08048000,即刚好是.text段的起始地址。当CPU
开始打算执行这个地址的指令时,发现页面0x08048000~0x08049000是个空页面,于是它
就认为这是一个页错误(Page Fault)。

当发生页错误的时候,操作系统将查询进程建立第二步建立的数据结构,找到空页面所在的虚拟内存,计算相应页面在可执行文件中的偏移,然后在物理内存分配一个物理页面,建立映射关系,然后再把控制权还给进程。

linux下ELF文件装载

检查ELF可执行文件有效性,如魔数
寻找动态链接的.interp段,设置动态链接器路径
根据ELF可执行文件的程序头表描述,对ELF文件进行映射,比如代码、数据、只读数据
初始化ELF进程环境
将系统调用返回地址修改为ELF可执行文件的入口点

参考文献

[1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.