《程序员的自我修养》笔记--可执行文件的装载

来源:互联网 发布:淘宝店铺出售转让平台 编辑:程序博客网 时间:2024/05/20 21:42

程序执行时所需要的指令和数据必须都在内存中才能正常运行,最简单的办法就是将程序运行时所需要的指令和数据全部都装入内存,这样程序就能顺利执行,这就是最简单的静态装入的方法。但是程序所需要的内存数量可能大于物理内存,静态装入就不太现实。

 

后来研究发现,程序运行时具有局部性原理,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘上,这就是动态装载的原理。

 

  覆盖装入和页映射是两种典型的动态装入方法。覆盖装入就是如果两个模块不会同时运行,则可以使这两个模块共用一块内存,需要哪个模块的时候就装入。现在基本已经淘汰了。

 

页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。在页映射机制中,程序装载和操作的单位都是页。最常见的Inter IA32处理器一般都使用4KB大小的页。

 

假设程序所有的指令和数据总和为32kB,那么程序总共分为8页,将其编号为P0-O7,并假设物理内存只有16kB,编号为F0-F3。如果程序刚开始执行时的入口地址为P0,这时装载管理器发现程序的P0不在内存中,于是将F0分配给P0,并将P0的内容装入F0;运行一段时间后,程序需要用到P5,于是装载管理器将P5装入F1,就这样,当程序用到P3和P6时,它们分别装入到了F2和F3。

 

很明显,如果这时程序只需要P0,P3,P5和P6这4个页,那么程序就能一直运行下去。但是如果这时候需要访问P4,那么装载管理器需要最初选择,它必须放弃目前正在使用的4个物理内存页中的一个来装载P4。至于选择哪个页,可以有多种算法选择,比如FIFO先入先出算法,LRU最近最少使用算法等。

 

其实,上面所说的装载管理器就是操作系统的存储管理器。

 

从操作系统的角度看可执行文件的装载

 

         从操作系统的角度看,一个进程最关键的特征就是它拥有独立的虚拟地址空间,这使得它有别于其他进程。这在http://blog.csdn.net/vividonly/archive/2011/05/04/6393516.aspx一文中有详细解释。要使一个可执行程序得以执行,首先必须创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只要做三件事:

1 创建一个独立的虚拟地址空间。这时候并不设置虚拟地址页和物理地址页的映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。当发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中再设置缺页的虚拟页与物理页的映射关系。这都是通过CPU的MMU来实现的。

 

2 读取可执行文件头(通过ELF文件头的信息可以找到段表的位置,从而找到各个段的位置和信息。),并且建立虚拟地址空间与可执行文件的映射关系(后文即将讨论)。这个映射关系只是保存在操作系统内部的一个数据结构。Linux中将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA, Virtual Memory Area)。比如对于.text段,在创建进程后,会在进程相应的数据结构中设置一个.text段的映射关系,记录了它在虚拟空间的地址以及它在ELF文件中的偏移。当程序执行发生页错误,通过查找这个数据结构,找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中的该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还给进程,进程动刚才页错误的位置重新开始执行。

 

 

3 将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将控制权转交给进程。这里的入口地址就是ELF文件头中的入口地址。

 

在操作系统里面,VMA出来被用来映射可执行文件中的各个段之外,还可以使用VMA来对进程的地址空间进行管理。事实上,进程地址空间中的“栈”和“堆”也是以VMA的形式存在的。

 

下面看一下建立虚拟地址空间和可执行文件映射关系的具体过程,也就是上面第二步的具体实现过程。在讨论之前,我们先关注一个事实,ELF文件被映射时,是以系统的页长度作为单位的,那么每个段在映射的时候应该都是系统页长度的整数倍,如果不是,那么多余部分也将占用一个页。一个ELF文件往往有十几个段,那么内存的浪费是可想而知的。实际上,ELF文件中,段得权限往往是为数不多的几种组合:

  • 以代码段为代表的权限为可读可执行的段
  • 以数据段和BSS段为代表的权限为可读可写的段
  • 以只读数据段为代表的权限为只读的段

Linux的解决方案是:对于相同权限的段,把它们合并到一起当做一个段进行映射。比如有两个段分别叫做".text"和".init",它们包含的分别是程序的可执行代码和初始化代码,权限相同,都是可读可执行,则可以把它们合并,以节省空间。

 

ELF可执行文件有一个专门的数据结构叫做程序头表(Program Header Table),用来保存各个VMA的信息,实际上,它就是上文我们讨论的保存虚拟地址空间和可执行文件映射关系的数据结构。上文中第2步的主要任务也可以概括成:读取可执行文件头,并根据可执行文件的程序头表建立映射关系。

 

可执行的文件的装载是通过虚拟内存的页映射机制来完成的。在映射过程中,页是映射的最小单位。如果我们要在一段物理内存和进程虚拟地址空间之间建立映射关系,这段内存空间的长度必须是4096的整数倍,并且这段空间在屋里内存和进程虚拟地址空间中的起始地址必须是4096的整数倍。如果为每一个段都分配4096的整数倍(比如某个段长度为127,则也需分配4096),势必会造成很大的内存浪费。举个例子,ELF文件中有三个段SEG0,SEG1,SEG2,长度分别为127,9899,1988。按照上面的方法,SEG0必须分配一个页,SEG1必须分配三个页,SEG2必须分配一个页,则总共必须分配5个物理页,对应于5个虚拟页。

 

        为了解决这种问题,有些UNIX系统采用了一个很取巧的办法,就是让那些各个段接壤部分共享一个物理页面,然后让该物理页面分别映射两次。同样是上个例子,实际上SEG0,SEG1,SEG2总共只需要三个物理页PAGE1,PAGE2,PAGE3就能装下,但是PAGE1会同时装有SEG0和SEG1,PAGE3也会同时装有SEG1和SEG2。此时,让PAGE0和PAGE3各映射两份到虚拟地址空间,PAGE1映射一份就行。在这种映射方式下,多个虚拟页面映射到了一个物理页面,对于一个物理页面来说,它可能同时包含了两个段的数据,甚至可能多于两个段。

 

        通过以上页映射机制,可执行文件和虚拟地址空间的映射关系,以及虚拟地址空间和物理空间的映射关系就都确定了。

 

        这里有一个疑惑:前面写静态链接的文章中提到过,链接后可执行文件中各段的虚拟地址已经分配。也就是说这里讨论的第二步建立映射关系所要用到的程序头表在链接时就已经建好了了。不知道这样理解对不。上文讨论的很多东西其实都是链接时的东西,不是装载时做的。

 

       最后总结一下Linux内核ELF文件的装载过程:

  • 检查ELF可执行文件的有效性,比如魔术,程序头表中段的数量。
  • 寻找动态链接的“.interp”段,设置动态链接器路径(与动态链接有关)。
  • 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码,数据,只读数据。
  • 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(和动态链接有关)
  • 将系统调用的返回地址修改为ELF可执行文件的入口,这个入口点取决于程序的链接方式,对于静态链接的可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。新的程序开始执行,ELF可执行文件装载完成。 
  • 当新程序开始运行后,每当遇到缺页错误(可以理解为没有为虚拟页面关联实际的物理页面,因此产生缺页错误),就会新分配一个物理页,并把该缺页内容(ELF文件的相应内容)从磁盘中读取到内存中,同时设置虚拟页面也物理页面的映射关系,并从缺页的虚拟地址处开始执行。     

 

 


原创粉丝点击