现在的位置: 首页 > 综合 > 正文

探究Windows 2000/XP原型PTE(http://webcrazy.yeah.net)

2013年08月18日 ⁄ 综合 ⁄ 共 6876字 ⁄ 字号 评论关闭
探究Windows 2000/XP原型PTE
              WebCrazy(http://webcrazy.yeah.net)

    内存管理可以说是操作系统实现中最重要的环节,也是最为复杂的一环节。对于相对贫乏的内存资源,内存共享也成了一个很重要的有效手段。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)。

抱歉!评论已关闭.