小议Windows NT/2000分页机制

WebCrazy()

    ※ 来源:http://www.lisoleg.net/lisoleg/memory/



    内存管理是操作系统最重要的一部分,其决定了操作系统的性能。Intel X86采用分
段、分页的内存机制,Windows NT/2000充分利用了此项先进的机制。段内IA-32体系使
用页目录(Page Directory)与页表(Page Table,SoftICE中Page命令可以显示出页目录与
页表等)形成对4G地址的寻址能力。文中未特别说明,均讨论运行平台为Intel 32位处理
器的Windows NT/2000,所提及的Windows也仅指Windows NT/2000。Windows中不管用户
态与核心态的代码段、数据段与堆栈段基址均为0,文中提到的逻辑地址(由段基址与偏
移量两部分组成)与线性地址值是相等的。由于对于用户来说线性地址是其可见的,若我
未特别指出物理地址,所说的地址也仅指线性地址。
    页目录(PDE)由1024项组成,每项均指向一页表(PTE),每一页表也由1024个页组成
,IA-32体系每页大小为4K,所以可寻址范围为4G(1024*1024*4K)。Windows中每个进程
都拥有其各自的进程地址空间,即拥有其各自的页目录与页表。每个进程均使用线性地
址C0300000H指向其特定的页目录所在的地址,而页目录中每项(即页表)均依次排列在线
性地址C0000000H处,每个页表均占用4K(1024*4)字节,如第一个页表位于C0000000H处
,而第二个页表位于C0000000+1000H(4K),即C0001000中,依次类推,计算公式即为C0
000000H+页目录偏移值(线性地址的高10位)*1000H,在下面我将利用此公式。当然以上
描述的前提是每个页表均位于物理内存中(由页目录中每项中的P位指定),这也是为什么
IA-32使用两级页表的原因,否则每个进程除其代码与数据等外还额外需要4M(4*1024*1
024)的存储器。
    上面的机制即实现了物理地址寻址,也就实现了在Windows NT/2000中物理地址与线
性地址的相互转换(虽然CPU在对内存操作时只需要线性地址转换成物理地址,但我们在
分析程序代码等仍需要物理地址转换成线性地址)。照例先看看SoftICE的分析吧:
    // addr explorer命令后以下操作将只在进程explorer的私有进程空间进行
    :addr explorer
    // 显示explorer进程页目录所在物理地址,即进程切换至explorer后PDBR(CR3)中
的值
    :addr
    CR3     LDT Base:Limit KPEB Addr PID  Name
            .
            .
            .
    00C10000               FF9FC920  036C explorer
            .
            .
            .
    /*
        线性地址的格式:0-11位对应1页(4096字节)的偏移量
        12-21位对当前页表中1024项中寻址
        22-31位对页目录进行寻址
        高20位(12-31位)又称为桢
        根据上面提及的公式,即可以得到物理地址高20位的值,再加上线性地址页表
偏移(作为物理
        地址的低12位),即实现了线性地址转化为物理地址,用公式表示为:
         @(C0000000h+PDE*1000h+PTE*4)&0fffff000h+PO
        =@(C0000000h+4*(PDE*400h+PTE))&0fffff000h+PO
        =@(C0000000h+(PDE>>10d+PTE)<<2d)&0fffff000h+PO
        =@(C0000000h+(LA>>12d)<<2d)&0fffff000h+PO
        =@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO
            上式中用PDE与PTE分别代表Page Directory与Page Table的偏移值,用LA
代表给定的线性
        地址,用PO代表LA的低12位,用h与d分别代表16/10进制,@后表示取后地址指
针中的内容。
        如此分析后,线性地址C0300000所对应的页目录为300h,页表为300h,偏移量
为0
        则 C0000000h+PD*1000h+PT*4+PO=C0000000+300h*1000h+300h*4+0
    */
    :dd c0000000+300*1000+300*4 l 4
    0010:C0300C00 00C10063 01A31063 00000000 0141F163 c...c.......c.A.
                     |
                     |_低12位(0-11)063为属性位、Intel保留位与系统(OS)使用位
    // 显示C0300000的物理地址(00C10000)
    :? dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
    00C10000
    //用SoftICE验证
    :phys dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
    C0300000
    :page C0300000
    Linear Physical Attributes
    C0300000 00C10000 P D A S RW
    其实上面最后一条命令就可以实现所有其它指令的功能,下面我列出实现的代码段:

    // 线性地址->物理地址
    // SoftICE中Page命令可以实现此功能
    // 一个线性地址对应唯一的物理地址
    // 此函数若返回0说明此线性地址未对应物理地址
    ULONG LinearAddressToPhysicalAddress(ULONG lAddress)
    {
        unsigned int *pAddr;
        unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
        unsigned int *PageTableEntry=(unsigned int *)0xC0000000;
        //判断页目录是否有效,第0位(P)为存在位,请参阅相关书籍
        if((!(PageDirectoryEntry[lAddress>>22]&0xFFFFF000))
            &&(!(PageDirectoryEntry[lAddress>>22]&0x00000001)))
            return 0;
        //@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO 见上叙述
        pAddr=(int *)((int)PageTableEntry+((lAddress&0xFFFFF000)>>10));
        if((*pAddr)&1)
            return ((*pAddr) &0xFFFFF000) |(lAddress&0x00000FFF);
        return 0;
    }
    那么又如何逆向实现物理地址转换成线性地址呢?虽然其间没有任何关系,但因为知
道页目录与页表的具体位置,可使用直接在这个范围中搜索的方法。理论上这个范围最
大为1024*1024*4(4M),但由于很多页目录项当前均不存在于物理内存中,所以实际上搜
索范围小得多。这也导致一个问题,有可能导致蓝屏(内核态访问不存在的地址)。所以
以下给出的代码我检查了页目录中每一项的P位。
    // 物理地址->线性地址
    // 相当于SoftICE中Phys命令
    // 搜索所有有效的页表寻找指定物理地址
    // 有可能多个线性地址同时指向同一个物理地址
    // 此函数若未输出任何结果表明当前还没有线性地址映射至此物理地址中
    void PhysicalAddressToLinearAddress(ULONG pAddress)
    {
        unsigned int *pAddr;
        unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
        unsigned int *PageTableEntry=(unsigned int *)0xC0000000;
        int i,j;
        DbgPrint("\n");
        for(i=0;i<1024;i++)
            if((PageDirectoryEntry[i]&0xFFFFF000)&&(PageDirectoryEntry[i]&0x
00000001))
                for(j=0;j<1024;j++){
                    pAddr=(int *)((int)PageTableEntry+i*4096+j*4);
                    if((*pAddr)&0x00000001)
                        if(((*pAddr)&0xFFFFF000)==(pAddress&0xFFFFF000))
                            DbgPrint("%08X\n",
                                ((i*4*1024*1024+j*4*1024)&0xFFFFF000)|(pAddr
ess&0x00000FFF));
                }
    }
    上面两个程序段涉及到2G-4G范围(线性地址C0000000以上)的内存访问,普通用户态
的程序无法实现。在Windows中请使用设备驱动程序以使其在核心态正确执行。
    那么Windows又是如何利用分页机制以高效、合理的利用好有限的物理内存呢?Jeff
rey Richter的经典著作< Edition>>全面阐述了Windows的内存管理机制,相关原理请具体参阅此书!下面我列出
同一程序(mspaint.exe)的两个运行实例的线性地址与物理地址的对应关系,以说明Win
dows的内存分页机制。
    // 下面是mspaint.exe装入内存后各段的地址(两个同时运行的mspaint.exe进程的
映射地址一致)
    :map32 mspaint
    Owner Obj Name Obj# Address Size Type
    mspaint .text 0001 001B:01001000 0003A500 CODE RO
    mspaint .data 0002 0023:0103C000 00002670 IDATA RW
    mspaint .rsrc 0003 0023:0103F000 000116C8 IDATA RO
                       -------------
                            |
                            |_逻辑地址
    // mspaint.exe第一个运行实例的线性地址与物理地址的对应关系
    Linear Address Range Physical Address Range Attributes
    -------------------- ---------------------- ----------
    00010000 - 00010FFF  03A8B000 - 03A8BFFF    047
    00020000 - 00020FFF  03BCC000 - 03BCCFFF    047
    0006D000 - 0006DFFF  018BC000 - 018BCFFF    047
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第一个实例的.text段
    01001000 - 01001FFF  00596000 - 00596FFF    005
    01002000 - 01002FFF  03F97000 - 03F97FFF    005
    01003000 - 01003FFF  03D58000 - 03D58FFF    005
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第一个实例的.data段
    0103C000 - 0103CFFF  0225F000 - 0225FFFF    047
    0103D000 - 0103DFFF  03620000 - 03620FFF    047
    0103E000 - 0103EFFF  03C1E000 - 03C1EFFF    047
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第一个实例的.rsrc段
    0103F000 - 0103FFFF  01652000 - 01652FFF    025
    01040000 - 01040FFF  02653000 - 02653FFF    005
    01041000 - 01041FFF  003D4000 - 003D4FFF    005
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第一个实例的页目录的表
    C0300000 - C0300FFF  030FD000 - 030FDFFF    063
    C0301000 - C0301FFF  017FE000 - 017FEFFF    063
    C0303000 - C0303FFF  0141F000 - 0141FFFF    163
             .                   .              .
             .                   .              .
             .                   .              .
    FFD0F000 - FFD0FFFF  000FF000 - 000FFFFF    023
    FFDF0000 - FFDF0FFF  0026A000 - 0026AFFF    163
    FFDFF000 - FFDFFFFF  00269000 - 00269FFF    163
    // mspaint.exe第二个运行实例的线性地址与物理地址的对应关系
    Linear Address Range Physical Address Range Attributes
    -------------------- ---------------------- ----------
    00010000 - 00010FFF  03A6A000 - 03A6AFFF    047
    00020000 - 00020FFF  0352B000 - 0352BFFF    067
    0006D000 - 0006DFFF  03413000 - 03413FFF    047
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第二个实例的.text段
    01001000 - 01001FFF  00596000 - 00596FFF    005
    01002000 - 01002FFF  03F97000 - 03F97FFF    005
    01003000 - 01003FFF  03D58000 - 03D58FFF    005
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第二个实例的.data段
    0103C000 - 0103CFFF  030DF000 - 030DFFFF    047
    0103D000 - 0103DFFF  009A0000 - 009A0FFF    047
    0103E000 - 0103EFFF  02089000 - 02089FFF    047
             .                   .              .
             .                   .              .
             .                   .              .
    //mspaint.exe第二个实例的.rsrc段
    0103F000 - 0103FFFF  01652000 - 01652FFF    005
    01040000 - 01040FFF  02653000 - 02653FFF    005
    01041000 - 01041FFF  003D4000 - 003D4FFF    005
             .                   .               .
             .                   .               .
             .                   .               .
    //mspaint.exe第二个实例的页目录的表
    C0300000 - C0300FFF  037C9000 - 037C9FFF    063
    C0301000 - C0301FFF  02F8A000 - 02F8AFFF    063
    C0303000 - C0303FFF  0141F000 - 0141FFFF    163
             .                   .              .
             .                   .              .
             .                   .              .
    FFD0F000 - FFD0FFFF  000FF000 - 000FFFFF    023
    FFDF0000 - FFDF0FFF  0026A000 - 0026AFFF    163
    FFDFF000 - FFDFFFFF  00269000 - 00269FFF    163
    上面列表直接从mspaint.exe的两个同时运行的实例的页目录与页表中取得。实际上
你只要理解物理地址与线性地址的相互转换,稍微修改一下上面所给的两个代码段便能
得到上面的信息(32位x86平台Windows 2000 Server Build 2195的某一时刻所取,我的机
器物理内存为64M,即040000000H字节)。
    Windows中每个进程均有其私有的线性地址,在线性地址的前2G(用户空间,Windows
 2000 Server的ntoskrnl.exe的MmHighestUserAddress指出用户空间最大值7FFEFFFFH)
,上例中列出的两个实例同时运行的mspaint.exe的.text段与.rsrc段均指向同一物理地
址空间,而.data段指向不同的物理空间。这是由于不同段的作用与性质决定,不难理解
。在后2G(内核空间,Windows 2000 Server的ntoskrnl.exe的MmSystemRangeStart指出其
线性地址的开始位置),大部分物理空间均由两个实例共享,实际上不同程序所有运行的
例程均共享这2G,当然页目录(C0300000h)与页表(C0000000h)等其它比较特殊的操作除
外,上例中基本可以看出这个规则。当然页目录与页表也有项目是指向同一物理区域,这
样才能实现进程间共享物理内存,如上面mspaint.exe的两个实例的页目录中C0303000-C
0303FFF所映射的线性地址00800000-00BFFFFF(4M)指向同一物理区域。
    这只是讨论通常情况下Windows如何在程序间高效的使用内存,实际上Windows赋予
页很多机制,如Copy On Write等,使.text段等需要时也可以指向不同的物理地址,典
型情况是使用用户态的调试器(Microsoft Visual C++所附的IDE调试环境等)对应用程序
进行调试。当然Windows也提供让数据共享同一物理内存区域的方法,即使用Microsoft
连接器(link)的section开关,赋予特定的段共享(S)属性。
    在<<浅析Windows NT/2000环境切换>>(Nsfocus Magazine 12)中我曾详细的介绍了
Windows NT/2000环境切换后页目录基址(CR3)切换代码,那么Windows NT/2000如何为应
用程序从头分配页目录与页表呢?因为只有在建立新进程时才牵涉到页目录与页表的分配
,所以还是让我们看看Kernel32.dll中的CreateProcessW代码吧(CreateProcessA间接调
用CreateProcessW).
    可以简单的将流程用代码如下显示:
    KERNEL32!CreateProcessW
    .
    .(一些错误例程如进程文件是否存在,内核对象安全性检查等过程)
    .
    ;打开文件
    001B:77E7DDD2 CALL [ntdll!NtOpenFile]
            .
            .(主要是一些参数的压栈代码)
            .
    ;为可执行文件分配虚拟地址
    001B:77E7DE0A CALL [ntdll!NtCreateSection]
            .
            .
            .
    ;关闭文件
    001B:77E7DE1E CALL [ntdll!NtClose]
            .
            .
            .
    ;调用NtCreateProcess创建进程
    001B:77E7DF83 CALL [ntdll!NtCreateProcess]
            .
            .
            .
    其实上面ntdll.dll的四个过程在Windows 2000 Server Build 2195中分别是Servi
ce ID为64h、2bh、18h与29h的System Service。关于System Service可参阅<<再谈Win
dows NT/2000内部数据结构>>(Nsfocus Magazine 11)。
    我们继续看看NtCreateProcess处理流程:
    :u ntdll!NtCreateProcess //用户态,就是常说的Native API
    ntdll!NtCreateProcess
    001B:77F92D2C MOV EAX,00000029
    001B:77F92D31 LEA EDX,[ESP+04]
    001B:77F92D35 INT 2E //使用中断门进入核心态
    001B:77F92D37 RET 0020
    :ntcall
    Service table address: 804704D8 Number of services:000000F8
            .
            .
            .
    0029 0008:804AD948 params=08 ntoskrnl!SeUnlockSubjectContext+0514
                |
                |_ID为29h的System Service(NtCreateProcess)的入口地址
            .
            .
            .
    :u 8:804ad948
    0008:804AD948 55             PUSH EBP
    0008:804AD949 8BEC           MOV EBP,ESP
    0008:804AD94B 6AFF           PUSH FF
    0008:804AD94D 6890354080     PUSH 80403590
    0008:804AD952 682CCC4580     PUSH ntoskrnl!_except_handler3
    0008:804AD957 64A100000000   MOV EAX,FS:[00000000]
    0008:804AD95D 50             PUSH EAX
    0008:804AD95E 64892500000000 MOV FS:[00000000],ESP
                    .
                    .
                    .
    ;EBP-30中此时存放新建进程的KPEB,以下几句实现对KPEB后的0A2H*4(648)字节清零

    0008:804ADAF5 B9A2000000     MOV ECX,000000A2
    0008:804ADAFA 33C0           XOR EAX,EAX
    0008:804ADAFC 8B7DD0         MOV EDI,[EBP-30]
    0008:804ADAFF F3AB           REPZ STOSD
                    .
                    .
                    .
    0008:804AD5E7 55             PUSH EBP
    0008:804AD5E8 8BEC           MOV EBP,ESP
    ;KPEB与进程Context分别作为这个过程的第一与第四个参数传入(ebp+8与ebp+14h)

    0008:804AD5EA 8B4508         MOV EAX,[EBP+08]
    0008:804AD5ED 8D4808         LEA ECX,[EAX+08]
    0008:804AD5F0 C60003         MOV BYTE PTR [EAX],03
    0008:804AD5F3 89480C         MOV [EAX+0C],ECX
    0008:804AD5F6 C640021B       MOV BYTE PTR [EAX+02],1B
    0008:804AD5FA 8909           MOV [ECX],ECX
    0008:804AD5FC 8A4D0C         MOV CL,[EBP+0C]
    0008:804AD5FF 884862         MOV [EAX+62],CL
    0008:804AD602 8B4D10         MOV ECX,[EBP+10]
    0008:804AD605 89485C         MOV [EAX+5C],ECX
    0008:804AD608 8A4D18         MOV CL,[EBP+18]
    0008:804AD60B 884864         MOV [EAX+64],CL
    0008:804AD60E 8B4D14         MOV ECX,[EBP+14]
    0008:804AD611 8B11           MOV EDX,[ECX]
    ;EDX存放进程Context(即页目录的物理地址)
    ;至于进程Context的算法,由于不仅与ntoskrnl.exe中的几个变量有关,还与执行环
境息息相关,感兴趣的自己用SoftICE跟跟
    ;将进程Context放入新建的KPEB中
    0008:804AD613 895018         MOV [EAX+18],EDX ;18H是进程Context相对KPEB的
偏移
                    .
                    .
                    .
    ;以下实现将新建的进程的KPEB插入系统KPEB的双向链表
    0008:804ADD22 A184A14680     MOV EAX,[8046A184]
    /*
        8046A180用以下两条SoftICE命令输出结果就可以很容易理解
        :? (@8046a180)-a0
        FE4E1D60 4266532192 (-28435104) "﨨`"
        :? @PsInitialSystemProcess //显示System进程的KPEB
        FE4E1D60 4266532192 (-28435104) "﨨`"
        即实现将新KPEB链插入已有链表尾
        在插入KPEB后,系统就可以根据上面提供的页目录对进程进行调度(即新进程拥
有新的私有的进程空间)
    */
    0008:804ADD27 8B4DD0                 MOV ECX,[EBP-30]
    0008:804ADD2A C781A000000080A14680   MOV DWORD PTR [ECX+000000A0],8046A1
80
    0008:804ADD34 8B4DD0                 MOV ECX,[EBP-30]
    0008:804ADD37 8981A4000000           MOV [ECX+000000A4],EAX
    0008:804ADD3D 8B4DD0                 MOV ECX,[EBP-30]
    0008:804ADD40 81C1A0000000           ADD ECX,000000A0 ;是不是可以找出链状
结构相对KPEB的偏移呢?
    0008:804ADD46 8908                   MOV [EAX],ECX
    0008:804ADD48 8B45D0                 MOV EAX,[EBP-30]
    0008:804ADD4B 05A0000000             ADD EAX,000000A0
    0008:804ADD50 A384A14680             MOV [8046A184],EAX
    为了更直观,上面的代码我只是按照系统执行流程列出(相对于实际的磁盘存放顺序
).其实系统在创建进程初,首先用ObCreateObject建立section内核对象(section对象并
不分配物理内存,Windows 2000 DDK Documentation中有详细介绍),然后才有牵涉到页目
录与页表,自己在分析代码时要特别注意,还有可以使用IDA对ntoskrnl.exe进程分析,毕
竟IDA对代码流程提供的比较清晰。至于以上所提的进程Context,系统KPEB双向链表等请
参阅<<浅析Windows NT/2000环境切换>>,在那我已经进行了比较详细的说明。
    在深入分析Windows的有关内存操作的API(如VirtualAllocEx,CreateFileMapping,
HeapAlloc等等)后,还可以发现很多其它方面重要的信息。如跟踪ntoskrnl!NtCreateSe
ction(Windows在装入可执行文件与CreateFileMapping等都最终调用此函数)可发现Cop
y On Write等机制是如何实现的等等.这些都留着你自己去找找了。Windows 2000支持多
个页面文件(命名为Pagefile.sys),这牵涉到原型PTE(Sofice中用ProtoPTE表示),分页文
件使用的PTE等等很多的机制,要分析之,还要对Windows 2000的分层驱动程序中的FSD有
很好的理解.简单的说即文中只讨论到PTE中的P位为1的情况。好了,还是那句话,以上分
析,如有错误,还望指点(tsu00@263.net)!

参考资料:
    1.Jeffrey Richter
        << Programming Applications for Microsoft Windows,Fourth Edition >>
    2.Intel Corp<< Intel Architecture Software Developer's Manual,Volume 3 >>
    3.<< Undocumented Windows NT >>附带源码
    4.Windows 2000 DDK Documentation

与我联系: , 欢迎到我的留言簿