小议Windows NT/2000分页机制
WebCrazy(tsu00@263.net)
内存管理是操作系统最重要的一部分,其决定了操作系统的性能。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中,依次类推,计算公式即为C0000000H+页目录偏移值(线性地址的高10位)*1000H,在下面我将利用此公式。当然以上描述的前提是每个页表均位于物理内存中(由页目录中每项中的P位指定),这也是为什么IA-32使用两级页表的原因,否则每个进程除其代码与数据等外还额外需要4M(4*1024*1024)的存储器。
上面的机制即实现了物理地址寻址,也就实现了在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]&0x00000001))
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)|(pAddress&0x00000FFF));
}
}
上面两个程序段涉及到2G-4G范围(线性地址C0000000以上)的内存访问,普通用户态的程序无法实现。在Windows中请使用设备驱动程序以使其在核心态正确执行。
那么Windows又是如何利用分页机制以高效、合理的利用好有限的物理内存呢?Jeffrey Richter的经典著作<<Programming Applications for Microsoft Windows,Fourth Edition>>全面阐述了Windows的内存管理机制,相关原理请具体参阅此书!下面我列出同一程序(mspaint.exe)的两个运行实例的线性地址与物理地址的对应关系,以说明Windows的内存分页机制。
// 下面是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-C0303FFF所映射的线性地址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中分别是Service ID为64h、2bh、18h与29h的System Service。关于System Service可参阅<<再谈Windows 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],8046A180
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!NtCreateSection(Windows在装入可执行文件与CreateFileMapping等都最终调用此函数)可发现Copy 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