非原创,原出处http://www.cnblogs.com/Kratos/archive/2009/09/09/1563624.html
1 介绍
Windows 32位 x86 操作系统最多能访问4GB的物理内存。这是因为处理器的寻址总线是32条(我们常说32位),能够访问的存储单位的范围是从0x00000000到0xFFFFFFFF,即4GB。Windows同样允许每个进程拥有自己的4GB逻辑地址空间。4G的逻辑地址空间中,可供用户操作的只有低位的2GB(是在用户模式(user mode)下可操控的),高位的2GB被windows核心模式(kernel mode)霸占了。
有人可能要问了,我机器总共没有几G内存,一个进程就可控4GB,那几个进程还不打起来。。。
好问题,为了实现这个功能,windows采用了x86处理器(386及其后续的处理器)的一个功能,叫做分页Paging。
分页机制引入了逻辑地址这个概念,它允许软件,或进程,使用逻辑地址,而不是物理地址。处理器的分页单元能够透明的将逻辑地址转换为物理地址。基于这个机制,windows实现了让每个进程拥有4GB逻辑空间的梦想。
想了解更多,我们先要看看x86的分页机制是如何工作的。
2 x86 处理器的分页机制
x86将物理地址空间(或物理内存)分为4KB的小块,称为内存页Page。
如此算来,4GB的内存就有1Mega(1024x1024)4KB页了。处理器使用二级索引结构来管理这1Mega内存页。你可以想象成一个1024x1024的二维矩阵。
一维叫做页目录Page Directory,二维叫做页表Page Table。
首先我们创建一个页目录包含1024项Entry,每一项指向一个页表,这样我们就有1024个页表。
每个页表也有1024项Entry,每一项指向4KB页。
:
每一个页目录的项Page Directory Entry(简称PDE)实际上是一个4byte的指针,指向一个页表Page Table。同样,每个页表项Page Table Entry(简称PTE)也是一个4byte的指针,指向4KB的物理内存地址。
因此我们需要4MB来构造这个二级索引结构来管理1024PDE,每个PDE包含1024PTE,即4 x 1024 x 1024 bytes。
综上所述,整个内存空间被分为4KB的页。因此当一个PDE或者PTE被使用时,它的高20位指向一个4KB的内存页,低12位用来表示页保护信息和其它的一些内部信息(OS的一些函数会使用)。高20位所表示的实际物理内存被称为页面号Page Frame Number(简称PFN)。
3 windows 页表管理
在windows系统中,每个进程都拥有自己的页目录和页表,即Windows为每个进程分配了4MB的空间来维护这个表。
当一个进程被创建时,页目录PD的每个项都指向页表PT的物理地址。
页表PT的每个项可以是有效的或无效的,有效的PTE指向分配给进程的4KB页物理内存。
无效的PTE会被设置成特殊位来表示其无效,我们称它们为无效PTE。
当进程要求内存时,这些PTE就被设置成分配的物理内存页。
这里有一件事需要强调,对于进程来说,它不知道物理内存的任何细节,它只使用逻辑地址。至于如何将逻辑地址映射到物理地址,这个是由Windows内存管理和处理器来完成的。
页目录PD的物理内存被称为页目录基本地址Page Directory Base address(简称PDBA)。这个页目录基本地址保存在一个特殊的CPU寄存器CR3(在x86)。当上下文切换时,windows会重设CR3的值并让其指向当前进程的PDBA。这样,每个进程都能得到自己的4GB物理内存空间了。当然,同一时刻,系统所有进程的总的内存分配不可能超过RAM+pagefile的大小,但上述的机制可以允许windows给每个进程自己独立的4GB逻辑(或者虚拟)地址空间。我们称其为虚拟地址空间是因为即使进程有整个可用的4GB地址空间,它也只能使用配置给它的内存。当一个进程试图访问没有配置的内存地址时,系统会给出一个违法访问的错误,因为对应的PTE指向的是一个无效值。
此外,进程要求的内存大小不能超过当前系统可配置内存的大小。
将逻辑内存与物理内存分离的方法有许多优点,一个进程可以拥有4GB的线性空间,因此程序员无需担心像Dos时代的内存碎片问题。它还允许windows运行多个进程彼此使用同一机器上的物理内存而不用担心A进程访问到B进程的地址空间(除非A,B进程使用一些共享内存)。所以,一个进程不可能读写另一个进程的内存。
逻辑地址到物理地址的转换是由处理器完成的。一个32位的逻辑地址被分成下图所示的3部分。
处理器首先根据CR3找到页目录的物理地址,然后由逻辑地址的前10位来索引对应的PDE,接着的10位来索引页表的PTE,从而得到PTE指向的4KB物理内存页。逻辑地址的低12位用来指向内存页中具体的位置,相当于页内的一个offset。
4 内存保护
windows对所有进程提供内存保护,例如一个进程不能访问其他进程的内存。这个确保了系统中同时几个进程友好共存。那么Windows是如何做到的呢:
每个进程的PTE中只存放分配给这个进程的物理内存地址。这保证了进程无法访问不属于自己的内存。
流氓进程(原文写的是a rouge process,rouge是胭脂,口红的意思,实在想不通,估计可能是a rogue process)可能试图修改它的页表来访问其它进程的物理内存。为了防止这种流氓行为,windows将页表存放在核心态,即高位的2GB内存空间。这样以来,一个用户程序就无法访问或修改页表。当然,一个核心态的驱动程序可以做这种“流氓”行为,因为一旦一个程序运行在核心态,就相当于拥有了整个系统的权限。下一节会具体解释这一点。
5 windows 逻辑内存布局
windows将逻辑地址空间的低位2GB(或者3GB,通过设置boot.ini可以达到)设置为用户态,高位的2GB(或1GB)给windows核心态。在核心态中,windows为页表和页目录保留了从0xC0000000到0xC03FFFFF的空间。每个进程的页表都在0xC0000000的逻辑地址,页目录在0xC0300000的逻辑地址。逻辑内存的布局如下图所示:
你可以用windows核心态调试器kd或者windbg来证实这个(参考!pte和!vtop调试指令)。页目录的物理地址存放在CR3.从0xC0300000开始的1024个地址表示了PDE。每个PDE占用4个字节的地址指针指向一个页表。每个页表有1024个项,每个项存放着4字节物理地址指向4KB物理内存页或者无效项的标识符。
那么为什么windows用0xC0000000和0xC0300000的逻辑地址来存放页表和页目录呢?
windows要将页表和页目录放在核心态,不是0x80000000(2GB的高位相邻地址)就行了吗。
。。。对,但是用户程序要求更多的内存来实现自己的大型程序。。。于是3GB模式登场了!cool!
用户可以编辑一个特殊的配置文件boot.ini来开启3GB设置,这样windows允许一个用户态程序可以控制低位的3GB内存。而0xC0000000则是3GB的高位相邻地址。所以我估计这就是页表和页目录选址的依据。仅供参考,具体请参看Windows文档。。。当然还有一些其他的重要因素决定了页表和页目录在内存中的布局。为了看得更明白些,我伪造了一个进程并画了一张页表,高亮一些页表项。注意,每个项的大小是4字节。P_PT表示页表的物理地址。
PDB表示该进程的页目录基地址。它表示该进程0xC0300000逻辑地址对应的物理地址。这个值保存在CR3中。注意,windows只能使用逻辑地址来访问任何内存,包括页目录,因此要访问页表和页目录,必须要在页目录中保持一些回访性息如PDB。上面页表显示的物理地址项对每个进程都不同,但是每个进程都有自己的PDB项存放在页目录0x300的地方。
下面我们试着将4个不同逻辑地址转换为物理地址,来体会一下PDB项的重要性,以及页表布局和地址转换如何工作。
我们将要转换的地址是:0x2034AC54,0xC0000000,0xC0300000,0xC0300000[0x300]即0xC030C00。
第一个地址是普通的用户态进程的逻辑地址,第二个地址是逻辑地址空间中第一个页表的第一个逻辑地址,第三个地址是页目录基地址的逻辑地址,第四个地址是一个特殊项的逻辑地址。
假设CR3指向一个物理地址为0x13453###的内存。 前面讲到,低12位用来存放页保护信息和OS需要的其他数据,这与我们当前的主题无关,因此将其屏蔽为###。高20位表示页面号PFN是4KB对齐页的物理地址。PFN对应真正的物理地址是0x13453000。让我们做如下的地址转换:
1. 0x2034AC54
我们把0x2034AC54分成三部分0010000000 1101001010 110001010100
高10位0010000000是页目录的索引。转成16进制是0x080。
从CR3得到也目录的物理地址是0x13453000,逻辑地址是0xC0300000。
因此0xC0300000[0x080]可以得到页表的物理地址P_PT。从上面的页表可以知道其逻辑地址是0xC00001000(物理地址是0x45045000)。现在我们看接着的10位1101001010(即0x34A)为页表的索引。
地址0xC00001000[0x34A]可以得到4KB页的物理地址,从上表可以得到为0x34005000。
最后的12位110001010100(即0xC54),是相对于4KB页内存起始位置的偏移。所以最终的物理地址是0x34005C54。
剩下的3个交给读者了。一旦你完成了,你就能明白为什么PDB要存在0x300的位置。
我也是读者,做一个吧。
2. 0xC0000000
同样我们把0xC0000000分成三部分1100000000 0000000000 0000000000
高10位1100000000是页目录的索引。转成16进制是0x300。
从CR3得到也目录的物理地址是0x13453000,逻辑地址是0xC0300000。
因此0xC0300000[0x300]可以得到页表的物理地址P_PT。从上面的页表可以知道其逻辑地址是PDB的地址0xC03000000(物理地址是0x13453000),还在页目录里,于是再查0xC0300000[0x000]项的值,是0xC0000000(物理地址是0x6A078000)。现在我们看接着的10位0000000000(即0x000)为页表的索引。
地址0xC00000000[0x000]可以得到4KB页的物理地址,从上表可以得到为0x10480000。
绕了一圈,结论是页表中的值存放的肯定就是对应的4KB页的物理地址。