早期的计算机中,要运行一个程序,会将这个程序全部装入内存,也就是说程序访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证程序用到的内存总量要小于计算机实际物理内存的大小。于,是有了以下的问题:
1.进程间的地址空间不隔离(直接整个运行在内存上,会导致进程间数据的相互影响)
2.内存使用率低(当内存空间不足以运行更多的程序时,不得不将某个在运行的程序的部分数据暂时拷贝到硬盘上,然后载入更多的程序)
3.程序运行的地址不确定(腾出了足够的内存空间,但腾出和分配时是随机的)
以上的问题衍生出变通的方法:分段
即增加一个中间层:虚拟地址。操作系统经由虚拟地址映射到实际的物理内存地址。
则程序访问的不再是实际的物理内存地址,而是虚拟地址。只要操作系统处理好虚拟地址到物理地址的映射,就能保证不同的程序最终访问的内存地址处于不同的区域(隔离)。
32位的系统中是4GB虚拟内存,因为一个指针长度是4字节。寻址能力是0x00000000 - 0xFFFFFFFF.
分段的方法解决了问题1和问题3。
但内存使用率低的问题仍然存在,因为程序仍然是整个的载入的。事实上,程序的运行有局部性特点,在某个时间段内,程序只是访问一小部分数据,即程序的大部分数据在一个时间段内都不会被用到。
所以衍生出更小粒度的方法:分页
即把地址空间等分成固定大小的页,一般是4kb或4Mb.分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘中。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。
实例:一个PE文件(可执行文件)就是一些编译链接好的数据和指令的集合,它会被分成很多页,在PE执行的过程中,它往内存中装载的单位就是页。操作系统先为该程序创建一个4GB的进程虚拟地址空间(为了映射机制所需要的数据结构:页表和页目)。
当创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是根据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系。当CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误(Page Fault) , CPU 也就知道了操作系统还未给该 PE 页面分配内存, CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。也就是说,先是依据PE文件的头部信息来建立虚拟地址空间中页与PE文件的映射关系,然后由CPU和操作系统协作产生缺页错误来建立虚拟地址空间和实际物理地址空间的映射。(CPU要执行哪页时,才真正的形成完整的映射)
函数调用:
1.首先,将所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
2.把当前指令的下一条指令的地址压入栈中,即返回地址。
3.跳转到函数体执行。
4.指令call执行时,先把ebp压入栈中(old ebp)。而后mov ebp,esp(即ebp指向栈顶,此时栈顶就是old ebp),再然后sub esp,XXX(在栈上分配临时空间),再就是push XXX(保存寄存器)
调用惯例:
1、参数传递方式:从右至左的顺序压参数入栈(对于一个foo(int m,int n )这样一个函数来说,参数入栈时的顺序就是先n后m)
2、出栈方:由函数调用方将参数等出栈(因此在前面的函数调用收尾的代码中,并没有出现对于返回地址上面的参数部分的出栈工作,它是由调用方完成的,对于被调用函数来说对其进行任何操作)