JIURL玩玩Win2k内存篇 内存共享(一) ProtoPTE
作者: JIURL
主页: http://jiurl.yeah.net
日期: 2003-7-30
内存共享
Windows 2000 中进程之间共享内存的几个主要应用是, 一个 dll(动态链接库)可能被多个进程使用,应该被共享。一个程序也应该可以被多个运行的实例共享。通过文件映射(Memory Mapped File)实现的进程之间通过内存通信,传输数据。
每个进程有自己的4G地址空间,地址空间通过进程自己的页目录和页表,以页为单位映射物理内存。把两个不同进程的一页地址空间映射到同一个物理页上,两个进程的一页地址空间就实现了共享,两个进程对该页地址空间的读写,都是对同一个物理页的读写。两个进程自己的某个 PTE(页表项,对应一页地址空间)中的物理页帧号相同,就使两个进程的某页映射到了同一个物理页。比如进程1的0x10000-0x10FFF这一页映射物理内存的第100页,进程2的0x20000-0x20FFF这一页也映射物理内存的第100页,那么进程1的0x10000-0x10FFF 的就是 进程2的0x20000-0x20FFF ,就是物理内存的第100页。
我们下面观察一下Win2k中dll的共享。ntdll.dll 通常会被所有的进程使用。 我们打开一个记事本程序,一个计算器程序,使用 SoftICE 来看看这两个进程负责映射 ntdll.dll 的 页表项(PTE)是否相同。相同的话,就说明共享了物理内存,不相同就说明没有共享物理内存。
运行计算器和记事本,使用SoftICE观察。
首先来看计算器
// 使用 addr 命令转换到 calc进程 也就是计算器进程的地址空间
:addr calc
// 使用 map32 -u 命令,看看用户地址空间中映射的模组
:map32 -u
Owner Obj Name Obj# Address Size Type
calc .text 0001 001B:01001000 000124EE CODE RO
calc .data 0002 0023:01014000 000010C0 IDATA RW
calc .rsrc 0003 0023:01016000 00002B98 IDATA RO
NVDESK32 .text 0001 001B:10001000 0000DB8F CODE RO
...
gdi32 .reloc 0004 0023:77F7A000 0000151C IDATA RO
ntdll .text 0001 001B:77F81000 00042492 CODE RO
ntdll ECODE 0002 001B:77FC4000 00004371 CODE RO
ntdll PAGE 0003 001B:77FC9000 00003983 CODE RO
ntdll .data 0004 0023:77FCD000 00002350 IDATA RW
ntdll .rsrc 0005 0023:77FD0000 00026D08 IDATA RO
ntdll .reloc 0006 0023:77FF7000 00001DA8 IDATA RO
...
MSVCRT .reloc 0005 0023:78043000 00002600 IDATA RO
// 看到了 ntdll 的各种节 映射到地址空间中的地址
// 代码放在 .text 节中, .text 节的开始地址为 77F81000
// 我们看 .text 节中的 77F82000-77F82FFF 这一页
// 这一页对应的 PTE 的虚拟地址为 (77F82000>>12)*4+0xC0000000= 0xc01dfe08
// 显示这部分页表项
:dd c01dfe04 l 100
0010:C01DFE04 00000000 04E63005 04E4B005 04E4C005 .....0..........
0010:C01DFE14 04E4D025 04E2E005 04E2F005 04E30005 %...............
0010:C01DFE24 04E11005 04E12005 04E1B025 04E1C025 ..... ..%...%...
0010:C01DFE34 04E1D005 04DFE005 04DFF025 04E80025 ........%...%...
0010:C01DFE44 04E61005 04E13005 04E14025 04E15005 .....0..%@...P..
0010:C01DFE54 04E16005 04DF7025 04DF8025 00000000 .`..%p..%.......
0010:C01DFE64 04E1A005 00000000 00000000 00000000 ................
0010:C01DFE74 00000000 00000000 04E43025 04E44005 ........%0...@..
0010:C01DFE84 00000000 04E46025 00000000 00000000 ....%`..........
...
// 0xc01dfe08 处的值为 04E63005 ,其中高20bit是物理页的页号,低12bit是标志
// 所以 ntdll .text 节中的 77F82000-77F82FFF 这一页 映射到了物理页 04E63
//(物理地址 04E63000-04E63FFF )
再来看记事本
// 转换到 notepad 的地址空间
:addr notepad
:map32 -u
Owner Obj Name Obj# Address Size Type
NOTEPAD .text 0001 001B:01001000 000065CA CODE RO
NOTEPAD .data 0002 0023:01008000 00001944 IDATA RW
NOTEPAD .rsrc 0003 0023:0100A000 00005238 IDATA RO
NVDESK32 .text 0001 001B:10001000 0000DB8F CODE RO
...
gdi32 .reloc 0004 0023:77F7A000 0000151C IDATA RO
ntdll .text 0001 001B:77F81000 00042492 CODE RO
ntdll ECODE 0002 001B:77FC4000 00004371 CODE RO
ntdll PAGE 0003 001B:77FC9000 00003983 CODE RO
ntdll .data 0004 0023:77FCD000 00002350 IDATA RW
ntdll .rsrc 0005 0023:77FD0000 00026D08 IDATA RO
ntdll .reloc 0006 0023:77FF7000 00001DA8 IDATA RO
...
MSVCRT .reloc 0005 0023:78043000 00002600 IDATA RO
// 看到了 ntdll 的各种节 .text 节的开始地址为 77F81000
// 同样看 .text 节中的 77F82000-77F82FFF 这一页
// 这一页对应的 PTE 的虚拟地址为 (77F82000>>12)*4+0xC0000000= 0xc01dfe08
// 显示这部分页表项
:dd c01dfe04 l 100
0010:C01DFE04 00000000 04E63005 04E4B025 04E4C005 .....0..%.......
0010:C01DFE14 04E4D005 04E2E005 04E2F005 04E30005 ................
0010:C01DFE24 04E11005 04E12025 04E1B005 04E1C005 ....% ..........
0010:C01DFE34 04E1D005 04DFE005 04DFF025 04E80025 ........%...%...
0010:C01DFE44 04E61025 04E13005 04E14005 04E15025 %....0...@..%P..
0010:C01DFE54 04E16005 04DF7005 04DF8025 00000000 .`...p..%.......
0010:C01DFE64 04E1A025 00F254C4 00000000 00000000 %....T..........
0010:C01DFE74 00000000 00000000 04E43005 04E44005 .........0...@..
0010:C01DFE84 00F254D2 00F254D4 00000000 00000000 .T...T..........
...
// 0xc01dfe08 处的值为 04E63005 ,其中高20bit是物理页的页号,低12bit是标志
// 所以 ntdll .text 节中的 77F82000-77F82FFF 这一页 映射到了物理页 04E63
//(物理地址 04E63000-04E63FFF )
可以看到,两个进程的地址空间 77F82000-77F82FFF 映射了相同的物理页。
Prototype PTE(原型PTE)
和共享的实现有着密切关系的一个数据结构是 页帧号数据库项 ( PFN DataBase Entry ) ,对处于Active(Valid) 状态的物理页,该物理页的 PFN DataBase Entry 的 +08 处,4个字节,是 share count,共享计数。
下面我们分析一下进程间共享物理页的几种情况。
进程1第一次将一些可以被共享的内容读入一个物理页。于是进程1相应的PTE有效,并指向这个物理页。被共享的物理页对应的 PFN DataBase Entry 状态为 Active(Valid) ,share count 值为1。
进程2需要共享这个物理页,于是进程2相应的PTE有效,并指向这个物理页。由于这个物理页现在被进程1和进程2共享,所以对应的 PFN DataBase Entry 的 share count 的值加1,变为2。
进程3需要共享这个物理页,于是进程3相应的PTE有效,并指向这个物理页。这个物理页对应的 PFN DataBase Entry 的 share count 的值加1,变为3。
进程2修整 Working Set (工作集),决定把该页从自己的 Working Set 中移出,于是相应的PTE无效。这个物理页对应的 PFN DataBase Entry 的 share count 的值减1,变为2。
进程1结束,这个物理页对应的 PFN DataBase Entry 的 share count 的值又减1,变为1。
进程2又需要访问这个共享页。于是PTE重新有效,并指向这个物理页。物理页的 share count 加1,又变为2。
进程2,进程3,修整 Working Set (工作集),都把这页从 Working Set 中移出,于是两个进程相应的PTE都无效。物理页的 share count 从 2 变成了 0。当变成0时,该物理页的状态从 Active(Valid) 变成了 Standby,链入了 Standby 链,不过该物理页中的内容不会被改变。
进程2又需要访问这个共享页。由于该物理页在 Standby 链上,内容没有被改变。于是直接取回该物理页,PTE重新有效并指向这个物理页。物理页状态从 Standby 变为 Active(Valid) , share count 变为1。
进程2又修整 Working Set (工作集),决定把该页从自己的 Working Set 中移出,于是相应的PTE无效。这个物理页的 share count 的值减1,变为0。当变成0时,该物理页的状态从 Active(Valid) 变成了 Standby,链入了 Standby 链,不过该物理页中的内容不会被改变。
系统需要内存,从 Standby 链上取走了该物理页。
进程2又需要访问这个共享页。没有物理页有原来的数据了,于是分配一个新的物理页,从文件中将数据读入。于是进程2相应的PTE有效,并指向这个新的物理页。这个物理页对应的 PFN DataBase Entry 状态为Active(Valid) ,share count 值为1。
需要强调的一点是,只有被共享的物理页的 share count 减为 0 时,才能被移入 Standby 链,然后被用来做其他事。如果 share count 不为0,就说明还有进程在使用这个物理页,如果这时把这个物理页移入 Standby 链,可能会有非常严重的后果。比如放着被共享的dll的程序代码的一个物理页,被多个进程共享,而其中一个结束,share count 减1但不为0,这时如果把这个物理页移入 Standby 链,系统又把这个物理页给清零,放入 Zeroed 链中待用。而这时其他几个进程的相应的PTE仍然是有效的,并且指向这个本来该是程序代码的物理页。当这几个进程执行这里的代码的时候,将会彻底出错。如果 share count 为0 ,所有进程相应的 PTE 都已经无效了。
共享机制还有一个问题。一个被共享的物理页 share count 为0 ,并且已经被用来做其他事,所有进程相应的 PTE 都已经无效了。当一个进程访问相应 PTE 对应的地址空间,系统会分配一个新的物理页,从文件中将数据读入新的物理页,相应的PTE有效,并指向这个新的物理页,这样这个进程就可以访问这个共享页。但是其他的进程无法知道这个新的物理页,他们相应的PTE仍然无效。他们如果也重新访问这个共享页,他们将如何得知新的物理页来填入PTE?Win2k 使用 Prototype PTE 来解决这个问题。
对于一个载入内存的应用程序,载入内存的动态链接库,或者一个文件映射,他们都有可能被共享。比如一个可能被共享的东西,映射到地址空间,需要100页,那么每页都有一个相应的 Prototype PTE (4个字节)。一个进程把这个100页的共享的东西映射到地址空间,需要用100个PTE。这100个PTE中,有效的PTE指向共享的物理页,无效的PTE将指向相应的 Prototype PTE ,Prototype PTE 指向物理页。这样一来,当遇到前面的问题,一个进程重新访问一个共享页,而这个共享页原来映射的物理页已经做其他事,系统分配新的物理页,把进程相应的PTE指向新的物理页,并把PTE变为有效之后,也把 Prototype PTE 指向新的物理页。其他的进程重新访问这个共享页时,他们的PTE是无效的,并且指向 Prototype PTE ,而 Prototype PTE 这时就已经指向了新的物理页,于是他们就可以把相应的PTE指向新的物理页,并把PTE变为有效。
载入内存的应用程序,载入内存的动态链接库,或者一个文件映射,他们都位于用户地址空间。为了描述方便我们把这些都叫做共享的东西。一个共享的东西都会有一个位于系统地址空间的 Segment 结构,这个共享的东西的 Prototype PTE 都顺序的放在这个 Segment 结构的 ProtoPtes数组中。比如一个共享的东西,映射到地址空间需要100页,那么它就有100个 Prototype PTE 放在它的 Segment 结构的 ProtoPtes数组中,并且它的第0页对应的 Prototype PTE 在数组的第0项,它的第1页对应的 Prototype PTE 在数组的第1项。Segment 结构0x38字节长,其中比较重要的是偏移+34处的 4个字节长的 ProtoPtes。ProtoPtes 中为 ProtoPtes数组 的首地址,不过通常情况下,ProtoPtes数组 就紧跟在 0x38字节长的 Segment 结构之后。Segment 结构偏移+8处4个字节长的 Total Ptes 指明了 ProtoPtes数组 中元素的个数。Segment 结构偏移+0处的4个字节长的 ControlArea,是一个指向 ControlArea 结构的指针,ControlArea 结构是一个很重要的结构,通过它我们可以找到相应的 File Object ,ControlArea 结构还有指回 Segment 结构 的指针。
有密切关系的几个结构是,映射了一个共享东西到一个进程地址空间,这块空间的 Vad
typedef struct _VAD_HEADER {
/*00*/ PVOID StartVPN;
/*04*/ PVOID EndVPN;
/*08*/ _VAD_HEADER* ParentLink;
/*0C*/ _VAD_HEADER* LeftLink;
/*10*/ _VAD_HEADER* RightLink;
/*14*/ ULONG CommitCharge : 20;
/*14*/ ULONG Flags : 12;
/*18*/ PVOID ControlArea;
/*1C*/ PVOID FirstProtoPte;
/*20*/ PVOID LastPTE;
/*24*/ ULONG Unknown;
/*28*/ LIST_ENTRY Secured;
/*30*/ } VAD_HEADER, *PVAD_HEADER;
Vad 结构偏移+18处4个字节的 ControlArea 是指向 ControlArea 结构的指针。
被共享物理页的 PfnDataBaseEntry
Zeroed 0x00
Free 0x01
Standby 0x02
Modified 0x03
Modified no-write 0x04
Bad 0x05
Active (Valid) 0x06
Transition 0x07
struct PfnDataBaseEntry (大小24个字节,即0x18个字节)
/*00*/ uint32 flink
/*04*/ uint32 pteaddress
/*08*/ uint32 blink / share count
/*0C*/ byte flags
/*0D*/ byte page state
/*0E*/ uint16 reference count
/*10*/ uint32 restore pte
/*14*/ uint32 containing page
处于 Active (Valid) 状态,被共享的物理页的 PfnDataBaseEntry 结构偏移+4处4个字节的 pteaddress ,
是 Prototype PTE 的地址。
实现机制
对于某个进程使用的某个共享页,PTE 有效时和正常一样,访问时 CPU 通过页目录,页表把虚拟地址转换成物理地址,访问内存。PTE 无效时,使用 Prototype PTE 保证共享机制的正确。PTE 无效时,系统设置 无效 PTE 的 Prototype 标志位。这样当一个进程访问该页的时候,因为PTE无效,所以会引起 Page-Fault 异常(Exception)。从而使 CPU 转去执行异常处理程序,异常处理程序检查PTE发现设置了 Prototype 标志,就会做相应的处理,重新使PTE有效,并指向正确的物理页。最后 CPU 重新执行引起异常的指令,这时该指令所访问的虚拟地址已经是正确的共享物理页了,并且该虚拟地址的PTE也有效了,于是就可以顺利执行。下面我们针对 x86 CPU 做更详细的说明。x86 CPU 的无效页表项定义如下
struct _HARDWARE_PTE_X86 (sizeof=4)
bits0-0 Valid
bits1-31 reserved
首先要说明的是,这个格式是由 CPU 定义的,CPU 将按照这个定义,对每一位做出解释,然后决定处理方式。
对于无效页来说,CPU只定义了bits0-0,用来判断是否有效。无效页的 bits0-0 值为0。其他都留给操作系统使用。Win2k 用 bits10-10 Prototype 来表示是否使用 Prototype,该位为0表示不使用,为1表示使用。
当某页的页表项无效,并设置了 Prototype ,当某条指令访问该页时,比如指令 MOV EAX , AddressInPrototypePage ,执行这条指令时,CPU 会自动通过页目录和页表把 虚拟地址AddressInPrototypePage 转换成物理地址,在地址转换过程中,CPU 在从页表项得到物理页地址的同时,会进行页保护检查,比如看该页表项是否有效,是否是只读等等。在这里我们指令中地址的页表项无效,于是就会引发异常。异常也是由 CPU 实现的。这里引起的是一个 Page Fault 异常,它的中断号是 0xe (十进制14),需要注意的是 Page Fault 的中断号是 0xe 这是由 CPU 定义的( x86 CPU 的 从 0 - 31 这32个中断是由 CPU 定义的,CPU 将根据这个定义做相应工作)。在发生异常时,CPU 自动把一些寄存器压入堆栈,然后根据中断号,(通过IDTR找到中断描述符表)在中断描述符表中找到相应的中断描述符,根据中断描述符中的地址,转到异常处理程序。中断描述符是由Win2k设置,异常处理程序也是由Win2k决定。对于 Win2k Build 2195 来说,中断 0xe 的处理程序是 ntoskrnl!KiTrap0E 地址在 804648a4 。当转到 KiTrap0E 时,CPU 已经在堆栈中压入了下面的内容
|-------------|
| EFLAGS |
|-------------|
| CS |
|-------------|
| EIP |
|-------------|
| Error Code |
|-------------|<---- [ ESP ]
page-fault 异常 (#PF) 的 Error Code 定义如下( CPU 定义 )
| 3 | 2 | 1 | 0 |
+---------------------------------------------------+
| Reserved |RSVD|U/S|R/W| P |
+---------------------------------------------------+
P 0 错误由无效页引起
1 错误由违反页保护引起
W/R 0 引起错误的内存访问是读
1 引起错误的内存访问是写
U/S 0 访问错误时处理器处在管理模式
1 访问错误时处理器处在用户模式
需要说明的是堆栈中压入的 EIP 就是引发异常的指令地址,将来将根据这个地址重新执行该指令。而寄存器 cr2 中是引发异常时访问的地址。
#PF异常处理程序 KiTrap0E(由Win2k提供)将会调用 ntoskrnl!MmAccessFault ,MmAccessFault 通过 CR2 中的访问地址,计算出相应的 PDE,PTE地址,检查 PTE 发现设置了 Prototype 标志(这个标志是由 Win2k 定义的,也是由它来处理),最终会调用 ntoskrnl!MiResolveProtoPteFault 来完成 Prototype 的相关工作。
把PTE指向正确的共享物理页,并把PTE重新设为有效。
执行完异常处理程序之后,CPU 重新执行引起异常的指令,这时指令可以正常执行了。至此 Prototype 已经被实现了。
对于 MmAccessFault 处理 Prototype 我们做更详细的说明。
通过分析汇编代码可以知道
MmAccessFault 根据引起异常的访问地址,计算出相应的 PDE,PTE地址,发现 PTE 设置了 Prototype 标志。检查 PTE ,如果PTE 的高 20 bit 不为 0xfffff ,将根据公式 PrototypePteAddress=0xe1000000+((Pte>>2)&0x3ffffe00)+(Pte&0xff)*2; 计算出 PrototypePteAddress 。
注意这个公式中,((Pte>>2)&0x3ffffe00)的值的范围 和 (Pte&0xff)*2的值的范围。由于是无效PTE,最低位本身就是0,(Pte&0xff)*2的值最低2位都会为0,这也保证了 PrototypePteAddress 以4字节为边界。如果 PTE 的高 20 bit 为 0xfffff,将调用 MiCheckVirtualAddress()。MiCheckVirtualAddress()中调用MiLocateAddress()。MiLocateAddress()将返回InValidAddress所在范围的 Vad 的地址。然后用下面的方法计算 PrototypePteAddress=( (InValidAddress>>0xa)&0x3ffffc -TheVad.StartVPN<<0x2 ) + TheVad.FirstProtoPte 也就是计算 InValidAddress 的虚拟页号,用 InValidAddress 的虚拟页号减去 Vad 中该范围的其实虚拟页号,然后找到 ProtoPtes数组 中的相应项的地址,就找到了 PrototypePteAddress 。
然后 MmAccessFault 用 InValidAddress,InValidAddress_PteAddress,InValidAddress_PrototypePteAddress 等7个参数,调用 MiResolveProtoPteFault()。 MiResolveProtoPteFault()将根据 PrototypePte 的各种标志,做不同的处理。对于 PrototypePte 最低位为1,PrototypePte 有效。表明 PrototypePte 中的物理页号,就是正确的被共享的物理页的页号。调用MiCompleteProtoPteFault(),将 PrototypePte 中的物理页号设置到 Pte 中,把 Pte 变为有效就可以了。对于 PrototypePte 最低位为0,PrototypePte 无效。就会检查 PrototypePte 中,看被共享的物理页是不是被放入了 Standby 链,如果是,通过 PrototypePte 中的物理页号,把该物理页移回来,把 PrototypePte 变为有效,把 Pte 指向这个物理页,把 Pte 也变为有效。如果原来被共享的物理页,已经彻底的不在了,就申请新的物理页,从文件中重新读入被共享的内容,把 PrototypePte 指向这个新的物理页,并且把 PrototypePte 变为有效,把 Pte 指向这个物理页,把 Pte 也变为有效。
下面我们使用 SoftICE 来观察几种不同的情况
因为所有的 ProtoPteFault 都会调用 MiResolveProtoPteFault() , 所以我们在这个函数中下断点。对于建立好堆栈帧之后,MiResolveProtoPteFault()的三个重要的参数分别位于 EBP+0C InValidAddress , EBP+10 InValidAddress_PteAddress , EBP+14 InValidAddress_PrototypePteAddress 。
使用 kd 反汇编 MiResolveProtoPteFault (以下具体地址只针对 Win2k Build 2195)
u MiResolveProtoPteFault
ntoskrnl!MiResolveProtoPteFault:
8043f6ab 55 push ebp
8043f6ac 8bec mov ebp,esp
8043f6ae 51 push ecx
8043f6af 51 push ecx
...
可以看到在 8043f6ae 处,堆栈帧就建完了,我么就在这个地址上下断点。
:bl
00) * BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l 10;"
...
并且断到之后,显示 ebp+c 的内容,也就是三个重要参数的值。
显示 ebp->10 的内容,InValidAddress_PteAddress 地址处的内容,InValidAddress_Pte。
显示 epb->14 的内容,InValidAddress_PrototypePteAddress 地址处的内容,InValidAddress_PrototypePte。我们根据显示的 Pte 和 ProtoPte 的值,ProtoPteFault 是很容易弄出来的,而且也很多,我们可以从中选择合适 Pte 和 ProtoPte 来进行分析。
1 进程的 PTE 和 ProtoPtes数组
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10;" (ET=388.52 microseconds)
0010:EBA24CC4 77D3C3A7 C01DF4F0 E17C19F0 EBA24CFC ...w......|..L..
// InValidAddress= 77D3C3A7
// PteAddress= C01DF4F0
// ProtoPteAddress= E17C19F0
0010:C01DF4F0 01F064F8 01F064FA 01F064FC 01F064FE .d...d...d...d..
// Pte
0010:E17C19F0 076A7121 07648121 075E9121 075CA121 !qj.!.d.!.^.!.\.
// ProtoPte
:query 77d3c3a7
Context Address Range Flags MMCI PTE Name
Explorer 77D20000-77D8E000 07100001 810F3908 E17C1980 rpcrt4.dll
// 使用 query 命令,查找 InValidAddress 所在范围的Vad。
// 其中 MMCI 就是 ControlArea,PTE 是 FirstProtoPte
可以看到 C01DF4F0 处的4个PTE 都无效, bits10-10 Prototype 标志位都为1,并且高20bit不为0xfffff。
所以使用 PrototypePteAddress=0xe1000000+((Pte>>2)&0x3ffffe00)+(Pte&0xff)*2 可以算出他们对应的 ProtoPte 的地址。计算得到 E17C19F0 ,E17C19F4,E17C19F8,E17C19FC。从中我们可以看出他们是顺序对应的。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10;" (ET=4.80 milliseconds)
0010:EB73FCC4 75B05CA1 C01D6C14 E356E214 EB73FCFC .\.u.l....V...s.
0010:C01D6C14 095B8C0A 04A8C025 04A2A025 04AEC025 ..[.%...%...%... // Pte
0010:E356E214 04A6E121 04A8C121 04A2A121 04AEC121 !...!...!...!... // ProtoPte
:query 75b05ca1
Context Address Range Flags MMCI PTE Name
NOTEPAD 75A90000-75CD2000 0710000F 810802C8 E356E040 mshtml.dll
对于有效的Pte,和有效的 ProtoPte,他们都指向了相同的物理页。
(有效的Pte,ProtoPte,高20位为物理页帧号)
2 PTE无效,高20bit不等于0xfffff。ProtoPte 有效。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=314.65 microseconds)
0010:EBA00CC4 77F90333 C01DFE40 E13C9560 EBA00CFC 3..w@...`.<.....
// InValidAddress=77F90333
0010:C01DFE40 00F254B0 04E61025 04E13025 04E14025 .T..%...%0..%@..
// Pte=00F254B0
0010:E13C9560 04E80121 04E61121 04E13121 04E14121 !...!...!1..!A..
// ProtoPte=04E80121 物理页帧号为 04e80
根据ProtoPte中的物理页帧号,在 PFN DataBase 中找该物理页的 PfnDataBaseEntry
对于 Win2k Build 2195 来说,PfnDatabase 的首地址在 81456000。
:dd 81456000+4e80*18 l 18
0010:814CBC00 00000011 E13C9560 0000000A 00010608 ....`.<.........
0010:814CBC10 93E6A478 00001D79 0000003C E13C95DC x...y...<.....<.
可以看到 PfnDataBaseEntry 的 /*04*/ pteaddress 值为 E13C9560 ,正是 ProtoPte 的地址。
共享计数/*08*/ share count为 0000000A,说明正在有10个进程使用这个共享物理页。/*0D*/ byte page state 为 06 ,说明物理页处于 Active (Valid) 状态。
:query 77f90333
Context Address Range Flags MMCI PTE Name
Explorer 77F80000-77FF8000 07100003 813E6A08 E13C9520 ntdll.dll
... // 我们从断点处开始继续执行
MiCompleteProtoPteFault() // 看到执行了 MiCompleteProtoPteFault()
... // 直到 MiResolveProtoPteFault() 返回处。
:dd c01dfe40 l 10
0010:C01DFE40 04E80025 04E61025 04E13025 04E14025 %...%...%0..%@..
// 可以看到Pte已经指向了ProtoPte中指向的物理页,而且Pte有效了。
:dd e13c9560 l 10
0010:E13C9560 04E80121 04E61121 04E13121 04E14121 !...!...!1..!A..
再来看看这个物理页的 PfnDataBaseEntry
:dd 81456000+4e80*18 l 18
0010:814CBC00 00000011 E13C9560 0000000B 00010608 ....`.<.........
0010:814CBC10 93E6A478 00001D79 0000003C E13C95DC x...y...<.....<.
// 共享计数加1了,从 0000000A 变成了 0000000B
这种情况就是一个进程把共享页移出了 Working Set,于是相应的Pte无效,
但是这个被共享的物理页还在被其他进程使用,所以这个物理页不会被移入 Standby 链,ProtoPte有效,并仍指向而这个物理页。这个进程再次访问这个共享页,只需要把Pte重新指向这个物理页就可以了。MiCompleteProtoPteFault() 的作用就是把ProtoPte中的物理页设置到 Pte 中,并设定 Pte 的低12位标志。
3 PTE无效,高20bit不等于0xfffff。ProtoPte 无效。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=22.20 milliseconds)
0010:EB6A7CC4 75951A3F C01D6544 E17BC2C4 EB6A7CFC ?..uDe....{..|j.
// InValidAddress= 75951A3F
0010:C01D6544 01EF0C62 00000000 00000000 00000000 b...............
// Pte= 01EF0C62
0010:E17BC2C4 07889860 0784A860 000000A0 90F4C430 `...`.......0...
// ProtoPte= 07889860
观察物理页7889的PfnDataBaseEntry
:dd 81456000+7889*18 l 18
0010:8150ACD8 00000696 E17BC2C4 000060C7 00000208 ......{..`......
0010:8150ACE8 90F4C460 00004E5C 0000790B E2EF088C `...\N...y......
/*04*/ pteaddress 值为 E17BC2C4 ,正是 ProtoPte 的地址。
/*0D*/ page state 值为 02 ,说明这个物理页状态为 Standby 。
这种情况就是所有共享这个物理页的进程的相应的Pte都已经无效了。于是物理页被移入了 Standby 链,不过
物理页中的内容仍然保持不变。ProtoPte 仍然指向这个物理页,不过 ProtoPte 无效。
:query 75951a3f
Context Address Range Flags MMCI PTE Name
NOTEPAD 75950000-75955000 07100001 810F4BA8 E17BC2C0 lz32.dll
从断点继续执行
...
MiResolveTransitionFault()
MiCompleteProtoPteFault()
...
直到 MiResolveProtoPteFault() 返回处
:dd c01d6544 l 10
0010:C01D6544 07889025 00000000 00000000 00000000 %...............
// Pte变有效了,并且就是前面已经放在 Standby 链上的物理页7889。
:dd e17bc2c4 l 10
0010:E17BC2C4 07889121 0784A860 000000A0 90F4C430 !...`.......0...
// ProtoPte 变有效了。指向物理页7889。
观察物理页7889的PfnDataBaseEntry
:dd 81456000+7889*18 l 18
0010:8150ACD8 0000024A E17BC2C4 00000001 00010608 J.....{.........
0010:8150ACE8 90F4C460 00004E5C 0000790B E2EF088C `...\N...y......
状态从02变为了06,即从Standby 变为了 Active (Valid)。
共享计数也变为了1。
这种情况就是,所有共享该物理页的进程都把该共享页从自己的 Working Set 中移出,于是物理页被移入了Standby 链,当一个进程重新访问的时候,只需要根据 ProtoPte 中的信息,把该物理页从 Standby 链移回就可以了。
4 PTE无效,高20bit等于0xfffff。ProtoPte 有效。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=2.38 seconds)
0010:EBAD5D44 003C0612 C0000F00 E2F5D0C0 EBAD5D7C ..<.........|]..
0010:C0000F00 FFFFF460 04483025 04440025 04423005 `...%0H.%.D..0B.
0010:E2F5D0C0 043BF163 04483163 04440163 04423163 c.;.c1H.c.D.c1B.
:dd 81456000+43bf*18 l 18
0010:814BB9E8 00002B23 E2F5D0C0 00000002 00010609 #+..............
0010:814BB9F8 000000C0 0000782A 000000D0 C0004F38 ....*x......8O..
:query 3c0612
Context Address Range Flags MMCI PTE Name
NOTEPAD 003A0000-0069F000 03400000 810C3D68 E2F5D040 Heap (mapped)
...
MiCompleteProtoPteFault
...
:dd c0000f00 l 10
0010:C0000F00 043BF025 04483025 04440025 04423005 %.;.%0H.%.D..0B.
:dd e2f5d0c0 l 10
0010:E2F5D0C0 043BF163 04483163 04440163 04423163 c.;.c1H.c.D.c1B.
:dd 81456000+43bf*18 l 18
0010:814BB9E8 00002B23 E2F5D0C0 00000003 00010609 #+..............
0010:814BB9F8 000000C0 0000782A 000000D0 C0004F38 ....*x......8O..
和 PTE无效高20bit不等于0xfffff,ProtoPte 有效 的情况相同。
5 PTE无效,高20bit等于0xfffff。ProtoPte 无效。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=16.16 milliseconds)
0010:EF0B55D4 09C2ACE8 C00270A8 E35C7AA8 EF0B560C .....p...z\..V..
0010:C00270A8 FFFFF420 00000000 00000000 00000000 ...............
0010:E35C7AA8 04D4E8C2 04D4F8C2 04D508C0 04D518C0 ................
:dd 81456000+4d4e*18 l 18
0010:814C9F50 00004D4F E35C7AA8 000068A1 00000208 OM...z\..h......
0010:814C9F60 C79314D8 000048D9 00004D53 E35C7AAC .....H..SM...z\.
:query 9c2ace8
Context Address Range Flags MMCI PTE Name
NOTEPAD 09C20000-09C33000 01000000 84793128 E35C7A80 shdocvw.dll
...
MiResolveTransitionFault
MiCompleteProtoPteFault
...
:dd c00270a8 l 10
0010:C00270A8 04D4E025 00000000 00000000 00000000 %...............
:dd e35c7aa8 l 10
0010:E35C7AA8 04D4E123 04D4F8C2 04D508C0 04D518C0 #...............
:dd 81456000+4d4e*18 l 18
0010:814C9F50 000004B3 E35C7AA8 00000001 00010608 .....z\.........
0010:814C9F60 C79314D8 000048D9 00004D53 E35C7AAC .....H..SM...z\.
和 PTE无效高20bit不等于0xfffff,ProtoPte 无效 的情况相同。
PTE无效,高20bit等于0xfffff。ProtoPte 无效 的另外一种情况。
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=297.11 microseconds)
0010:EB9315D4 07E00200 C001F800 E134A040 EB93160C [email protected].....
// InValidAddress= 07E00200
0010:C001F800 FFFFF480 FFFFF480 FFFFF480 FFFFF480 ................
// Pte= FFFFF480
0010:E134A040 90B20CD8 00000000 000E01CE 00000003 ................
// ProtoPte= 90B20CD8
我的物理内存是128M,所以是不会有90B20这么大的物理页帧号的
看看 81456000+90b20*18 地址处的内容
:dd 81456000+90b20*18 l 18
0010:821E6B00 006E006F 00740069 0072006F 8B550000 o.n.i.t.o.r...U.
0010:821E6B10 08EC81EC 56000002 5610758B FF0C75FF .......V.u.V.u..
明显不是PfnDataBaseEntry
:query 7e00200
Context Address Range Flags MMCI PTE Name
NOTEPAD 07E00000-07E7F000 04000000 810B20A8 E134A040 ~DFD8D0.tmp
从断点继续
...
MiResolveMappedFileFault()
...
直到 MiResolveProtoPteFault() 返回处
:dd c001f800 l 10
0010:C001F800 FFFFF480 FFFFF480 FFFFF480 FFFFF480 ................
:dd e134a040 l 10
0010:E134A040 067A18C0 00000000 000E01CE 00000003 ..z.............
:dd 81456000+67a1*18 l 18
0010:814F1718 813E7348 E134A040 00000000 0001000A Hs>[email protected].........
0010:814F1728 90B20CD8 00001BB2 0000242B E2BB014C ........+$..L...
Pte仍然无效,ProtoPte已经指向了一个真正的物理页67a1,
物理页67a1的PfnDataBaseEntry 状态为00,即Zeroed,/*04*/ pteaddress 为 E134A040。
从 MiResolveProtoPteFault() 返回到了 MiDispatchFault() 中,继续执行。
...
IoPageRead()
MiCompleteProtoPteFault()
...
:dd c001f800 l 10
0010:C001F800 067A1067 FFFFF480 FFFFF480 FFFFF480 g.z.............
:dd e134a040 l 10
0010:E134A040 067A1163 00000000 000E01CE 00000003 c.z.............
:dd 81456000+67a1*18 l 18
0010:814F1718 0000078E E134A040 00000001 00010609 [email protected].........
0010:814F1728 90B20CD8 00001BB2 0000242B E2BB014C ........+$..L...
又看到了 MiCompleteProtoPteFault() ,把ProtoPte的物理页设置到Pte,并把Pte变有效。
物理页67a1的共享计数也变为了1。
6 PTE无效,高20bit等于0xfffff。ProtoPte 无效,并且高20位为0
Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l
10" (ET=1.78 milliseconds)
0010:EB6675D4 07CA0000 C001F280 E354CDE0 EB66760C ..........T..vf.
0010:C001F280 FFFFF480 00000000 00000000 00000000 ................
0010:E354CDE0 00000080 00000000 00100000 00000000 ................
// ProtoPte= 00000080
不过明显 ProtoPte 的高20位并不是共享物理页的页帧号
:dd 81456000+0*18 l 18
0010:81456000 00000000 C0200000 00000001 00010600 ...... .........
0010:81456010 00000000 00000032 00000000 C0200004 ....2......... .
从物理页0的 PfnDataBaseEntry 也看到了 /*04*/ pteaddress 为 C0200000 ,而不是 E354CDE0。
:query 7ca0000
Context Address Range Flags MMCI PTE Name
NOTEPAD 07CA0000-07CA0000 04000000 810B7CC8 E354CDE0
...
MiResolveDemandZeroFault
MiCompleteProtoPteFault
...
:dd c001f280 l 10
0010:C001F280 068CA067 00000000 00000000 00000000 g...............
:dd e354cde0 l 10
0010:E354CDE0 068CA163 00000000 00100000 00000000 c...............
:dd 81456000+68ca*18 l 18
0010:814F32F0 000005ED E354CDE0 00000001 00010609 ......T.........
0010:814F3300 00000080 00004465 000068EC E2BAE934 ....eD...h..4...
最后Pte和ProtoPte都变为有效,指向物理页068CA。
7 PTE无效,高20bit不等于0xfffff。ProtoPte 无效,并且高20位为0
为了能比较容易找到这种情况,使用了在下断点时,使用了if,判断条件是否合适
Break due to BPX #0008:8043F6AE IF (((EBP->0x14)->0)<0x1000) (ET=510.83
milliseconds)
:dd ebp+c l 10
0010:F057FCC4 01009938 C0004024 E3084564 F057FCFC [email protected].
:dd ebp->10 l 10
0010:C0004024 082114B2 00000000 00000000 00000000 ..!.............
:dd ebp->14 l 10
0010:E3084564 000000A0 0381A820 0381B820 0381C820 .... ... ... ...
// ProtoPte 中的高20bit并不是物理页帧号
// 物理页0的PfnDataBaseEntry 也说明了这一点。
:dd 81456000+0*18 l 18
0010:81456000 00000000 C0200000 00000001 00010600 ...... .........
0010:81456010 00000000 00000032 00000000 C0200004 ....2......... .
:query 1009938
Context Address Range Flags MMCI PTE Name
NOTEPAD 01000000-0100F000 07100002 836F8408 E3084540 NOTEPAD.EXE
...
执行几句之后
:dd ebp+c l 10
0010:F057FCC4 01009938 C0004024 00000001 F057FCFC [email protected].
:dd c0004024 l 10
0010:C0004024 00000080 00000000 00000000 00000000 ................
:dd e3084564 l 10
0010:E3084564 000000A0 0381A820 0381B820 0381C820 .... ... ... ...
...
MiResolveDemandZeroFault
:dd ebp+c l 10
0010:F057FCC4 01009938 C0004024 00000001 F057FCFC [email protected].
:dd c0004024 l 10
0010:C0004024 06934067 00000000 00000000 00000000 g@..............
:dd e3084564 l 10
0010:E3084564 000381A820 0381B820 0381C820 .... ... ... ...
:dd 81456000+6934*18 l 18
0010:814F3CE0 000000EF C0004024 00000001 00010601 ....$@..........
0010:814F3CF0 00000080 000059F5 00005B36 FFFFFFFF .....Y..6[......
最后Pte有效。
对 Prototype PTE 的分析就到这里。
欢迎交流,欢迎交朋友,
欢迎访问 http://jiurl.yeah.net http://jiurl.cosoft.org.cn/forum
本文地址:http://com.8s8s.com/it/it1549.htm