本文只为整理思路,供自己日后参考。现在就从从一个执行文件a.out的运行开始,自上而下地分析linux是如何运行一个执行文件的。
1、首先,需要了解一下a.out这个目标文件。a.out在linux下是ELF(Executable Linkable Format)文件格式,该目标文件由一个文件头、代码段、数据段(已初始化)、从定位信息区、符号表及符号名字字符串构成,如下左图所示,经过链接后生成执行文件如下右图所示,需要说明的是1).bss段在目标文件和执行文件中并不占用文件的空间,但是它在加载时占用地址空间;2)链接后各个段在虚拟空间上的地址就确定了,并且linux下,ELF可执行文件默认从地址0x080480000开始分配。
我们知道在linux下运行一个程序只要在shell中执行 ./a.out 这个命令就OK了,剩下的事情操作系统会替我们完成。但是操作系统到底做了什么,它是怎么做的呢,接下来就来解析一下。
2、linux系统中每个程序都运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间。当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制品。子进程通过execve系统调用启动加载器。加载器删除子进程已有的虚拟存储段,并创建一组新的代码、数据、堆、栈段,新的堆和栈被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小组块,新的代码和数据段被初始化为可执行文件的内容,最后将CUP指令寄存器设置成可执行文件入口,启动运行。
执行完上述操作后,其实可执行文件的真正指令和数据都没有别装入内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟内存之间的映射关系而已。现在程序的入口地址为0x08048000,刚好是代码段的起始地址。当CPU打算执行这个地址的指令时,发现页面0x8048000~0x08049000(一个页面一般是4K)是个空页面,于是它就认为是个页错误。此时操作系统根据虚拟地址空间与可执行文件间的映射关系找到页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,并在虚拟地址页面与物理页面间建立映射,最后把文件中页面拷贝到物理页面,进程重新开始执行。该过程如下图所示:
3、这里比较难理解的就是这个分页机制,讲到分页机制,就不得不提linux的分段与分页机制,这也是这篇文章的重点。我们先来看一张图:
这张图展示了虚拟地址进过分段、分页机制后转化成物理地址的简单过程。其实分段机制是intel芯片为兼容以前产品而保留下来的,然后linux中弱化了这一机制。下面我们先简单介绍一下分段机制:
分段提供了隔绝各个代码、数据和堆栈区域的机制,它把处理器可寻址的线性地址空间划分成一些较小的称为段的受保护地址空间区域。如果处理器中有多个程序在运行,那么每个程序可分配各自的一套段。此时处理器就可以加强这些段之间的界限,并确保一个程序不会通过访问另一个程序的段而干扰程序的执行。为了定位指定段中的一个字节,程序必须提供一个逻辑地址,该地址包括一个段选择符和一个偏移量。实模式下,段值还是可以看作地址的一部分,段值位XXXXh表示以XXXX0h开始的一段内存。而保护模式下,段值仅仅变成了一个索引,只想一个数据结构的一个表项,该表项中定义了段的起始地址、界限、属性等内容。cs、ds等寄存器中存的就是这个段选择符,用段选择符中的段索引在GDT或LDT表中定位相应的段描述符,把段描述符中取得的段基地址加上偏移量,就形成了一个线性地址。