不知大家在平时想过没有,我们放在磁盘(之前我一直认为Windows的C盘是主存,DEF盘是磁盘,哈哈,应该没有像我这样无知的人吧)上的一个可执行文件(或者应用程序)是如何得到执行的,而且为什么我们在写程序的时候怎么感觉程序中的一些变量的地址好像在各个不同的程序中都差不多,同时这个地址到底真正对应的是什么?是我们可执行文件对应所在位置的磁盘地址吗?下面我就以Linux为平台(Windows也一样,只是将命令方式变为图形方式了)为大家详细讲解一下一个可执行文件是如何得到执行的。
在Linux中当我们打开shell时,我们相当于已经新建了一个进程,这个进程运行的是shell这个应用程序。当在shell中输入一个可执行目标文件的名字时,shell会用fork()函数创建一个新的进程,在这个新进程中调用execve()函数来加载和执行这个可执行文件。
我准备详细来说明一下这个execve()函数是如何来工作的,比如它是如何将磁盘上的目标文件拷贝到主存中来让CPU运行的?程序中我们所看到的地址到底是什么?带着这些问题我们来一步一步分析。
首先因为execve()函数是在shell这个进程的子进程中运行的,而子进程必定会拷贝(其实也不是拷贝,要不然这个进程设计的也太臃肿了,是一种叫写时拷贝的机制)很多父进程已存在的内容,所以必须删除掉。
然后它开始映射(看到映射有没有想到数学中叫函数映射的东西,本质上都是一样的)我们可执行文件中的内容,谈到映射那必然是X------>Y,现在Y是我们的可执行文件,那X呢?先给大家补充一点进程中的知识,等补充完了,才能说X。每个进程中都有一个叫页表的东西,页表有很多项,每一项叫页表项(为了简化问题的复杂性我们就假设Linux是一级页表吧),同时在操作系统中一般一个页或者物理块的大小为4KB(对应为12位的页内地址),所以在一个32的操作系统中只需要保存2^20个页表项就可以表示地址从0x00000000到0xffffffff的范围,其中这个地址的后12位为页内地址,而我们在程序中所见到的地址就是这个地址,根本不是什么我们程序对应的物理地址。记住,这个地址并不是真正对应的磁盘或者内存的地址,而是虚拟的,叫虚拟地址。如果现在还不太明白等我全部讲完就会懂的。
讲到这里大家先稍微理解理解,免得看的一头雾水。那我开始,刚刚我们说到进程中的页表项,每一个页表项从开始到结束对应的编号为0x00000-0xfffff(一共2^20个,大家可以画一画),这个页表项主要有两个部分,第一个部分用来指向磁盘的物理块或者内存上的块,第二个部分表明所指向的块是在磁盘上还是内存上或者这部分就根本没用。
那么我们现在可以说X是什么了,就是虚拟地址!说完了X,Y,那还有映射规则呢,对于我们程序中的文本块,数据块,栈,堆等在Linux中分别对应不同的虚拟地址,而且是固定的,对所有程序都一样。这也就可以解释为什么不同的程序不同的变量有时候地址却差不多,因为他们的虚拟地址都是从0x00000000---0xffffffff,因此当他们的变量都保存在栈中时,对应的虚拟地址也很接近。
映射完之后,execve()调用启动代码,启动代码将调用main()函数,大家一定会想现在可执行目标不是还在磁盘上吗?它是怎么拷贝到内存上,然后被CPU执行的呢?确实如此,因此当启动代码将main()函数的虚拟地址传递给CPU时,CPU通过解析虚拟地址发现内存中没有main()相对应的页或者物理块,然后CPU通过进程中的页表项找到我们可执行文件所在的磁盘位置,将磁盘上的块拷贝到内存中,这样CPU就可以顺利的执行我们的程序了。