内存管理可以说是操作系统实现中最重要的环节,也是最为复杂的一环节。对于相对贫乏的内存资源,内存共享也成了一个很重要的有效手段。Windows 2000/XP在此方面的实现借助于一个称为原型PTE(Prototype PTE,PPTE)的软件机制。在《小议Windows NT/2000分页机制》中我详细的介绍了Intel X86实现分段、分页的硬件PTE工作方式。我们来回顾一下这种机制:
假设我们的一个进程映射了从虚拟地址0xXXXXXXXX(假设位于分配粒度上)开始的4M空间,而这4M空间当前都相应的映射了实际的物理内存(鉴于Lazy evaluation等的先进思想,这种情况在Windows 2000/XP中比较少见)。我们将这4M空间分成1000块的4K(PAGE_SIZE,X86处理器决定),对于第n个4K(0<=n<1000),其虚拟地址(0xXXXXXXXX+n*4K),我们都有一个对应的硬件PTE,指出目前这4K驻留于物理内存的位置。通过由PDBR(CR3寄存器)与虚拟地址可定位这个硬件PTE(具体请参阅《小议Windows NT/2000分页机制》)。
现在让我们来考虑这样一种情况,我们有一个文件其大小也为4M,我们知道通常我们要使用这个文件都要将它读入内存。试想同时有两个或更多的进程需读写这个文件,这就需要解决内存共享的问题。实际上就算当前只有一个进程访问这个文件,对于这种潜在的需要共享的文件,Windows 2000/XP均会事先考虑共享情况。她通过一个称为Section的内核对象来实现这样的目的。仔细想想,这种情况下内存共享决不仅仅是内存资源的充分利用,就算我们可以为每个进程各分配4M空间,但是这将导致各个进程某种时刻可能得不到这个文件的最新内容。这是非常糟糕的情况。在内部Windows 2000/XP利用原型PTE来解决这样的情况。基于硬件PTE相同的原理,对于这样一个4M的文件,在映射这个文件时,Windows 2000/XP同样的将这个文件分成1000块,每块4K(PAGE_SIZE)大小。然后从页交换区分配1000个DWORD,每个DWORD值都是原型PTE,它们组成原型PTE表。对于这个文件的第n个4K(0<=n<1000),如果当前其驻留在物理内存中的话,其对应的PPTE的Valid位(bit 0,与硬件PTE一致)为1,然后这个PPTE的Page Frame Number(PPTE的高20位)用于指示物理内存。如果当前其仍然在磁盘中的话,Valid位为0。针对这种情况,通过PPTE的高20位(PFN Entry),查找Page Frame Datbase(由MmPfnDatabase定位),通过PFN Entry的Subsection PTE(windbg中称为restore pte,《Inside Windows 2000》中称为original pte,Windows XP内部称为Subsection PTE),定位Subsection,然后通过Subsection指向的Control Area的FILE_OBJECT,与PPTE在PPTE表的偏移n,通过公式:
PFN Entry Subsection PTE->Subsection->Control Area->FileObject + n * 4K
定位所要访问的文件偏移,这样Windows 2000/XP使用页面调入IO读入这页内容,更新PPTE表的这个PPTE。以上的这一系列定位转换算法,如Subsection PTE如何定位Subsection,我将另行介绍。上面的描述解决了一个非常重要的问题,我们不需要更新所有引用这一页面的进程的硬件PTE,因为此时所有进程的PTE均指向PPTE,我们只要更新PPTE就能达到目的。至于进程PTE如何指向PPTE,下面我会涉及到这个内容。这儿你只要有一个概念,进程的PTE为了指向PPTE,肯定是一个Invalid PTE,即bit 0为0,而且其bit 10为1(PPTE标志,具体请看我在《探寻Windows NT/2000 Copy On Write机制》列出的HARDWARE_PTE_X86结构)。
对于PPTE,因为X86处理器没有提供这样一种方式,像处理硬件PTE一样,由CPU直接进行地址转换。Windows 2000/XP内存管理器在处理Page Fault时,通过软件机制来模拟这种实现,这可以说是硬件PTE与PPTE的一个本质区别。
应该重点提出的是PPTE存在于页交换区(由MmPagedPoolStart与MmPagedPoolEnd指定的位置,从虚拟地址0xE1000000开始),其本身也有可能被Page Out,Windows 2000/XP通过MiCheckProtoPtePageState判断是否被Page Out,还有页交换区的起始地址0xE10000000将用于从无效PTE转化成原型PTE所在的地址,这等一下我会介绍到的。
照例我们用SoftICE来验证一下我们前面的描述:
:bpint e
只要我们截获这个硬件中断,我们就知道肯定发生了Page Fault,但是我们并不能确定这都是由于指向PPTE的无效PTE导致的。事实上Copy On Write等等其他机制,均会发生Page Fault(《探寻Windows NT/2000 Copy On Write机制》有详细讨论)。但是正如我们前面提及的PPTE的bit 10为1,我们还是很容易的判定一个Page Fault是不是由于指向PPTE的无效PTE导致的。由于发生Page Fault的虚拟地址由CR2寄存器指定,经过几次尝试以后,我们继续以下的讨论:
Break due to BPINT 0E (ET=2.23 Seconds)
:cpu
Processor 00 Registers
----------------------
CS:EIP=0008:801648A4 SS:ESP=0010:FCBEADC8
EAX=C002100B EBX=77E74A02 ECX=00000102 EDX=00000000
ESI=00085108 EDI=000493E0 EBP=0140FF74 EFL=00000006
DS=0023 ES=0023 FS=0038 GS=0000
CR0=8000003B PE MP TS ET NE PG
CR2=77D3BB26 //发生Page Fault的虚拟地址。
.
.
.
:page 77d3bb26
Linear Physical Attributes
77D3BB26 NP 01A714F6
从PTE值01A714F6的bit 10为1我们知道这是一个指向PPTE的无效PTE。通过query命令我们可以找到CR2指定的地址,位于模块rpcrt4.dll中。从下面可以看到:
:query 77d30000
Context Address Range Flags MMCI PTE Name
explorer 77D20000-77D8E000 07100001 FF8D1328 E169C580 rpcrt4.dll
结合我文章开始的介绍,通过以下的计算:
:? (77d3bb26-77d20000)/1000*4+e169c580
unsigned long = 0xE169C5EC, -513161748, "\xE1i\xC5\xEC"
我们可以得到其实PTE 01A714F6应该指向0xE169C5EC位置。这时候由MMCI指向的Control Area,根据我上面提到的计算公式,即可以读出rpcrt4.dll偏移(0xE169C5EC-0XE169C580)/4*1000处,即0x1B000处的4K字节,读入虚拟地址77D3B000中((0xE169C5EC-0XE169C580)/4*1000+77D20000),而CR2指定的地址77D3BB26肯定在这4K之中。
其实这样我们已经描述了MmAccessFault处理指向PPTE的无效PTE的一个典型过程。这里只是演示了原型PTE指向的页面未驻留在物理内存的情况,试想如果我们的页面已经在物理内存了,我们还有必要去费时的查找VAD吗?这就要涉及到无效的PTE如何定位原型PTE,所以我一直使用指向PPTE的无效PTE的叫法。《Inside Windows 2000》中指出指向PPTE的无效PTE的具体格式,但我发现其描述的不尽正确,我一直深信像作者那样能触及Windows 2000代码的人肯定不会有什么问题,所以我在理解PPTE时一直卡在此处。后来通过反汇编实现时发现实际上通过下面的方式来计算PPTE的位置:
(PTE>>2) & 0x3FFFFE00 + (PTE & 0x000000FF) << 1 + 0xE1000000
其中PTE为指向PPTE的无效PTE,0xE10000000是页交换区的起始地址。同样我们使用上面的例子来演示这个算法:
上面的无效PTE为01A714F6,有了这个值,我们可以得到:
PPTE Address = (0x01A714F6 >> 2) & 0x3FFFFE00 + (0x01A714F6 & 0x000000FF) << 1 + 0xE1000000
= 0x0069C53D & 0x3FFFFE00 + 0xF6 << 1 + 0xE1000000
= 0x69C400 + 0x1EC + 0xE1000000
= 0xE169C5EC
与我们通过VAD查找到的PPTE位置0xE169C5EC一致。
为了更好的理解PPTE,我们再来看一个例子。我们知道在Windows 2000/XP中ntdll.dll是个非常重要的dll,只要操作系统正常启动,ntdll肯定会被多个进程共享。我们用SoftICE作如下分析:
:query -x 77f50000
Context Address Range Flags MMCI PTE Name
smss 77F50000-77FF8000 07100005 80E6FA50 E131F9E8 ntdll.dll
.
.
.
explorer 77F50000-77FF8000 07100005 80E6FA50 E131F9E8 ntdll.dll
.
.
.
:addr smss
:mod ntdll
hMod Base PEHeader Module Name File Name
77F50000 77F500E8 ntdll \WINDOWS\system32\ntdll.dll
根据ntdll的基地址77F50000,我们查看其硬件PTE:
:dd 1df*1000+350*4+c0000000 l 4 //详细请参考《小议Windows NT/2000分页机制》
0010:C01DFD40 02267027 02F2E005 02F2F005 00C7E4FA 'p&.............
从smss进程的这些页表,我们很容易知道ntdll.dll第1至3个4K均驻留于物理内存地址中,因为它们都是有效的硬件PTE,而第四个PTE(00C7E4FA),虽然其是一个无效PTE(bit 0为0),但由于其是一个指向PPTE的PTE(bit 10为1),所以我们不能仅凭此PTE是个无效PTE,就断定ntdll.dll的第4个4K就不在物理内存中。我们要进一步的分析这个PTE,找出指向的PPTE判断这第4个4K是不是真的就是在磁盘中。OK,通过上面提及的算法,我们很容易的算出PPTE Address为E131F9F4,我们来看看这个PPTE的值:
:dd e131f9f4 l 4
0010:E131F9F4 02F30121 02F31121 02F32121 02F33121 !...!...!!..!1..
从值02F30121我们这时就可以判定这第4个4K也存在于物理地址中,位于Page Frame Number为02F30的物理内存中,剩下的就是查PFN Database了。
我们也可以来查看查看explorer进程的ntdll.dll映射情况,来验证一下这种情况:
:addr explorer
:dd 1df*1000+350*4+c0000000 l 4
0010:C01DFD40 02267025 02F2E025 02F2F025 02F30025 %p&.%...%...%...
这回清楚了吧。文章开头我提及:“我们不需要更新所有引用这一页面的进程的硬件PTE,因为此时所有进程的PTE均指向PPTE,我们只要更新PPTE就能达到目的了”。从中我们也可以看到ntdll.dll的第4个4K实际上位于物理内存中,但Windows 2000/XP并没有更新每个引用此页面的PTE,就正如smss进程一样。而PPTE却已经指向其实际地址了。当smss进程首次访问这个区域时,内存管理器才将02F30025(假设属性与explorer进程使用这页的属性一样且为考虑访问位标志)这个有效的硬件PTE更新上面的00C7E4FA,现在一切都明朗了吧。
本文虽然着重点在于介绍PPTE,但实际上我已将Section对象的内部机制说得非常清楚。这也是我原先将文章标题定为剖析Section之类的。关于PPTE,我的理解也经历了较多时间,主要是目前这部分资料实在是没有,仅有的《Inside Windows 2000》在没深入介绍的同时其指向PPTE的无效PTE格式未明确指出(特别是加上0xE1000000,这让我吃尽了苦头),本文介绍的这个格式我已经在Windows 2000及XP上测试过,实际上本文的两个例子一个是在Windows 2000 Server Build 2195,另一个在XP专业版Build 2600上演示的。
在这次介绍PPTE后,我们来回顾一下内存管理器内部的几个千丝万缕的联系:
FILEOBJECT的SECTION_OBJECT_POINTERS->DataSectionObject或SECTION_OBJECT_POINTERS->ImageSectionObject(决定于Section对象映射的文件的打开方式)指向Control Area,同时进程描述这文件映射的虚拟地址的VAD的MMCI成员(SoftICE叫法)也指向这个Control Area,Control Area底下存在一至多个SubSection,SubSection指向PPTE,PPTE table一般位于Control Area指向的Segment结构的底部。Section对象指向Segment;进程Page Table指向PPTE;这一切现在已描述的比较清楚了。还有一个主要的联系,即PFN Entry的Restore PTE(Original PTE)指向Subsection,这个关系我将在下次予以介绍。
从《小议Windows NT/2000分页机制》到今天这篇介绍PPTE,我对Windows 2000/XP的内存管理部分才有了比较深入的理解,至于未提及到的Working Set等概念也是非常重要的。经历过很多的模糊,对内存管理器也总算有了些许概念了。所有讨论均基于自己的理解,对错请多多指教(tsu00@263.net)