可执行文件只有装载到内存后才能被CPU执行。
6.1进程虚拟地址空间
因为程序在运行的时候处于操作系统的监管下,操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。我们经常在Windows下碰到“进程因为非法操作需要关闭”或Linux下的“segmentation fault”很多时候是因为进程访问了未经允许的地址。
6.2装载的方式
6.2.1覆盖装入
6.2.2页映射
6.3从操作系统的角度看可执行文件的装载
6.3.1进程的建立
创建一个独立的虚拟地址空间
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
创建一个独立的虚拟地址空间。回忆第一章,我们知道一个虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构(1)。
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。上面那一步的页映射关系函数是将虚拟空间到物理内存的映射关系,这一步所做的是虚拟空间与可执行文件的映射关系。当程序执行发生错误时,操作系统会将物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系,这样程序才得以正常运行。但是很明显的一点是,当操作系统捕获到缺页错误时,它应该知道程序当前所需要的页在可执行文件的哪一个位置。
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件。
让我们考虑最简单的情况,假设我们的ELF可执行文件只有一个代码段“.text”,它的虚拟地址为0x08048000,它在文件中的大小为0x000e1,对齐为0x1000。由于虚拟存储页映射都是以页为单位的,在32位的Intel IA32下一般为4096字节,所以32位ELF的对齐粒度为0x1000。由于该”.text”段大小不到一个页,考虑到对齐该段占用一个段。所以一旦可执行文件被装载,可执行文件与进程的虚拟空间映射关系如下图:
很明显,这种映射关系只是保存在操作系统内部的一个数据结构(2)。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA);在Windows中将这个叫做虚拟段。上例中:操作系统创建进程后会在进程相应的数据结构中设置一个.text段的VMA:它在虚拟空间中的地址为0x08048000~0x0804900,它对应ELF文件中偏移为0的.text,它的属性为只读。
VMA(进程虚拟空间中的一个段)是一个很重要的概念,它对于我们理解程序的装载执行和操作系统如何管理进程的虚拟空间有非常重要的帮助。
操作系统在内部保存这种结构,很明显是因为当程序发生段错误时,它可以通过查找这样的一个数据结构来定位错误也在可执行文件中的位置。
将CPU的指令寄存器设置成可执行文件的入口地址(ELF文件头中保存的入口地址),启动运行
6.3.2页错误
建立进程虚拟空间的数据结构1,创建映射函数所需要的相应的数据结构;
装载时的数据结构2,得到文件和虚拟空间的映射关系。
上面步骤执行完后,其实可执行文件的真正指令和数据都没有被装入到内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟之间的映射关系而已。
当CPU开始打算执行这个地址的指令时,发现是个空页面,于是认为是页错误。CPU将控制权交给操作系统,操作系统有专门的页错误处理例程来处理。操作系统将查询数据结构(2),找到空页面所在的VMA,计算出相应页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系(这里用到数据结构(1)了吧),然后把控制权还回给进程,进程从刚才页错误的位置重新开始执行。
6.4进程虚拟空间分布
6.4.1ELF文件链接视图和执行视图
对于“load”类型的“segment”来说,如果P_memsize>P_filesz,就表示该“segment”在内存中所分配的空间大小超过文件实际大小,这部分多余的部分全部填充为0.这样做的好处是,我们在构造ELF可执行文件时不需要再额外设立“bss”的segment了,可以把数据segment的P_memsz扩大,那些额外的部分就是bss。
6.4.2堆和栈
Linux的进程虚拟空间管理的VMA的概念并非与segment完全对应,Linux规定一个VMA可以映射到某个文件的一个区域,或者是没有映射到任何文件;而我们这里的第二个segment要求是,前面部分映射到文件中,而后面一部分不映射到任何文件,直接为0,也就是说前面的从“tdata”到“.data”段部分建立从虚拟空间到文件的映射,而“.bss”和“__libcfreeres_ptrs”部分不要映射到文件。这样两个概念就不完全相同了。所以Linux实际采用了一种取巧的方法…
6.4.3堆的最大申请数量
甚至有可能每次运行的结果都不同,因为有些操作系统使用了一种叫做随机地址空间分布的技术(安全考虑),使得进程的堆空间变小。堆内容第四部分还会详细介绍。
6.4.4段地址对齐
我们要映射将一段物理内存和进程虚拟地址空间之间建立映射关系,这段内存空间的长度必须是4096的整数倍,并且这段空间在物理内存和进程虚拟地址空间中的起始地址必须是4096的整数倍。
pl:
常规虚拟地址映射(VMA)
虚拟内存利用率太低,解决的办法:
让那些各个段接壤部分共享一个物理页面,然后将物理页面分别映射两次(不理解),比如将seg0和seg1的接壤部分的那个物理页,系统将它们映射两份到虚拟地址空间,一份为seg0,另一份为seg1,其他的页都按照正常的粒度进行映射(理解了,只是seg0和seg1按两个小粒度映射了而已,因此可以完全利用物理内存)。
根据上面的段对齐方案,由此我们可以推算出一个规律,那就是,在ELF文件中,对于任何一个可装载的segment,它的p_vaddr除以对齐属性的余数等于p_offset除以对齐属性的余数。比如前面例子中,第二个segment的p_vaddr和p_offset分别为(0x08048000+seg0_size)_4bytes和(seg0_size)_size(seg_0的偏移为0),第三个segment的p_vaddr和p_offset分别为((0x08048000+seg0_size)_4bytes+seg1_size)_4bytes和(seg0_size)_size+(seg1_size)_size(seg_0的偏移为0)。因为段0装载的时候,偏移是0。
6.4.5进程栈初始化
6.5Linux内核装载ELF过程简介
6.6windows PE装载