📄 jiurl玩玩win2k内存篇 内存共享(一) protopte.htm
字号:
这时就已经指向了新的物理页,于是他们就可以把相应的PTE指向新的物理页,并把PTE变为有效。<BR><BR>
载入内存的应用程序,载入内存的动态链接库,或者一个文件映射,他们都位于用户地址空间。为了描述方便我们把这些都叫做共享的东西。一个共享的东西都会有一个位于系统地址空间的
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 结构
的指针。<BR><BR>有密切关系的几个结构是,映射了一个共享东西到一个进程地址空间,这块空间的 Vad <BR><BR>typedef
struct _VAD_HEADER {<BR>/*00*/ PVOID StartVPN;<BR>/*04*/ PVOID
EndVPN;<BR>/*08*/ _VAD_HEADER* ParentLink;<BR>/*0C*/ _VAD_HEADER*
LeftLink;<BR>/*10*/ _VAD_HEADER* RightLink;<BR>/*14*/ ULONG CommitCharge :
20;<BR>/*14*/ ULONG Flags : 12;<BR>/*18*/ PVOID ControlArea;<BR>/*1C*/
PVOID FirstProtoPte;<BR>/*20*/ PVOID LastPTE;<BR>/*24*/ ULONG
Unknown;<BR>/*28*/ LIST_ENTRY Secured;<BR>/*30*/ } VAD_HEADER,
*PVAD_HEADER;<BR><BR>Vad 结构偏移+18处4个字节的 ControlArea 是指向 ControlArea
结构的指针。<BR><BR>被共享物理页的 PfnDataBaseEntry</FONT>
<P><FONT face=宋体>Zeroed 0x00<BR>Free 0x01<BR>Standby 0x02<BR>Modified
0x03<BR>Modified no-write 0x04<BR>Bad 0x05<BR>Active (Valid)
0x06<BR>Transition 0x07<BR><BR>struct PfnDataBaseEntry
(大小24个字节,即0x18个字节)<BR>/*00*/ uint32 flink<BR>/*04*/ uint32
pteaddress<BR>/*08*/ uint32 blink / share count<BR>/*0C*/ byte
flags<BR>/*0D*/ byte page state<BR>/*0E*/ uint16 reference count<BR>/*10*/
uint32 restore pte<BR>/*14*/ uint32 containing page<BR><BR>处于 Active
(Valid) 状态,被共享的物理页的 PfnDataBaseEntry 结构偏移+4处4个字节的 pteaddress ,<BR>是
Prototype PTE 的地址。</FONT>
<P><FONT face=宋体><B>实现机制</B></FONT>
<P><FONT face=宋体>对于某个进程使用的某个共享页,PTE 有效时和正常一样,访问时 CPU
通过页目录,页表把虚拟地址转换成物理地址,访问内存。PTE 无效时,使用 Prototype PTE 保证共享机制的正确。PTE 无效时,系统设置
无效 PTE 的 Prototype 标志位。这样当一个进程访问该页的时候,因为PTE无效,所以会引起 Page-Fault
异常(Exception)。从而使 CPU 转去执行异常处理程序,异常处理程序检查PTE发现设置了 Prototype
标志,就会做相应的处理,重新使PTE有效,并指向正确的物理页。最后 CPU
重新执行引起异常的指令,这时该指令所访问的虚拟地址已经是正确的共享物理页了,并且该虚拟地址的PTE也有效了,于是就可以顺利执行。下面我们针对 x86
CPU 做更详细的说明。x86 CPU 的无效页表项定义如下<BR><BR>struct _HARDWARE_PTE_X86
(sizeof=4)<BR>bits0-0 Valid<BR>bits1-31 reserved<BR><BR>首先要说明的是,这个格式是由 CPU
定义的,CPU 将按照这个定义,对每一位做出解释,然后决定处理方式。<BR>对于无效页来说,CPU只定义了bits0-0,用来判断是否有效。无效页的
bits0-0 值为0。其他都留给操作系统使用。Win2k 用 bits10-10 Prototype 来表示是否使用
Prototype,该位为0表示不使用,为1表示使用。<BR><BR>当某页的页表项无效,并设置了 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
已经在堆栈中压入了下面的内容<BR><BR>|-------------| <BR>|
EFLAGS |<BR>|-------------|<BR>|
CS
|<BR>|-------------|<BR>| EIP
|<BR>|-------------|<BR>| Error Code |<BR>|-------------|<---- [
ESP ]<BR><BR><BR>page-fault 异常 (#PF) 的 Error Code 定义如下( CPU 定义
)<BR> |
3 | 2 | 1 | 0
|<BR>+---------------------------------------------------+ <BR>|
Reserved |RSVD|U/S|R/W|
P
|<BR>+---------------------------------------------------+ <BR><BR>P
0 错误由无效页引起<BR> 1 错误由违反页保护引起<BR>W/R 0
引起错误的内存访问是读<BR> 1 引起错误的内存访问是写<BR>U/S 0
访问错误时处理器处在管理模式<BR> 1 访问错误时处理器处在用户模式<BR><BR>需要说明的是堆栈中压入的
EIP 就是引发异常的指令地址,将来将根据这个地址重新执行该指令。而寄存器 cr2 中是引发异常时访问的地址。<BR><BR>#PF异常处理程序
KiTrap0E(由Win2k提供)将会调用 ntoskrnl!MmAccessFault ,MmAccessFault 通过 CR2
中的访问地址,计算出相应的 PDE,PTE地址,检查 PTE 发现设置了 Prototype 标志(这个标志是由 Win2k
定义的,也是由它来处理),最终会调用 ntoskrnl!MiResolveProtoPteFault 来完成 Prototype
的相关工作。<BR>把PTE指向正确的共享物理页,并把PTE重新设为有效。<BR><BR>执行完异常处理程序之后,CPU
重新执行引起异常的指令,这时指令可以正常执行了。至此 Prototype 已经被实现了。<BR><BR>对于 MmAccessFault 处理
Prototype 我们做更详细的说明。<BR><BR>通过分析汇编代码可以知道</FONT>
<P><FONT face=宋体> MmAccessFault 根据引起异常的访问地址,计算出相应的
PDE,PTE地址,发现 PTE 设置了 Prototype 标志。检查 PTE ,如果PTE 的高 20 bit 不为 0xfffff
,将根据公式
PrototypePteAddress=0xe1000000+((Pte>>2)&0x3ffffe00)+(Pte&0xff)*2;
计算出 PrototypePteAddress 。<BR>
注意这个公式中,((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
。</FONT>
<P><FONT face=宋体> 然后 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 也变为有效。</FONT>
<P><FONT face=宋体>下面我们使用 SoftICE 来观察几种不同的情况</FONT>
<P><FONT face=宋体>因为所有的 ProtoPteFault 都会调用 MiResolveProtoPteFault() ,
所以我们在这个函数中下断点。对于建立好堆栈帧之后,MiResolveProtoPteFault()的三个重要的参数分别位于 EBP+0C
InValidAddress , EBP+10 InValidAddress_PteAddress , EBP+14
InValidAddress_PrototypePteAddress 。<BR><BR>使用 kd 反汇编
MiResolveProtoPteFault (以下具体地址只针对 Win2k Build 2195)<BR><BR>u
MiResolveProtoPteFault<BR>ntoskrnl!MiResolveProtoPteFault:<BR>8043f6ab 55
push ebp<BR>8043f6ac 8bec mov ebp,esp<BR>8043f6ae 51 push ecx<BR>8043f6af
51 push ecx<BR>...<BR><BR>可以看到在 8043f6ae
处,堆栈帧就建完了,我么就在这个地址上下断点。<BR><BR>:bl<BR>00) * BPX #0008:8043F6AE DO "dd
ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l 10;"<BR>...<BR><BR>并且断到之后,显示
ebp+c 的内容,也就是三个重要参数的值。<BR>显示 ebp->10 的内容,InValidAddress_PteAddress
地址处的内容,InValidAddress_Pte。<BR>显示 epb->14
的内容,InValidAddress_PrototypePteAddress
地址处的内容,InValidAddress_PrototypePte。我们根据显示的 Pte 和 ProtoPte 的值,ProtoPteFault
是很容易弄出来的,而且也很多,我们可以从中选择合适 Pte 和 ProtoPte 来进行分析。<BR><BR>1 进程的 PTE 和
ProtoPtes数组<BR><BR>Break due to BPX #0008:8043F6AE DO "dd ebp+c l 10;dd
ebp->10 l 10;dd ebp->14 l<BR>10;" (ET=388.52
microseconds)<BR>0010:EBA24CC4 77D3C3A7 C01DF4F0 E17C19F0 EBA24CFC
...w......|..L..<BR>// InValidAddress= 77D3C3A7<BR>// PteAddress=
C01DF4F0<BR>// ProtoPteAddress= E17C19F0<BR>0010:C01DF4F0 01F064F8
01F064FA 01F064FC 01F064FE .d...d...d...d..<BR>// Pte<BR>0010:E17C19F0
076A7121 07648121 075E9121 075CA121 !qj.!.d.!.^.!.\.<BR>//
ProtoPte<BR>:query 77d3c3a7<BR>Context Address Range Flags MMCI PTE
Name<BR>Explorer 77D20000-77D8E000 07100001 810F3908 E17C1980
rpcrt4.dll<BR>// 使用 query 命令,查找 InValidAddress 所在范围的Vad。<BR>// 其中 MMCI 就是
ControlArea,PTE 是 FirstProtoPte<BR><BR>可以看到 C01DF4F0 处的4个PTE 都无效,
bits10-10 Prototype 标志位都为1,并且高20bit不为0xfffff。<BR>所以使用
PrototypePteAddress=0xe1000000+((Pte>>2)&0x3ffffe00)+(Pte&0xff)*2
可以算出他们对应的 ProtoPte 的地址。计算得到 E17C19F0
,E17C19F4,E17C19F8,E17C19FC。从中我们可以看出他们是顺序对应的。<BR><BR>Break due to BPX
#0008:8043F6AE DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14
l<BR>10;" (ET=4.80 milliseconds)<BR>0010:EB73FCC4 75B05CA1 C01D6C14
E356E214 EB73FCFC .\.u.l....V...s.<BR>0010:C01D6C14 095B8C0A 04A8C025
04A2A025 04AEC025 ..[.%...%...%... // Pte<BR>0010:E356E214 04A6E121
04A8C121 04A2A121 04AEC121 !...!...!...!... // ProtoPte<BR>:query
75b05ca1<BR>Context Address Range Flags MMCI PTE Name<BR>NOTEPAD
75A90000-75CD2000 0710000F 810802C8 E356E040
mshtml.dll<BR><BR>对于有效的Pte,和有效的
ProtoPte,他们都指向了相同的物理页。<BR>(有效的Pte,ProtoPte,高20位为物理页帧号)<BR><BR><BR>2
PTE无效,高20bit不等于0xfffff。ProtoPte 有效。<BR><BR>Break due to BPX #0008:8043F6AE
DO "dd ebp+c l 10;dd ebp->10 l 10;dd ebp->14 l<BR>10" (ET=314.65
microseconds)<BR>0010:EBA00CC4 77F90333 C01DFE40 E13C9560 EBA00CFC
3..w@...`.<.....<BR>// InValidAddress=77F90333<BR>0010:C01DFE40
00F254B0 04E61025 04E13025 04E14025 .T..%...%0..%@..<BR>//
Pte=00F254B0<BR>0010:E13C9560 04E80121 04E61121 04E13121 04E14121
!...!...!1..!A..<BR>// ProtoPte=04E80121 物理页帧号为
04e80<BR><BR>根据ProtoPte中的物理页帧号,在 PFN DataBase 中找该物理页的
PfnDataBaseEntry<BR>对于 Win2k Build 2195 来说,PfnDatabase 的首地址在
81456000。<BR><BR>:dd 81456000+4e80*18 l 18<BR>0010:814CBC00 00000011
E13C9560 0000000A 00010608 ....`.<.........<BR>0010:814CBC10 93E6A478
00001D79 0000003C E13C95DC x...y...<.....<.<BR><BR>可以看到
PfnDataBaseEntry 的 /*04*/ pteaddress 值为 E13C9560 ,正是 ProtoPte
的地址。<BR>共享计数/*08*/ share count为 0000000A,说明正在有10个进程使用这个共享物理页。/*0D*/ byte
page state 为 06 ,说明物理页处于 Active (Valid) 状态。<BR><BR>:query
77f90333<BR>Context Address Range Flags MMCI PTE Name<BR>Explorer
77F80000-77FF8000 07100003 813E6A08 E13C9520 ntdll.dll<BR><BR>... //
我们从断点处开始继续执行<BR>MiCompleteProtoPteFault() // 看到执行了
MiCompleteProtoPteFault()<BR>... // 直到 MiResolveProtoPteFault()
返回处。<BR><BR>:dd c01dfe40 l 10<BR>0010:C01DFE40 04E80025 04E61025 04E13025
04E14025 %...%...%0..%@..<BR>//
可以看到Pte已经指向了ProtoPte中指向的物理页,而且Pte有效了。<BR>:dd e13c9560 l
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -