MMU(Memory Management Unit):内存管理单元,它将虚拟地址转换位物理地址。
程序和进程的区别:程序是静态的,进程是动态的。进程是程序运行时的一个过程,程序是预先编译好的指令和数据的集合。
一般来说C语言中的指针大小与虚拟地址空间的位数相同,例如,Win32平台下虚拟地址空间为32位,所以指针大小也是32位。
程序都是运行在虚拟地址空间中的。
一般来讲,内存空间被分成两部分——系统区和用户区,我们平时所能操作的都是用户区,系统区是被禁止的。
32位的CPU最大支持4G内存,这实际上说的是虚拟地址空间大小,但实际上你可以将物理地址空间扩展到4G以上。所谓的32位的操作系统也就是支持最大虚拟地址空间为232=4G的操作系统。
程序在运行时所需要的内存往往大于物理内存所能提供的。
程序的局部性原理就是说程序在运行时只有一部分需要放进内存。
动态装载的基本原理是程序的局部性原理,它的思想是程序用到哪个模块就把哪个模块放进内存。
动态装载的两种典型的方法是覆盖装入和页映射。
程序员手工编写用于控制模块动态装载的管理代码,它被称为覆盖管理器。
它的思想是各模块轮流共用内存的一片区域,程序员手动控制哪个模块在某一时刻该进入该区域。
模块之间往往存在依赖的关系,这使得独一模块无法完成功能,这种依赖关系使各模块间形成了树状结构。加载一个模块的时候也必须把存在依赖关系的模块,换句话说就是该模块的组成模块,也一同加载进内存。这也说明你不要把非组成模块加载进内存。
模块覆盖慢,因为它需要把模块在内存和硬盘之间换进换出,而且还需要经过覆盖管理器。
它是一种时间换空间的解决方案。
它把磁盘中所有的数据和指令以页为单位进行划分,装载用的也是页。
比如说现在页的大小是212=4096byte,内存大小为512M=229byte,所以在某一时刻该内存可容纳最多229/212=217个页面,即131072页。
虚拟存储的发展使得页映射机制成为可能,如果是物理地址直接装载的话,那么还需要重定位,好在现阶段操作系统已经接管了这一工作。
一个进程区别于其他进程的特征就是它拥有它自己独立的虚拟地址空间。
创建进程通常需要做三件事:
1、创建独立的虚拟地址空间。
2、读取可执行文件,建立虚拟地址空间与可执行文件的映射关系。
3、将CPU的指令寄存器设置为程序执行入口,开始执行。
创建虚拟空间并不是创建空间而是创建应设函数所需要的数据结构,例如Linux只创建一个也目录就完事了。它的作用是创建虚拟地址空间与物理地址空间的映射关系。
当程序执行到某一处时发现它所需要的指令和数据不在内存中,这时候就产生了缺页中断,操作系统捕获到该中断就磁盘中的可执行文件中的该页换入到内存中。但是问题是该把可执行文件中的哪一页还进内存呢?解决方案是把虚拟地址空间和可执行文件之间建立映射关系。这种映射关系保存在操作系统中的一个数据结构中,在Linux下叫VMA(Virtual
Memory Area),在Windows下叫VS(Virtual
Section)。这里边存储着ELF的虚拟地址空间的起始和终止地址、段名、段的属性等。
然后操作系统控制CPU将控制权转交给程序,具体来讲是跳转到程序入口,自此程序开始执行。
ELF文件中的段大小是页的整数倍,页比段小。
操作系统关心的只是段的权限,即,只读、只写、读写。
把相同权限的段合并到一起进行映射,可以页面碎片,节省内存空间。ELF就是这样做的。从虚拟存储的角度讲ELF分成若干segment,每个segment又分成若干section。
ELF被划分成3个segment,它们是可读可执行的VMA0、可读可写VMA1和调试信息+字符串表。
section是ELF的链接视图而segment是ELF的执行视图。
在装载时指的是ELF的执行视图,即,segment。
segment的信息保存在ELF文件中的程序头表中。
由于ELF目标文件不会被装载,因此目标文件没有程序头表,剩下的像可执行文件和共享库文件都是由程序头表的。和前面讲的结构一样,程序头表也是个结构体,它的名字叫Elf32_Phdr。
操作系统还通过VMA对进程的地址空间进行管理,比如堆和栈。
AVMA(Anonymous Virtual Memory Area)是没有映射到文件中的VMA。几乎每个进程都有3个AVMA,它们是堆、栈和vdso。其中,vdso是进程与内核通信的模块。
P192~P193之间对进程与VMA之间的关系做了简要的总结概括。
从图6-9可以看出内存虚拟地址空间中从低地址到高地址依次分布着code、data、heap和stack等几个区域。
堆主管动态空间分配,它最多能分配的空间受操作系统版本、程序本身大小、动态/共享库数量、程序栈数量大小等因素影响。
这一点决定了虚拟起址必须是页大小的整数倍,于是这就存在一个优化的问题。即,如何优化才能使空间利用率提高。
当一个页面的实际大小不足一页但却非要占去一个页空间的话会浪费很大的页面。
为此UNIX提出了一个解决方案。它让两个段之间相邻的部分共享一块物理内存区域,然后再把这块区域映射到各段各自的虚拟地址空间中去。
比如seg0和seg1是相邻的,seg0独立的部分被映射在5Page处,seg1和seg0相邻的地方被映射在4Page处,seg1独立的部分被映射在3Page处。由于4Page处是共享的部分,所以它要被映射到seg0和seg1各自的虚拟空间中去。即,seg0的虚拟地址空间=seg0独立的虚拟地址空间+seg0与seg1相邻的虚拟地址空间。而seg1的虚拟地址空间=seg1独立的虚拟地址空间+ seg0与seg1相邻的虚拟地址空间。
从这张图也可以看出可执行文件的各个段是先被映射成物理地址空间,然后再被映射成虚拟地址空间的。
它是一种系统提供的程序运行的环境,进程栈中保存着系统环境变量和运行参数,它也同样是在虚拟地址空间中。
图6-12展示了Linux进程初始化栈的布局。
ESP(Extended stack pointer):栈顶指针。
bash:unix下的shell。
bash调用fork()创建新进程,新进程再调用execve()系统调用执行ELF文件。
在内核中execve()的入口是sys_execve(),sys_execve()在进行一些参数检查复制后调用do_ execve()。
do_ execve()会检查可执行文件的前128字节的内容以判断该文件的格式。每种可执行文件的前4个字节被称为魔数,每种可执行文件的魔术都是不同的。
然后,do_
execve()再调用search_binary_handle()查找和匹配相应的可执行文件的装载处理过程,这一过程是由search_binary_handle()检查魔数来完成的。
装过处理过程所要做的事情可参见P199所述内容。
以上过程完成以后EIP(Extend
Instruction Pointer)寄存器就指向了ELF程序的入口,于是ELF程序开始执行。
PE文件中的段的起始地址和长度都是页的整数倍。
PE文件中段的数量比ELF文件中的段数量少很多,一般只有代码段、数据段、只读数据段和BSS等几个。
RVA(Relative Virtual Address):它是相对于PE文件装载基址的偏移地址。
每个PE文件都会被装载到某一特定地址,改地址被称为目标地址,也就是基地址,这个基地址不是固定的。
PE文件的装载过程可参见P200~P201所述部分。
与PE文件装载有关的信息都在PE扩展头(PE Optional Header)和段表中。