吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5575|回复: 14
收起左侧

[系统底层] 《Windows 7 x64内存管理》之用户范围内存管理 Part2

[复制链接]
BeneficialWeb 发表于 2022-4-9 21:30

用户范围内存管理 Part2

1 延迟TLB失效的影响

1.1 为什么需要延迟失效以及会带来哪些问题

TLB无效化对性能有负面影响,因为它必须发生在所有处理器上。这意味着VMM必须向所有处理器发送一个处理器内部中断,指示它们对过期的项进行失效。VMM代码必须依赖某种形式的处理器内部同步机制,以确保所有处理器都完成无效化。这个过程会使其他一切都变慢,所以在可能的情况下,等到有一定数量的过期项时再将其全部失效是有意义的。

然而,留下陈旧的TLB项意味着一个或多个处理器可能仍然在使用过时的PTE内容来映射虚拟地址。考虑这样的情况:VMM从工作集中删除一个页面,并把它放在Standby链表中。这个页面的TLB项必须作为这个过程的一部分被无效,因为,否则,一个处理器可能仍然在访问这个页面,即使从Standby链表中,这个页面可以被获取并重新用于另一个工作集。

另外,如果一个进程释放了一些虚拟内存,VMM应该立即使相应的项无效,因为不能保证进程代码不会因为其逻辑中的错误而试图访问释放的范围。假设VMM在这种情况下没有使TLB失效,并为另一个进程重新使用释放的物理页。第一个进程可能通过一个陈旧的项写进第二个进程的页面。

一般来说,用户模式地址的TLB项很可能立即失效,因为系统无法控制用户模式代码的行为,所以这是确保每个虚拟用户模式范围真正与其他范围隔离的唯一方法。

1.2 对于系统临时映射的延迟失效

显然,VMM延迟了它用于内部使用的内核模式虚拟地址的无效化,例如MiMapPagelnHyperSpaceWorkerMiUnmapPagelnHyperSpaceWorker使用的PTEs

这些函数在0xFFFFF880'000000-0xFFFFF900'000000范围内映射和取消映射页面,这实际上不是Hyperspace,而是系统PTE范围。

0: kd> dt nt!_KPRCB -y HyperPte @$prcb
   +0x49e0 HyperPte : 0xfffff880`00800033 Void

MiMapPagelnHyperSpaceWorker使用一个工作PTE来映射一个物理页面,这意味着,如果我们为两个不同的页面调用它两次,它两次都使用同一个PTE。我们不能同时调用它来映射不同的物理页;相反,我们可以在其工作VA上用它来映射最多一个页面。

此外,MiMapPagelnHyperSpaceWorker不会使其工作VATLB项失效,因此,如果我们调用它两次来映射两个不同的页面,在第二次调用后对工作VA的访问仍可能被映射到第一个物理页面,因为TLB项还是旧的。

这里是MiUnmapPagelnHyperSpaceWorker发挥作用的地方:当被调用时,它将VAPTE设置为0,然后增加一个计数,MiMapPagelnHyperSpaceWorker使用它来确定映射的工作VA。在下一次调用映射函数时,它将不会使用最后使用的VA,因为TLB项已经过期,但会使用下一个虚拟页的地址。总之,在调用了一次映射函数之后,我们必须在再次调用映射函数之前调用取消映射函数。但是请注意,使用过的VAs仍然是陈旧的TLB项,因为unmapping函数在每次调用时不会使TLB失效。

MiUnmapPagelnHyperSpaceWorker不会盲目地继续增加计数器并无休止地留下陈旧的TLB项。当64个虚拟页被使用后,它就会使所有的TLB项失效,并重置计数器,这样下一次映射就会重新使用该范围内的第一个工作VA

如果一个调用映射函数的线程在调用取消映射函数之前被抢占了,另一个线程可能会调用映射函数并把事情搞乱。首先,新的映射可能不会工作,因为TLB项已经过时了,而且,当被抢占的线程最终被恢复时,它将不再有它正在工作的物理页面的映射。

由于这个原因,映射函数在内部将IRQL提高到DISPATCH,并返回时将其留在这个值上。这确保了线程在调用取消映射函数之前不能被抢占。之前的IRQL被保存在一个缓冲区中,调用映射函数的人必须提供这个缓冲区,然后被传递给取消映射函数,之后取消映射函数恢复原来的IRQL

然而,在多处理器系统中,这还不够:一个处理器可能在映射和取消映射之间,例如,忙于将映射的页面清零,而另一个处理器调用MiMapPagelnHyperSpaceWorker,改变虚拟到物理的映射,导致灾难。经典的解决方案是使用自旋锁来保护映射,但这将产生一个显著的瓶颈,所以VMM采用了一个更巧妙的技巧。

工作VA和计数器被存储在_KPRCBHyperPte成员中,每个处理器都有一个_KPRCB实例。此外,每个处理器使用不同的VA范围,所以一个处理器可以在其当前的工作VA上自由映射而不干扰其他处理器。

这个技巧与以下事实相得益彰:当映射生效时,线程不能被抢占:使用一个处理器的当前工作VA的线程永远不会在另一个处理器上执行,因为为了发生这种情况,线程本身应该被抢占并被另一个处理器恢复。这意味着,一个处理器的64个工作虚拟页将只被该处理器执行的代码所引用,并且只有在该特定处理器的TLB中才有它们的TLB项。因此,当反映射函数使TLB失效时,它只对正在执行该函数的处理器这样做,而不会向其他处理器发送昂贵的处理器间中断。

_KPRCB.HyperPte的格式如下:第63:12位包含范围内第一页的VPN,第11:00位包含计数器。映射函数将物理页映射到由以下公式给出的地址上:

(HyperPte & 0xFFFFFFFFFFFFF000) + (HyperPte & 0xFFF) * 4096

注意第11:0位的计数器是如何以页面大小的增加VA的。

1.3 最后的思考

上一节解释的逻辑是基于以下原则:VMM可以延迟失效,因为它记录了哪些VA有过时的TLB项并避免使用它们。然而,这种方法有一个弱点:可以想象一个使用野指针的错误内核模式组件可以使用这些VA,并通过一个陈旧的TLB项写入物理内存页,从而造成不可预测的影响。这个使用野指针的内核模式组件可以造成巨大的破坏力,使用陈旧的TLB项,它可以破坏内核变量;它可以破坏任何地址空间的用户模式范围的内容;它可以通过引用内存映射的设备地址而导致错误;等等。

由于担心软件漏洞已经成为一种风尚,所以值得指出的是,陈旧的TLB项对恶意软件来说并不十分有用。这些项映射了系统地址,只有在内核模式下运行的代码才能访问它们。如果我们要担心恶意代码会访问它们,那么我们就隐含地考虑到恶意的内核模式代码已经以某种方式被执行了,也就是rootkit。这种情况本身就是一场灾难,因为内核模式的代码可以做任何它想做的事情。这样的代码不需要依赖像陈旧的TLB项这样的异想天开的东西,这些项可能被保留在处理器中,也可能不被保留。内核模式代码可以简单地访问它想要的任何地址,也可以附加到任何进程的地址空间来访问特定的用户模式空间。总之,如果内核模式的恶意软件在系统上被执行,我们就会有严重的麻烦,而陈旧的TLB项已经是我们最不再需要考虑的问题了。

还有一个值得一提的。可以想象,通过避免重复使用这些项所引用的物理页,就有可能留下陈旧的TLB项而不产生不良影响。这样一来,使用无效指针的错误代码最多只能访问一个别人不使用的物理页。然而,这似乎不是VMM内部发生的事情。MiMapPagelnHyperSpaceWorkerMiUnmapPagelnHyperspaceWorker被调用的一种情况是在MilnitializePfn中,当一个即将被映射到地址空间的物理页需要从缓存(内存缓存,而不是TLB)中刷新时。由于缓存刷新指令是在虚拟地址上操作的,该页暂时被映射和刷新,然后被取消映射。正如前面所解释的,这为VA留下了一个陈旧的TLB项;这个项又指向一个正在使用中的物理页:它即将被映射到一个地址空间。这种情况并不是对每一个页面的映射都会发生,而是只有当物理页面的缓存策略发生变化,有必要将其从缓存中驱逐出去时才会发生。

总之,这两个函数是用来为使用物理页做准备的,所以通过它们映射的页不可避免地会在之后被使用。也许,VMM的设计者认为,延迟失效所带来的性能提升值得让野指针来访问陈旧的项,因为无论如何这类错误都会导致不可预测的效果,即使没有延迟失效也是如此。

2 工作集修剪

以前我们描述了一个物理页是如何被分配来解决一个demand-zero错误并成为工作集的一部分的。为了能够跟踪这样一个页面在不同状态下的生命周期,我们必须分析如何从工作集中删除一个页面。术语修剪是指从工作集中删除页面的行为。

2.1 工作集大小限制

VMM将一个进程的最大WS大小设置为345给页面,但这个限制通常被忽略,这意味着,如果有足够的可用内存,VMM允许工作集的增长超过这个值。

进程可以通过调用SetProcessWorkingSetSizeEx,指定QUOTA_LIMITS_HARDWS_MAX_ENABLE标志,为其工作集大小设置一个真正的上限,这会设置_EPROCESS.Vm.Flags.MaximumWorkingSetHard = 1。(没有这个调用,该标志被设置为0)。

这个标志由MiDoReplacement评估,MiDoReplacementMiAllocateWsle调用,MiAllocateWsle是为一个新页面分配WSL项的函数。当该标志被设置为1并且当前的WS大小大于或等于
_Eprocess.Vm.MaximumWorkingSetsize时,会调用MiReplaceWorkingSetEntry,这将导致在分配新的WSL项之前会释放一个WSL项。

然而,通常的情况是MaximumWorkingSetHard = 0,这样,只要有足够的可用内存,WS就可以增长,以后当VMM决定释放一些内存的时候,WS就会被修剪。这项工作由一个名为工作集管理器(WSM)的VMM组件执行,并在函数MmWorkingSetManager中实现。

2.2 工作集管理器

2.2.1 平衡集管理器线程

MmWorkingSetManager由一个专门的系统线程(即属于系统进程的线程)执行,该线程在一个名为KeBalanceSetManager的函数内旋转,其工作是执行一些内务工作,其中包括对MmWorkingSetManager的调用。

BSM等待两个事件:一个是每秒一次的周期性信号,另一个是由其他VMM函数执行工作集修剪时发出的信号。

2.2.2 内存上下文激活

WSM检查工作集以决定哪些页面需要删除,并更新映射被删除页面的PTEs。工作集链表和PTEs都被映射到私有的虚拟区域中,也就是说,这些区域的内容会从一个地址空间变为另一个地址空间。记住,hyperspace(WSL所在)和分页结构区域的PML4项指向不同地址空间的不同物理页。因此,WSM面临一个基本问题:如何访问系统中每个地址空间的WSL和分页结构。

解决方案在于一个内核特性,它允许一个线程将自己 "附加 "到另一个进程的内存上下文中。在任何时候,一个线程都属于一个进程,也就是创建它的那个进程,但也可能被附加到另一个进程,也就是说,当线程执行时,后者的内存上下文被映射。通常情况下,一个线程看到的是它自己进程的内存上下文,但是内核有一些措施允许一个线程附加到另一个进程。

作为一个例子,下面是属于explorer.exe的线程的!thread调试器扩展命令的输出结果:

0: kd> !process 0 4 explorer.exe
PROCESS fffffa800e670a70
    SessionId: 1  Cid: 0a88    Peb: 7fffffd6000  ParentCid: 0a68
    DirBase: 37b3b000  ObjectTable: fffff8a0010a8010  HandleCount: 606.
    Image: explorer.exe

        THREAD fffffa800eb7bb60  Cid 0a88.0a8c  Teb: 000007fffffde000 Win32Thread: fffff900c07a6a00 WAIT
        THREAD fffffa800e64e060  Cid 0a88.0a90  Teb: 000007fffffdc000 Win32Thread: 0000000000000000 WAIT
        THREAD fffffa800e4ec060  Cid 0a88.0a94  Teb: 000007fffffda000 Win32Thread: fffff900c3567820 WAIT
        THREAD fffffa800eb98950  Cid 0a88.0aa8  Teb: 000007fffffd8000 Win32Thread: fffff900c354f500 WAIT
        THREAD fffffa800eb98450  Cid 0a88.0aac  Teb: 000007fffffd4000 Win32Thread: 0000000000000000 WAIT
        THREAD fffffa800ebc1060  Cid 0a88.0ab0  Teb: 000007fffff9e000 Win32Thread: fffff900c3533500 WAIT
        THREAD fffffa800ebc1a50  Cid 0a88.0ab4  Teb: 000007fffff9c000 Win32Thread: fffff900c1fb2c30 WAIT
        THREAD fffffa800ebcf8c0  Cid 0a88.0abc  Teb: 000007fffff9a000 Win32Thread: fffff900c3005c30 WAIT
        THREAD fffffa800ebe68f0  Cid 0a88.0acc  Teb: 000007fffff98000 Win32Thread: fffff900c3567440 WAIT
        THREAD fffffa800ebe63f0  Cid 0a88.0ad0  Teb: 000007fffff96000 Win32Thread: fffff900c356b820 WAIT
        THREAD fffffa800ebe0b60  Cid 0a88.0ad8  Teb: 000007fffff92000 Win32Thread: fffff900c0793c30 WAIT
        THREAD fffffa800ebe7060  Cid 0a88.0aec  Teb: 000007fffff90000 Win32Thread: fffff900c06cdc30 WAIT
        THREAD fffffa800ec2c4e0  Cid 0a88.0afc  Teb: 000007fffff94000 Win32Thread: fffff900c3561c30 WAIT
        THREAD fffffa800ec37b60  Cid 0a88.0b00  Teb: 000007fffff8e000 Win32Thread: fffff900c0798640 WAIT
        THREAD fffffa800ec4b060  Cid 0a88.0b2c  Teb: 000007fffff86000 Win32Thread: fffff900c30c86b0 WAIT
        THREAD fffffa800ec4cb60  Cid 0a88.0b30  Teb: 000007fffff84000 Win32Thread: fffff900c301f650 WAIT
        THREAD fffffa800ec4d990  Cid 0a88.0b34  Teb: 000007fffff82000 Win32Thread: fffff900c35ab470 WAIT
        THREAD fffffa800ec0e060  Cid 0a88.0b60  Teb: 000007fffff8c000 Win32Thread: fffff900c3597c30 WAIT
        THREAD fffffa800e819060  Cid 0a88.0b64  Teb: 000007fffff8a000 Win32Thread: fffff900c35915e0 WAIT
        THREAD fffffa800eafdb60  Cid 0a88.0b68  Teb: 000007fffff88000 Win32Thread: fffff900c30c0c30 WAIT
        THREAD fffffa800eb01b60  Cid 0a88.0b6c  Teb: 000007fffff80000 Win32Thread: fffff900c35855e0 WAIT
        THREAD fffffa800eaf95d0  Cid 0a88.0b70  Teb: 000007fffff7e000 Win32Thread: fffff900c352b500 WAIT
        THREAD fffffa800eafb6b0  Cid 0a88.0b74  Teb: 000007fffff7c000 Win32Thread: fffff900c35ab850 WAIT
        THREAD fffffa800eafa3b0  Cid 0a88.0b7c  Teb: 000007fffff7a000 Win32Thread: fffff900c35abc30 WAIT
        THREAD fffffa800d063060  Cid 0a88.0b80  Teb: 000007fffff78000 Win32Thread: fffff900c079b290 WAIT
        THREAD fffffa800ece75e0  Cid 0a88.081c  Teb: 000007fffff76000 Win32Thread: 0000000000000000 WAIT
        THREAD fffffa800ed56a90  Cid 0a88.040c  Teb: 000007fffff72000 Win32Thread: fffff900c01f7980 WAIT
        THREAD fffffa800ed5b060  Cid 0a88.0a50  Teb: 000007fffff70000 Win32Thread: 0000000000000000 WAIT
        THREAD fffffa800edae720  Cid 0a88.0a1c  Teb: 000007fffff6c000 Win32Thread: 0000000000000000 WAIT

0: kd> !thread fffffa800eb7bb60
THREAD fffffa800eb7bb60  Cid 0a88.0a8c  Teb: 000007fffffde000 Win32Thread: fffff900c07a6a00 WAIT: (WrUserRequest) UserMode Non-Alertable
    fffffa800e498bb0  SynchronizationEvent
Not impersonating
DeviceMap                 fffff8a000d80420
Owning Process            fffffa800e670a70       Image:         explorer.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      5817           Ticks: 418 (0:00:00:06.520)
Context Switch Count      1827           IdealProcessor: 1                 LargeStack
UserTime                  00:00:00.109
KernelTime                00:00:00.218
Win32 Start Address 0x00000000ff0db790
Stack Init fffff880063d0c70 Current fffff880063d0730
Base fffff880063d1000 Limit fffff880063c5000 Call 0000000000000000
Priority 12 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`063d0770 fffff800`044db992     : fffff900`c07a6a00 fffffa80`0eb7bb60 fffff900`00000000 fffff880`063d0b60 : nt!KiSwapContext+0x7a
fffff880`063d08b0 fffff800`044de1af     : ffffffff`ffb3b4c0 fffffa80`0d69c3d0 fffffa80`00000000 00000000`00000000 : nt!KiCommitThreadWait+0x1d2
fffff880`063d0940 fffff960`0012b837     : fffff900`c07a6a00 fffff960`0000000d 00000000`00000001 fffff960`00117c00 : nt!KeWaitForSingleObject+0x19f
fffff880`063d09e0 fffff960`0012b8d1     : 00000000`00000000 00000000`00000000 00000000`00000001 00000000`00000000 : win32k!xxxRealSleepThread+0x257
fffff880`063d0a80 fffff960`0013e4d2     : fffffa80`0eb7bb60 fffff880`063d0b60 00000000`00000000 00000000`00000000 : win32k!xxxSleepThread+0x59
fffff880`063d0ab0 fffff800`044d58d3     : fffffa80`0eb7bb60 fffff880`063d0b60 00000000`00000001 00000000`00000020 : win32k!NtUserWaitMessage+0x46
fffff880`063d0ae0 00000000`778f933a     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ fffff880`063d0ae0)
00000000`001af758 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : user32!NtUserWaitMessage+0xa

Owning ProcessAttached Process分别显示拥有当前线程的进程和当前线程附加的进程。当线程没有附加到不同的进程时,就显示为N/A

WSM附加到每个正在运行的进程,以便访问其WSLPTE。这可以通过在MiTrimWorkingSet上设置断点来观察,MiTrimWorkingSet是为减少WS而调用的函数,通过运行一个测试程序来增加其工作集,直到VMM决定介入并从其中删除页面。

bp nt!MiTrimWorkingSet

我们看到的第一个有趣的事情是,在一个有足够内存的空闲系统上(例如一个有1GB内存的Windows 7上),断点只是很少被命中,这意味着VMM在没有理由的情况下不会积极地修剪工作集。

当我们吃了比较多的内存时,通过使用MemTests.exe进行不断分配和申请内存后,VMM才决定采取行动,并且最终触发断点,我们会发现:

1: kd> !thread $@thread
THREAD fffffa800cdbb1a0  Cid 0004.0060  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 1
Not impersonating
DeviceMap                 fffff8a001476b70
Owning Process            fffffa800cd91040       Image:         System
Attached Process          fffffa800ea9f060       Image:         MemTests.exe
Wait Start TickCount      225152         Ticks: 64 (0:00:00:00.998)
Context Switch Count      5123           IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.093
Win32 Start Address nt!KeBalanceSetManager (0xfffff800044bc500)
Stack Init fffff88004739c70 Current fffff880047395c0
Base fffff8800473a000 Limit fffff88004734000 Call 0000000000000000
Priority 16 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047399b8 fffff800`0454a63d     : 00000000`00000000 fffffa80`0ea9f3f8 00000000`00000001 fffffa80`0cd91040 : nt!MiTrimWorkingSet
fffff880`047399c0 fffff800`044bc206     : 00000000`00000db8 00000000`00000000 00000000`00000000 00000000`00000000 : nt! ?? ::FNODOBFM::`string'+0x4d834
fffff880`04739a40 fffff800`044bc6c3     : 00000000`00000008 fffff880`04739ad0 00000000`00000001 fffffa80`00000000 : nt!MmWorkingSetManager+0x6e
fffff880`04739a90 fffff800`0476ecce     : fffffa80`0cdbb1a0 00000000`00000080 fffffa80`0cd91040 00000000`00000001 : nt!KeBalanceSetManager+0x1c3
fffff880`04739c00 fffff800`044c2fe6     : fffff800`04643e80 fffffa80`0cdbb1a0 fffffa80`0cdbb680 01020302`01020001 : nt!PspSystemThreadStartup+0x5a
fffff880`04739c40 00000000`00000000     : fffff880`0473a000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

可以发现系统进程的线程附加到了MemTests.exe进程。

线程栈里可以看出他在MmWorkingSetManager里面。

我们可以从几个方面确认当前激活的地址空间是MemTests.exe的空间。首先,@$proc调试器的伪寄存器指向MemTests_EPROCESS实例,这一点可以通过!process进程扩展命令看到:

1: kd> !process @$proc 0
PROCESS fffffa800ea9f060
    SessionId: 1  Cid: 04ac    Peb: 7fffffd8000  ParentCid: 0aac
    DirBase: 17d4b000  ObjectTable: fffff8a001c5e260  HandleCount:  11.
    Image: MemTests.exe

我们反汇编一下MemTest.exe的用户模式区域代码:

1: kd> u memtests!main
MemTests!main [F:\cpp\WmipCodeSamples\MemTests\MemTests.cpp @ 63]:
00000001`3fa611b0 4883ec28        sub     rsp,28h
00000001`3fa611b4 33c0            xor     eax,eax
00000001`3fa611b6 48c705a7e40000ffffffff mov qword ptr [MemTests!g_hFileMapping (00000001`3fa6f668)],0FFFFFFFFFFFFFFFFh
00000001`3fa611c1 488905c0e80000  mov     qword ptr [MemTests!g_pMappedRegionStart (00000001`3fa6fa88)],rax
00000001`3fa611c8 48890589e40000  mov     qword ptr [MemTests!g_pMappedRegionEnd (00000001`3fa6f658)],rax
00000001`3fa611cf 488905a2e60000  mov     qword ptr [MemTests!g_pPrivateRegionStart (00000001`3fa6f878)],rax
00000001`3fa611d6 48890563e40000  mov     qword ptr [MemTests!g_pPrivateRegionEnd (00000001`3fa6f640)],rax
00000001`3fa611dd 488905b4e80000  mov     qword ptr [MemTests!g_pMdl (00000001`3fa6fa98)],rax

这清楚地表明,在用户模式区域,MemTests的地址空间是可见的。

观察一下调用MiTrimWorkingSet的结果也很有趣:下面是当断点被命中时可用的和已修改的页面计数:

1: kd> dd nt!MmAvailablePages l1
fffff800`0464cbc0  0000905d
1: kd> dt nt!_mmpfnlist Total @@masm(nt!MmModifiedPageListHead)
   +0x000 Total : 0x148

我们有0x905d个可用内存页面和0x148个Modified页面。现在我们让执行到MiTrimWorkingSet返回并再次检查计数:

1: kd> g @$ra
nt! ?? ::FNODOBFM::`string'+0x4d834:
fffff800`0454a63d 418b4518        mov     eax,dword ptr [r13+18h]
1: kd> dd nt!MmAvailablePages l1
fffff800`0464cbc0  00009079
1: kd> dt nt!_mmpfnlist Total @@masm(nt!MmModifiedPageListHead)
   +0x000 Total : 0x1fa6

这下我们0x9079个可用页面和0x1fa6个Modified页面。额外的页面已经从MemTests的工作集中删除。

2.2.3 进程修剪准则

WSM考虑到各种因素来决定修剪哪些工作集,试图有利于实际需要物理内存的进程。例如,一个拥有大量工作集且长期闲置的进程将比一个拥有较小工作集的较活跃的进程更优先地被修剪;运行前台应用程序的进程被认为是最后一个,以此类推。WSM继续修剪工作集,直到达到最小可用页数,然后停止。当系统上有足够的 "空闲页面 "时,WSM就会停止修剪。可能,术语 "空闲 "是一种通用说法,而不是实际指Free链表上的页面。一般来说,VMM将修剪过的页面保留在Standby链表上,直到它们真正被repurposed来解决一个错误,可用内存通常被计算为ZeroedFreeStandby链表大小的总和。Standby链表上的页面可以被重新使用,因为分页文件存储了它们内容的副本,所以被认为是可用内存。同时,在它们真正被重用之前,它们被留在Standby状态,这样涉及它们的最终错误就不需要磁盘访问。

整体逻辑是在系统可用页数不足时释放内存,而不对一个进程能消耗多少内存进行硬性限制。毕竟,当有大量的FreeZeroed(因此是未使用的)内存时,限制一个进程可以使用多少内存就没有意义了。

2.2.4 工作集链表项年龄

WSM决定修剪一个工作集时,它也必须选择从其中移除哪些页面。为了有效地做到这一点,WSM会跟踪一个页面最近被访问的时间,这样它就可以删除最近使用最少的页面。

这是通过利用PTE的访问位来实现的:当一个页面第一次被映射时,访问位被设置。当WSM检查一个工作集时,它将清除访问位。如果在再次检查同一工作集时发现该位是清零的,这意味着在两次检查之间没有访问过该页,所以WSM会增加一个存储在_MMWSLE.e1.Age的计数器。因此,该计数器的值等于WSL检查该页并发现其未被访问的次数。另一方面,如果发现被访问的位被设置了,计数器就会被重置,并且该位会再次被清除。WSL首先删除Age值较高的页面,因为它们是最近使用最少的。

值得指出的是,年龄实际上不是物理页的属性,而是工作集项的。尽管我们还没有讨论共享内存,但众所周知,普通DLLs的物理页是在进程之间共享的。这样的页面可以在一个特定的进程中未被访问并因此老化,而在另一个进程中被访问。这就是为什么年龄信息被保存在WSL项中的原因,它是针对每个进程的,而不是在描述全局物理页的_MMPFN中。

WSL项的年龄可以用!wsle扩展命令来检查。由于WSL是一个进程私有区域,在使用!wsle之前,必须用.process /P.process /i命令使所需的进程成为当前进程。

2.3 解决页面错误时的修剪

到目前为止,我们描述了两种页面移除的情况:

  • 当工作集的大小限制生效时,会删除一个页面以腾出空间给另一个页面。
  • WSL执行全局修剪以回收内存时

处理进程遇到的页面错误时,VMM也可以选择修剪工作集。这与上面的第一种情况类似,但不完全相同,因为不是从工作集中删除一个页面,而是调用MiTrimWorkingSet,这表明实际上可以删除多个页面。这至少可以在处理页面错误的两个阶段观察到。

一个是在错误解决的末期,在VA被映射之后。在MmAccessFault+0x3D3处调用MiCheckAging,这个函数在某些条件下又调用MiForcedTrim,后者调用MiTrimWorkingSet,也就是WSM调用的同一个函数。下面这个调用和书上讲的有区别,比较难复现书上的讲的调用流程,先保留这个问题吧。

0: kd> .reload
Connected to Windows 7 7601 x64 target at (Wed Mar 30 19:07:40.791 2022 (UTC + 8:00)), ptr64 TRUE
Loading Kernel Symbols
...............................................................
................................................................
..........................
Loading User Symbols
........................
Loading unloaded module list
.........Unable to enumerate user-mode unloaded modules, Win32 error 0n30

************* Symbol Loading Error Summary **************
Module name            Error
SharedUserData         No error - symbol load deferred

You can troubleshoot most symbol related issues by turning on symbol loading diagnostics (!sym noisy) and repeating the command that caused symbols to be loaded.
You should also verify that your symbol search path (.sympath) is correct.
0: kd> !thread $@thread
THREAD fffffa800ea35060  Cid 0acc.05e4  Teb: 000007fffffde000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a001476b70
Owning Process            fffffa800eae6b30       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      2391555        Ticks: 3 (0:00:00:00.046)
Context Switch Count      1226           IdealProcessor: 0             
UserTime                  00:00:00.062
KernelTime                00:00:01.060
Win32 Start Address MemTests!mainCRTStartup (0x000000013f987d1c)
Stack Init fffff88003304c70 Current fffff880033045c0
Base fffff88003305000 Limit fffff880032ff000 Call 0000000000000000
Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`03304908 fffff800`0459a1ed     : fffffa80`00217140 00000000`0000b25c fffffa80`00217140 00000000`00000000 : nt!MiTrimWorkingSet
fffff880`03304910 fffff800`04464ec4     : 80000000`0b25c867 fffff880`03304a00 fffff880`03304a00 00000000`00000000 : nt!MiForcedTrim+0x11d
fffff880`03304980 fffff800`044cf76e     : 00000000`00000000 00000000`36718000 00000000`00000401 00000000`00000003 : nt! ?? ::FNODOBFM::`string'+0x46ed5
fffff880`03304ae0 00000001`3f981734     : 00000001`3f9898c0 000007fe`f9463400 00000000`001cdb28 00000000`002e3c78 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`03304ae0)
00000000`001cf820 00000001`3f981912     : 00000001`3f98a490 00000000`00000070 00000000`0000000d 00000000`00000001 : MemTests!AccessRegion+0x344 [F:\cpp\WmipCodeSamples\MemTests\MemTests.cpp @ 121] 
00000000`001cf8c0 00000001`3f985bb0     : 00000000`002e1ad0 00000000`00000000 00000000`00000000 000007fe`f94620b0 : MemTests!AccessRegionInterface+0x162 [F:\cpp\WmipCodeSamples\MemTests\MemTests.cpp @ 178] 
00000000`001cf8f0 00000001`3f981392     : 00000000`002e1ad0 00000000`002e1ad0 00000000`001cdc58 00000000`002e3c4f : MemTests!ProcessOption+0x50 [F:\cpp\WmipCodeSamples\MemTests\MemTests.cpp @ 2163] 
00000000`001cf950 00000001`3f987cac     : 00000000`00000000 00000000`00000000 00000000`00000000 01d843db`dd1b585b : MemTests!main+0x1e2 [F:\cpp\WmipCodeSamples\MemTests\MemTests.cpp @ 74] 
(Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : MemTests!invoke_main+0x22 (Inline Function @ 00000001`3f987cac) [d:\a01\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
00000000`001cf980 00000000`76f9652d     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MemTests!__scrt_common_main_seh+0x10c [d:\a01\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
00000000`001cf9c0 00000000`771cc521     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`001cf9f0 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d

我们可以看到这个线程属于MemTests.exe,所以MiTrimWorkingSet没有被WSM调用,相反,出错的线程正在修剪它自己的工作集。这就是出错的线程,因为我们在堆栈上看到了KiPageFaultMmAccessFault

调用堆栈条目 "nt! ?::FNODOBFM::'string'+0x46ed5 "实际上是MmAccessFault的一部分,但之所以这样显示,是因为该指令在主函数代码之外的一个代码块中。这是64位Windows代码中常见的情况,这可以通过查看返回地址后附近的汇编进行确认。

Untitled.png

另一个发生这种情况的阶段是在MiDoReplacement内部(由MiAllocateWsle调用),它可能在某些条件下选择调用MiForcedTrim

MiDoReplacement检查三个条件以决定是否调用MiForcedTrim:

  1. 计算_MMSUPPORT.WorkingSetSize-_MMSUPPORT.FirstDynamic的值,它应该代表工作集中可分页项的数量。结果必须大于物理页总数的75%,才会发生调用。这种情况意味着,与物理内存的数量相比,工作集很大。
  2. 检查自上次BSM运行以来(至少每秒一次),Standby页是否被用于满足内存请求。如果没有Standby页被重新使用,则不调用MiForcedTrim
  3. Free页、Zeroed页和Standby页的计数之和必须小于0x40000,即小于1GB。

这一策略始终有效,即使在工作设定的限制不存在时也是如此(通常是这样)。

这个调用流程,也很难复现,留着吧。

2.3.1 虚拟内存管理器是怎么追踪Standby页面的重复使用的

在本节中,我们将更详细地看到条件2是如何被验证的。本节是为好奇的读者准备的,不感兴趣可以跳过。

MmGetStandbyRepurposed是这个函数估计返回自系统启动以来已经被重复使用的Standby页的数量。它的返回值是一个名为MiStandbyRepurposedByPriority的计数器数组的总和,这些计数器似乎为每个页面的优先级追踪一个单独的计数。

可以看到,MiRestoreTransitionPte增加了这些计数器,而这个函数又被MiRemoveLowestPriorityStandbyPage调用,它被用来从Standby列表中取出一个页面,以便重新使用它(或 "重新利用 "它,正如计数器的名字所暗示的)。换句话说,当一个Standby页被重新使用时,其优先级的计数器被更新;MmGetStandbyRepurposed返回每个优先级计数器的当前值的总和。

MiComputeSystemTrimCriteria函数被WSM执行并调用MmGetStandbyRepurposed,调用指令在+0x2F。稍后,在+0x40处,它将返回的计数复制到一个名为MiLastStandbyRePurposed的变量。换句话说,至少每秒钟一次,从MmGetStandbyRepurposed返回的值被采样并存储到MiLastStandbyRePurposed中。

通过在MiComputeSystemTrimCriteria +0x40处设置断点,我们可以看到它很快就被命中,通常是在WSM内部。下面是一个用调用堆栈命中的例子:

1: kd> bp MiComputeSystemTrimCriteria+0x40
1: kd> g
Breakpoint 0 hit
nt!MiComputeSystemTrimCriteria+0x40:
fffff800`044bc480 890502a91d00    mov     dword ptr [nt!MiLastStandbyRePurposed (fffff800`04696d88)],eax
0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff880`04739a10 fffff800`044bc1ec     nt!MiComputeSystemTrimCriteria+0x40
01 fffff880`04739a40 fffff800`044bc6c3     nt!MmWorkingSetManager+0x54
02 fffff880`04739a90 fffff800`0476ecce     nt!KeBalanceSetManager+0x1c3
03 fffff880`04739c00 fffff800`044c2fe6     nt!PspSystemThreadStartup+0x5a
04 fffff880`04739c40 00000000`00000000     nt!KiStartSystemThread+0x16

在+0x40处的mov将从MmGetStandbyRepurposed返回的值存储到MiLastStandbyRePurposed中,该值被保存在eax中,所以,通过这里的断点,我们可以看到变量的新旧两个值。使用Windbg,我们能够设置一个断点,当它被命中时,会自动执行一系列的命令,所以我们可以让调试器执行r命令,显示寄存器的内容和mov前的变量值(因为mov引用了内存变量,Windbg将其内容作为r输出的一部分显示)。之后,我们可以让Windbg执行g命令,恢复执行,这样,断点实际上并没有停止系统,而是显示计数器的新旧值并自动恢复。这样的断点是用以下调试器命令设置的:

bp nt!MiComputeSystemTrimCriteria+0x40 "r;g"

然后我们可以看到断点每秒钟被击中一次(当WSM唤醒时),显示eax中的计数器新值和变量中的旧值。在每一次命中,我们都会得到以下输出

rax=00000000000d323d rbx=0000000000000000 rcx=0000000000000000
rdx=fffff80004680860 rsi=0000000000000000 rdi=0000000000022f0f
rip=fffff800044bc480 rsp=fffff88004739a10 rbp=0000000000000001
 r8=0000000000000000  r9=0000000000000100 r10=fffff8000464d070
r11=000000000000002d r12=fffff88004739a60 r13=fffff80004702900
r14=0000000000000000 r15=fffff800058e0080
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
nt!MiComputeSystemTrimCriteria+0x40:
fffff800`044bc480 890502a91d00    mov     dword ptr [nt!MiLastStandbyRePurposed (fffff800`04696d88)],eax ds:002b:fffff800`04696d88=000d323d

我们可以观察到的第一件事是,只要系统中有充足的可用内存,计数器的值就不会改变,即使是在很长一段时间内。

如果我们启动一个需要大量内存的程序,我们会看到计数器在增加,断点被更频繁地击中,尽管频率不固定,因为WSM被试图从进程中回收内存的VMM唤醒了。下面是MemTests在一台1GB的机器上访问700MB范围的所有页面时的输出摘录。

rax=00000000000d9c38 rbx=0000000000000000 rcx=0000000000000000
rdx=fffff80004680860 rsi=0000000000000000 rdi=000000000001f26c
rip=fffff800044bc480 rsp=fffff88004739a10 rbp=0000000000000001
 r8=0000000000000000  r9=0000000000000100 r10=0000000000000000
r11=0000000000000000 r12=fffff88004739a60 r13=fffff80004702900
r14=0000000000000000 r15=fffff800058e0080
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
nt!MiComputeSystemTrimCriteria+0x40:
fffff800`044bc480 890502a91d00    mov     dword ptr [nt!MiLastStandbyRePurposed (fffff800`04696d88)],eax ds:002b:fffff800`04696d88=000d9c38
rax=00000000000dd510 rbx=0000000000000000 rcx=00000000000d9c38
rdx=fffff80004680860 rsi=00000000000038d8 rdi=0000000000018a81
rip=fffff800044bc480 rsp=fffff88004739a10 rbp=0000000000000001
 r8=0000000000000000  r9=0000000000000100 r10=0000000000000000
r11=0000000000000000 r12=fffff88004739a60 r13=fffff80004702900
r14=0000000000000000 r15=fffff800058e0080
iopl=0         nv up ei pl nz ac po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
nt!MiComputeSystemTrimCriteria+0x40:
fffff800`044bc480 890502a91d00    mov     dword ptr [nt!MiLastStandbyRePurposed (fffff800`04696d88)],eax ds:002b:fffff800`04696d88=000d9c38
rax=00000000000e9a24 rbx=0000000000000000 rcx=00000000000dd510
rdx=fffff80004680860 rsi=000000000000c514 rdi=000000000000c678
rip=fffff800044bc480 rsp=fffff88004739a10 rbp=0000000000000001
 r8=0000000000000000  r9=0000000000000100 r10=0000000000000000
r11=0000000000000000 r12=fffff88004739a60 r13=fffff80004702900
r14=0000000000000000 r15=fffff800058e0080
iopl=0         nv up ei pl nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
nt!MiComputeSystemTrimCriteria+0x40:
fffff800`044bc480 890502a91d00    mov     dword ptr [nt!MiLastStandbyRePurposed (fffff800`04696d88)],eax ds:002b:fffff800`04696d88=000dd510

值得注意的是,该变量的值似乎总是等于上一个断点击中的eax中的值。这表明MiLastStandbyRePurposed只被这条指令更新。

MiLastStandbyRePurposed似乎是对一个Standby页被重新利用的所有次数的运行计数,它从未减少过。它与当前有多少内存可用无关,当测试程序终止后释放内存时,这一事实得到了证实:该计数器没有减少,只是再次稳定到一个常数,因为系统不再需要重新利用Standby页了。

把我们的注意力转回到上一节的条件#2,MiDoReplacement调用MmGetStandbyRepurposed,并将返回值与MiLastStandbyRePurposed的当前值进行比较;如果数值相等,它不会调用MiForcedTrim

当这两个值不同时,意味着在MiComputeSystemTrimCriteria最后一次更新MiLastStandbyRePurposed和解决页面错误期间,Standby页面已经被重新利用,所以MiForcedTrim被调用,前提是条件#1和#3也被满足。

2.4 修剪对页面共享计数和引用计数的影响

当一个页面由于前面解释的任何原因从工作集中删除时,它的共享计数,存储在_MMPFN.u2.ShareCount被递减了。每次有页面被添加到工作集时,共享计数都会被增加。对于私有页,共享计数最多只能是1,但对于共享内存(后面会分析),可以大于1。

当共享计数被递减并降至0时,页面引用计数(_MMPFN.u3.ReferenceCount)也被递减,如果这个计数器也达到了0,页面就会被移至ModifiedStandby列表。

2.5 锁定内存中的页面

像设备驱动这样的内核模式组件可以锁定内存中的一个页面,防止它被重新利用,不管它是否是用户模式WS的一部分。这是通过增加ReferenceCount来完成的,通过内核模式的运行时函数来锁定一个页面。当这种情况发生时,即使ShareCount下降到0,ReferenceCount仍然大于0,所以VMM将该页保持在Active状态。准确的说当共享计数下降到0时MiPfnShareCountlsZero被调用,递减引用计数,如果它仍然大于0,将页面状态改为ModifiedStandby,但避免将页面链接到相关链表中。因此,页面的状态不是严格意义上的Active,但由于页面本身不在任何列表上,所以它不会被重新利用。锁定的目的是为了确保物理页面留给锁定它的人使用。当所有之前锁定该页的组件解锁后,ReferenceCount下降到0,该页被移到其中一个列表中。为了简单起见,我们将说一个被锁定的页面仍然是Active的。

在这种逻辑下,只要一个页面的引用计数不为零,它就不能被带出Active状态。包括该页在内的每个工作集(对于共享页可以有多个工作集)都会增加引用计数。其他组件建立的每个锁也会增加引用计数。

3 脏页移除和置出

现在我们知道了页面是如何从工作集中移除的,我们可以回到跟踪一个页面的生命周期。在第26节中,我们看到了一个新的物理页是如何被错误地认为是被修改过的或脏的。下一个逻辑步骤是看当一个已修改的页面从工作集中被移除时会发生什么。

3.1 从工作集到Modified链表

3.1.1 Modified List是做什么的

VMM决定从工作集中删除一个页面时,它必须询问自己在分页文件中是否存在该页面内容的副本。如果该页从未被写入分页文件或自上次写入后被修改过,其内容必须被保存到文件中,然后才能被重新使用。一个需要保存的页面,其PTE的位_MMPTE.u.Hard.Dirty_MMPTE.u.Hard.Dirty1设置为1。此外,这样的页面的_MMPFN.u3.e1.Modified也会设置为1。

以前我们讲过一个页面是开始时它是脏的。我们以后会看到一个页面何时变得干净,何时又变脏。

一个被删除的脏页被添加到Modified List 中。这在下面的状态图中用粗箭头表示:

Untitled 1.png

Modified List中的一个页面正在等待被写入分页文件以成为可重用的。在列表中的页面,只要有指令访问它,它就可以在任何时候被faulted back到工作集中。PTE_MMPFN的设置是为了帮助VMM执行这些任务。

3.1.2 Modified List 细节

通过一点调查,我们可以看到,VMM使用三个_MMPFNLIST的静态实例来跟踪Modified List

MmModifiedPageListHead实际上并不指向列表元素,因为它的FlinkBlink成员总是被设置为0xFFFFFFFF'FFFFFFF(或-1),这代表一个空列表。然而,MmModifiedPageListHead.Total存储了系统中已修改页面的总数,也就是说,它的值似乎总是等于Sysinternals的Process Explorer所显示的总数。

0: kd> dt nt!_MMPFNLIST @@masm(nt!MmModifiedPageListHead)
   +0x000 Total            : 0x63
   +0x008 ListName         : 3 ( ModifiedPageList )
   +0x010 Flink            : 0xffffffff`ffffffff
   +0x018 Blink            : 0xffffffff`ffffffff
   +0x020 Lock             : 0

MmModifiedPageListByColor实际上是一个_MMPFN实例链表的头部。Flink成员持有PFN数据库中第一个_MMPFN实例的索引(而不是地址)。反过来,_MMPFN.u1.Flink都存储着列表中下一个实例的索引。作为链表一部分的_MMPFNs_MMPFN.u3.e1.PageLocation = 3,对应于_MMLISTS.ModifiedPageList

0: kd> dt nt!_MMPFNLIST @@masm(nt!MmModifiedPageListByColor)
   +0x000 Total            : 2
   +0x008 ListName         : 3 ( ModifiedPageList )
   +0x010 Flink            : 0x135ac
   +0x018 Blink            : 0x352ab
   +0x020 Lock             : 0

尽管它的名字是这样的,但这个链表似乎并没有将具有相同颜色的页面分组,而是通常包括任何颜色的页面。此外,MmModifiedPageListByColor不是一个数组的地址(可能是按颜色索引的),而是_MMPFNLIST的一个实例。

MmModifiedProcessListByColor是另一个List的头,其结构与前一个相同。

在进入Modified状态之前,MmModifiedPageListByColor中的页面映射的是系统地址范围内的地址,而MmModifiedProcessListByColor所指向的页面映射的是用户地址。这是对链表内容进行随机探索后得出的结论。

作为一个例子,我们可以检查 MmModifiedPageListByColor list的最后一个节点。首先,我们从Blink成员那里获得节点索引:

0: kd> dt nt!_MMPFNLIST Blink @@masm(nt!MmModifiedPageListByColor)
   +0x018 Blink : 0x352ab

注意使用 @@masm()结构用来 "强迫 "Windbg理解后面的表达式结果是地址。如果没有@@masm(),Windbg会把nt!MmModifiedPageListByColor解释为一个成员名,并抱怨说它在_MMPFNLIST中找不到它。

有了索引,我们就可以计算_MMPFN地址,因为我们知道PFN数据库从0xFFFFFA80'00000000开始,每个实例的大小是0x30字节。我们把这个地址输入到dt命令中,以检查PteAddress成员:

0: kd> dt nt!_MMPFN PteAddress @@masm(FFFFFA8000000000 + 352ab*30)
   +0x010 PteAddress : 0xfffff8a0`0122da20 _MMPTE

下面PTE的地址,它使用物理页来映射虚拟地址。请记住,在Modified list中的页面仍然保存着内存内容(实际上是未保存的),而且指向它的PTE仍然 "指向 "物理页面,尽管P位是清零的。!pte扩展命令给了我们PTE与这个地址所映射的VA:

注意:根据内存区域图,这里得到的PteAddress明显有问题,由于多次复现,仍然得不到书上的例子。

Untitled 2.png

0: kd> !pte 0xfffff8a0`0122da20
                                           VA fffff8a00122da20
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1400    PDE at FFFFF6FB7E280048    PTE at FFFFF6FC50009168
contains 0000000039F04863  contains 0000000004B38863  contains 000000000F010863  contains B87000001ADAC963
pfn 39f04     ---DA--KWEV  pfn 4b38      ---DA--KWEV  pfn f010      ---DA--KWEV  pfn 1adac     -G-DA--KW-V

这里就按照书上的模糊图讲吧:

Untitled 3.png

Untitled 4.png

Untitled 5.png

VA"后面的地址是被映射到我们页面的虚拟地址,我们可以看到它是一个系统范围的地址。这似乎总是如此,并表明这个list是针对系统地址的。

!pte的输出中,contains阴影中的是PTE的值(0000000021600863),即存储在PTE地址的8个字节被分组为一个四字。这个值提醒了我们一个很容易被遗忘的事实,因为它似乎与我们开始的_MMPFN没有任何关系。首先,它是一个奇数值,所以PTE的第0位,也就是P位被置1了,因此它是一个有效的PTE,主动映射了一个虚拟地址。另外,页面的PFN,即第47:12位被设置为0x21600,而不是_MMPFN的索引——0x2088F,

这是因为分页结构范围映射了当前地址空间的分页结构,而这恰好不是我们的脏页所属的空间。换句话说,!pte命令是在寻找另一个PTE,即在当前地址空间的地址0xFFFFF6FB'80008400处发现的那个。

然而,这个PTE映射地址0xFFFFF700'01080000的事实在任何地址空间都是存在的。这是分页结构如何映射的一个特征,所以我们可以使用!pte来从PTE地址中获得映射的地址。

最后,观察一下映射的VA 0xFFFFF700'01080000是hyperspace区域的一部分,它是每个地址空间的私有部分,这一点很有用。这就解释了为什么PTE的内容会在不同的地址空间中变化。对于其他的系统范围区域,虚拟到物理的映射在不同的地址空间是相同的,我们不会看到PTE内容和__MMPFN之间明显的不一致,下面的就是两个相等的值:

1: kd> dt nt!_MMPFNLIST Blink @@masm(nt!MmModifiedPageListByColor)
   +0x018 Blink : 0x1eb47f
1: kd> dt nt!_MMPFN PteAddress @@masm(FFFFFA8000000000 + 0x1eb47f*30)
   +0x010 PteAddress : 0xfffff6fc`4001c270 _MMPTE
1: kd> !pte 0xfffff6fc`4001c270
                                           VA fffff8800384e000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E2000E0    PTE at FFFFF6FC4001C270
contains 000000022D004863  contains 000000022D003863  contains 00000001EC790863  contains 80000001EB47FBE2
pfn 22d004    ---DA--KWEV  pfn 22d003    ---DA--KWEV  pfn 1ec790    ---DA--KWEV  not valid
                                                                                  Transition: 1eb47f
                                                                                  Protect: 1f - Outswapped kernel stack

如果我们对列表中的节点进行同样的探索,由MmModifiedProcessListByColor所指向的列表节点进行同样的探索,我们通常会找到用户范围地址的_MMPFN实例。

0: kd> dt nt!_MMPFNLIST Blink @@masm(nt!MmModifiedProcessListByColor)
   +0x018 Blink : 0x1c0840
0: kd> dt nt!_MMPFN PteAddress @@masm(FFFFFA8000000000 + 0x1c0840*30)
   +0x010 PteAddress : 0xfffff680`00014ec8 _MMPTE
0: kd> !pte 0xfffff680`00014ec8
                                           VA 00000000029d9000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB400000A0    PTE at FFFFF68000014EC8
contains 0070000215302867  contains 008000022F503867  contains 0000000000000000
pfn 215302    ---DA--UWEV  pfn 22f503    ---DA--UWEV  contains 0000000000000000
not valid

3.1.3 怎么观察脏页的移除

通过下面的步骤,我们有机会看到脏页的移除:

  • 编写一个程序,用VirtualAllocEx保留并提交大量的内存(大约占总物理内存的70%);该程序还必须向所有的页面写入数据(每页写一个字节就够了),以确保物理页面存在并被视为脏页(因为我们前面说过仅仅读取页面是不够的)。

  • 运行程序的一个实例,用以下方法获得其_EPROCESS实例的地址

    !process 0 0 executable_name

  • _MMPFNLIST.Total成员上设置写入断点。由于这实际上是该结构的第一个成员,因此在符号地址处设置断点即可。使用/p选项,使断点只对测试程序有效:

ba w 8 /p eprocess_address nt!MmModifiedPageListByColor
ba w 8 /p eprocess_address nt!MmModifiedProcessListByColor
  • 运行程序的另一个实例,或者不运行,其他进程也会触发断点

其他程序实例将脏页从第一个程序实例中踢出,这就触发了断点。当这种情况发生时,通常我们会发现当前的线程是工作集管理器,附属于进程的第一个实例,正在努力将一个页面插入到一个修改过的列表中。下面是一个典型的例子:

Breakpoint 2 hit
nt!MiInsertPageInList+0x152:
fffff800`044ab3b2 488b4f18        mov     rcx,qword ptr [rdi+18h]
0: kd> !thread @$thread
THREAD fffffa800cdbeb60  Cid 0004.0060  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000fcf610
Owning Process            fffffa800cd91040       Image:         System
Attached Process          fffffa800e475420       Image:         MemTests.exe
Wait Start TickCount      2173076        Ticks: 64 (0:00:00:00.998)
Context Switch Count      39114          IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:01.918
Win32 Start Address nt!KeBalanceSetManager (0xfffff80004476500)
Stack Init fffff88004947c70 Current fffff880049475c0
Base fffff88004948000 Limit fffff88004942000 Call 0000000000000000
Priority 16 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`04947620 fffff800`0449b65c     : 00000000`00000000 fffffa80`00090ba0 00000000`00000000 2aaaaaaa`aaaaaaab : nt!MiInsertPageInList+0x152
fffff880`049476b0 fffff800`044745aa     : fffffa80`00417540 00000000`00000001 ffffffff`ffffffff fffff800`04490992 : nt!MiPfnShareCountIsZero+0x19c
fffff880`04947720 fffff800`04553ebf     : fffffa80`0e4757b8 fffff880`04947930 fffffa80`00000000 fffff880`00000001 : nt!MiFreeWsleList+0x28e
fffff880`04947910 fffff800`0450463d     : 00000000`00002000 fffffa80`000248ac 00000000`00000005 00000000`00000000 : nt!MiTrimWorkingSet+0x14f
fffff880`049479c0 fffff800`04476206     : 00000000`00008483 00000000`00000000 00000000`00000000 00000000`00000000 : nt! ?? ::FNODOBFM::`string'+0x4d834
fffff880`04947a40 fffff800`044766c3     : 00000000`00000008 fffff880`04947ad0 00000000`00000001 fffffa80`00000000 : nt!MmWorkingSetManager+0x6e
fffff880`04947a90 fffff800`04728cce     : fffffa80`0cdbeb60 00000000`00000080 fffffa80`0cd91040 00000000`00000001 : nt!KeBalanceSetManager+0x1c3
fffff880`04947c00 fffff800`0447cfe6     : fffff800`045fde80 fffffa80`0cdbeb60 fffffa80`0cdbb180 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04947c40 00000000`00000000     : fffff880`04948000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

当执行在MilnsertPagelnList+0x152处停止时,就像这个例子一样,rdi持有正在添加页面的链表地址,rbp是页面的PFN。在当前的例子中,我们有

0: kd> ln @rdi
Browse module
Clear breakpoint 2

(fffff800`045f9bd8)   nt!MmModifiedProcessListByColor   |  (fffff800`045f9c00)   nt!MiInitialPoolFreed
Exact matches:
    nt!MmModifiedProcessListByColor = <no type information>
0: kd> r @rbp
rbp=000000000000303e
0: kd> r rdi
rdi=fffff800045f9bd8

3.1.4 Modified 页面的_MMPFN以及PTE

Modified List中的一个页面的PTE_MMPTE_TRANSITION的一个实例表示。下面是一个典型的例子:

1: kd> dt nt!_MMPFNLIST ffffff800045f9bd8
   +0x000 Total            : 2
   +0x008 ListName         : 3 ( ModifiedPageList )
   +0x010 Flink            : 0x303e
   +0x018 Blink            : 0x303e
   +0x020 Lock             : 0
1: kd> dt nt!_MMPFN PteAddress @@masm(FFFFFA8000000000 + 0x303e*30)
   +0x010 PteAddress : 0xfffff680`00000ff0 _MMPTE
1: kd> dt nt!_mmpte u.trans. 0xfffff680`00000ff0
   +0x000 u        :
      +0x000 Trans    :
         +0x000 Valid    : 0y0
         +0x000 Write    : 0y1
         +0x000 Owner    : 0y1
         +0x000 WriteThrough : 0y0
         +0x000 CacheDisable : 0y0
         +0x000 Protection : 0y00100 (0x4)
         +0x000 Prototype : 0y0
         +0x000 Transition : 0y1
         +0x000 PageFrameNumber : 0y000000000000000000000011000000111110 (0x303e)
         +0x000 Unused   : 0y1001110011100000 (0x9ce0)
1: kd> dt nt!_MMPFNLIST ffffff800045f9bd8
   +0x000 Total            : 2
   +0x008 ListName         : 3 ( ModifiedPageList )
   +0x010 Flink            : 0x303e
   +0x018 Blink            : 0x303e
   +0x020 Lock             : 0
1: kd> dt nt!_MMPFN PteAddress @@masm(FFFFFA8000000000 + 0x303e*30)
   +0x010 PteAddress : 0xfffff680`00000ff0 _MMPTE
1: kd> dt nt!_mmpte u.trans. 0xfffff680`00000ff0
   +0x000 u        :
      +0x000 Trans    :
         +0x000 Valid    : 0y0
         +0x000 Write    : 0y1
         +0x000 Owner    : 0y1
         +0x000 WriteThrough : 0y0
         +0x000 CacheDisable : 0y0
         +0x000 Protection : 0y00100 (0x4)
         +0x000 Prototype : 0y0
         +0x000 Transition : 0y1
         +0x000 PageFrameNumber : 0y000000000000000000000011000000111110 (0x303e)
         +0x000 Unused   : 0y1001110011100000 (0x9ce0)

Valid位是0,使PTE是无效的。

WriteOwnerWriteThroughCacheDisable是硬件(即处理器定义的)PTER/WU/SPWTPCD位,这似乎保留了PTE有效时的值。

Protection跨越了第5-9位,是VMM定义的页面保护编码,例如,最初来自VAD的,由传递给内存分配函数的保护属性产生。当PTE变得有效时,之前的PTE值,包括第5-9位的VMM保护,被复制到_MMPFN.OriginalPte,所以当页面被移到Modified list中时,VMM有这个值可用。我们将在以后讲解的软故障中看到,当页面故障发生在像这样的PTE映射的地址上时,保护位被用来建立页面的实际硬件保护,与demand zero fault的情况很相似。因此,将保护位放回PTE中,使其为最终的错误处理做好准备。

Transition被设置为1,标志着这个PTE是一个Transition PTE,即在Modified listStandby list(统称为Transition lists)中的页面。注意,对于一个有效的PTE来说,同样的位(第11位),对软件来说是可用的,并且被VMM用来标记一个可写页。然而,有效PTE的第0位被设置为1;当第0位被清除时,第11位表示一个Transition PTE

PageFrameNumber横跨第12-47位,它存放的是页面的PFN,这个页面仍在存储虚拟页面内容的。这个字段的值与PTE有效时的值相同。

我们将在后面的内容中看到,当试图引用这个PTE映射的VA时,会发生软错误,即VMMModified list中取出该页并将其映射回地址空间中。VMM显然需要PFN来设置PTE和实现_MMPFN实例。这里的解决方案是简单地让PFNPTE本身中不被访问。

Modified list中的_MMPFN具有以下特征:

  • u1.Flinku2.Blink用来链接链表中的实例。当页面被使用时,u1存储的是工作集索引,u2存储的是共享计数;当页面从工作集中删除后,这两个数据就不再有用了。
  • PteAddress始终存放着E映射页面的PTE的虚拟地址。
  • u3.e1.PageLocation设置为3,也就是_MMLISTS.ModifiedPageList
  • OriginalPte的值与该页在工作集时的值相同,即第5-9位被设置为软件保护值。正如我们刚才看到的,同样的值现在也在实际PTE的第5-9位。对于修改列表中的_MMPFNOriginalPte的其他位通常为0,

3.1.5 TLB无效化

当一个页面被移到Modifed list中时,相应的PTE中的P位被清空。为了确保这一变化被所有的处理器察觉,VMM必须使相应的TLB项失效。VMM在执行的处理器上使TLB项无效,并向其他处理器发送处理器间中断,以指示它们做同样的事情。

3.1.6 页面移除期间的同步

移除一个页面包括多个操作,因为PTE_MMPFN都必须被更新,所以VMM必须准备好处理一个正在被移除的页面上的页面错误。这是通过工作集推锁(_EPROCESS.Vm.WorkingSetMutex)来完成的。WSM在更新页面状态之前调用MiAttachAndLockWorkingSet获得推锁,并在所有的状态信息被更新后释放它。页面错误处理程序试图在故障处理的一开始就获取这个推锁,因此,如果它已经被 WSM 所拥有,错误线程就会被挂起,直到页面移除完成。

工作集推锁以前已经提到过了。

3.2 将页面内容写入到分页文件中

3.2.1 Standby List 细节

当一个页面被置出时,它就会进入Standby list。我们知道实际上有8个按页面优先级排列的链表。通过分析一个名为MiRemoveLowestPriorityStandbyPage的函数,我们可以看到静态的MmStandbyPageListByPriority是一个由8个_MMPFNLIST结构组成的数组,每个优先级有一项。第项0是优先级为0的页面链表的头部,以此类推。在下文中,当提及某个特定的子列表并不重要时,我们将笼统地称为 "Standby list"。

3.2.2 Modified 页面写者线程

Modified写者(MPW)是一个系统线程,它运行在优先级为17的位置,执行MiModifiedPageWriter函数,它将modified页面写入分页文件,并将它们移到Standby list

这个线程等待三个同步对象:

  1. MmModifiedPageWriterGate(一个门对象),在各种条件下发出信号,包括以下情况:
    • 工作集管理器发现Zeroed 和 Free List的总页面数小于20,000个页面。
    • 工作集管理器发现存储在MmAvailablePages中的可获取页面小于262,144时,也就是小于1GB
    • 显示请求刷新整个页面
  2. MiRescanPageFilesEvent事件
  3. 分页文件头(MmPagingFileHeader)内部的一个事件, 这个数据结构用于管理分页文件。

当把modified 页面内容整合到分页文件的时候,所有这些同步对象都被VMM用来唤醒MPW

我们可以通过设置如下断点来观察MPW的工作情况。首先,我们通过!process命令转储系统进程,并寻找以MiModifiedPageWriter为Win32起始地址的线程,从而找到MPW_ETHREAD地址。

1: kd> !process 0 0 System
PROCESS fffffa800cd91040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00187000  ObjectTable: fffff8a0000017d0  HandleCount: 550.
    Image: System
1: kd> !process fffffa800cd91040 7
...

THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 WAIT: (WrFreePage) KernelMode Non-Alertable
            fffff8000467e520  Gate
        Not impersonating
        DeviceMap                 fffff8a000008c10
        Owning Process            fffffa8030f23040       Image:         System
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      304            Ticks: 13063 (0:00:03:23.784)
        Context Switch Count      1              IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
        Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
        Stack Init fffff88004740c70 Current fffff88004740940
        Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
        Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
        Child-SP          RetAddr               : Args to Child                                                           : Call Site
        fffff880`04740980 fffff800`044d4992     : fffffa80`30f23040 fffffa80`30f55680 fffffa80`30f55680 00000000`00000008 : nt!KiSwapContext+0x7a
        fffff880`04740ac0 fffff800`04496b8b     : 00000000`00000010 fffffa80`30f23040 fffff8a0`00000000 00000000`00000010 : nt!KiCommitThreadWait+0x1d2
        fffff880`04740b50 fffff800`0445f28a     : fffffa80`30f55680 00000000`00000001 fffffa80`30f23040 00000000`00000000 : nt!KeWaitForGate+0xfb
        fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x5a
        fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
        fffff880`04740c40 00000000`00000000     : fffff880`04741000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16
...

然后,我们把两个断点设置在这个线程对MmModifiedProcessListByColorMmModifiedPageListByColor的写入断点。对Modified list页面计数以及MPW在处理modified页面时会更新它们。断点是通过以下命令设置的:

1: kd> ba w 8 /t fffffa800cdbb180 nt!MmModifiedPageListByColor
1: kd> ba w 8 /t fffffa800cdbb180 nt!MmModifiedProcessListByColor

之后,如果一些进程产生一些分页活动,直到其中一个断点被击中。我们看到一个像下面这样的调用栈:

书上这个断点太频繁了,导致系统比较卡,因此换个思路,先禁用上面的两个断点,通过IDA找到MiGatherPagefilePages的引用,然后得到断点偏移位置。

Untitled 6.png

1: kd> u nt!MiModifiedPageWriter+1b6
nt!MiModifiedPageWriter+0x1b6:
fffff800`0445f3e6 e8b5f4ffff      call    nt!MiGatherPagefilePages (fffff800`0445e8a0)
fffff800`0445f3eb 833d4a0d2a0000  cmp     dword ptr [nt!MmSystemShutdown (fffff800`0470013c)],0
fffff800`0445f3f2 7575            jne     nt!MiModifiedPageWriter+0x239 (fffff800`0445f469)
fffff800`0445f3f4 8b056ef02100    mov     eax,dword ptr [nt!MmWriteAllModifiedPages (fffff800`0467e468)]
fffff800`0445f3fa 85c0            test    eax,eax
fffff800`0445f3fc 7558            jne     nt!MiModifiedPageWriter+0x226 (fffff800`0445f456)
fffff800`0445f3fe 4c8b155b0c2a00  mov     r10,qword ptr [nt!MmNumberOfPhysicalPages (fffff800`04700060)]
fffff800`0445f405 8d4806          lea     ecx,[rax+6]
1: kd> bp nt!MiModifiedPageWriter+0x1b6

接下来,通过查看任务管理器空闲内存数,用MemTests.exe内存分配相应的空闲内存,就能断下来了:

Untitled 7.png

断下来后,再启用两个硬件断点,等待二次断下,就能得到书上的调用栈了:

0: kd> bl
     0 d Enable Clear  fffff800`0463dbb0 w 8 0001 (0001) nt!MmModifiedPageListByColor
     Match thread data fffffa80`30f55680
     1 d Enable Clear  fffff800`0463dbd8 w 8 0001 (0001) nt!MmModifiedProcessListByColor
     Match thread data fffffa80`30f55680
     2 e Disable Clear  fffff800`0445f3e6     0001 (0001) nt!MiModifiedPageWriter+0x1b6

0: kd> be 0-1
0: kd> g
Breakpoint 1 hit
nt!MiUnlinkPageFromLockedList+0x206:
fffff800`044e0c36 44393d1f7a1800  cmp     dword ptr [nt!MiMirroringActive (fffff800`0466865c)],r15d
0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff880`04740120 fffff800`0445f1e6     nt!MiUnlinkPageFromLockedList+0x206
01 fffff880`047401b0 fffff800`0459abd3     nt!MiReferencePageForCluster+0x86
02 fffff880`047401e0 fffff800`0445eb09     nt!MiBuildPageFileCluster+0x633
03 fffff880`04740ac0 fffff800`0445f3eb     nt!MiGatherPagefilePages+0x269
04 fffff880`04740ba0 fffff800`0476ccce     nt!MiModifiedPageWriter+0x1bb
05 fffff880`04740c00 fffff800`044c0fe6     nt!PspSystemThreadStartup+0x5a
06 fffff880`04740c40 00000000`00000000     nt!KiStartSystemThread+0x16

3.2.3 Write I/O 期间的页面状态

向分页文件写入一个页面意味着执行一个实际的磁盘I/O操作,这需要时间来完成,所以MPW发起的是一个异步写入,在I/O进行的同时继续执行。当写完后,一个APC中断MPW,使该页进入最终状态。

VMM通过设置_MMPFN.u3.e1.WriteinProgress = 1来跟踪该页正在进行写入的事实,这意味着该页处于Transition状态。

千万不要混淆Transition状态和Transition lists,即Standby和Modified list:处于这种状态的页面不在任何列表上,并且有一个I/O正在进行;处于StandbyModified列表上的页面没有I/O正在进行。

当写操作结束时,该页被移到Standby list

这些状态转换在下面的状态图中由两个粗箭头表示。

Untitled 8.png

3.2.4 Standby 页面的PTE和_MMPFN的最终状态

最相关的_MMPFN成员如下:

u1.Flink.u2.Blink:将该实例链接到list的其他部分

PteAddress 当它处于活动状态时。仍然指向映射该页面的PTE

u3.e1.PageLocation = _MMLISTS.StandByPageList

u3.e1.Modified = 0

OriginalPte.u.Soft.Protection 仍然设置为VMM定义的保护掩码

OriginalPte.u.Soft.PageFileLow 设置为一个选择分页文件的索引(Windows支持多个分页文件)。

OriginalPte.u.Soft.PageFileHigh 进入分页文件的偏移量(以页为单位,即右移三个十六进制数字,因为它们总是0)。

后面我们将在调试会话中看到这些成员跟踪一个页面被移到Standby list中,我们也将看到PTE并没有因为页面写入而被更新,也就是说,当页面变为Modified页面时,它保留了原来的值。

3.2.5 对簇读取的支持

VMM试图通过读取多个页面来优化分页文件的访问。我们将在第34章(第249页)分析分页时看到,它不是读取单个页面来解决错误,而是读取一个块,由所需的页面和它前后的几个页面组成,其操作原则是,当一个进程访问一个地址时,它很可能很快就会访问它附近的其他地址。这样的页面块被称为。由于程序指令是在虚拟地址上操作的,所以簇是由连续的虚拟页号组成的。例如,考虑一个在虚拟地址0x5234上发生硬错误的指令:VMM需要读取VA 0x5000处要映射的内容,但实际上它读取的是由VPN 0x4000、0x5000、0x6000组成的簇(这只是一个例子,实际的簇大小将在后面详细说明)。用于映射的物理页不需要是连续的,它们很可能在稀疏的地址上,因为用于分配的标准是基于从哪里分配它们:Free、Zeroed、Standby list、页面颜色、NUMA节点,等等。

为了使簇有效,簇中的页面应该在分页文件中的连续偏移处有其内容,以尽量减少磁盘搜索时间。为了实现这一点,VMM试图将连续的虚拟页块写到连续的分页文件偏移处。这意味着MPW不会从Modified list中随机抓取页面并将其写入分页文件的第一个可用槽,而是建立一个连续的页面链表,寻找一个足够大的空闲分页文件区域来容纳它们,并在一次操作中写入它们,从而优化写入I/O,并将换出的内容放置在一起,这样簇读取效率会提高。

因此在它们被移到Modified List之前,MPW必须能够建立在连续的虚拟地址上映射的页面列表。此外,对于用户范围的页面,它们必须属于相同的地址空间,因为集群读取会在以后寻找特定地址空间的被换出的内容。同样,这与写入时的页面物理地址没有关系。

这带来了两个问题:

  1. MPW需要知道在Modified list中拥有这个页面的地址空间
  2. 它必须能够检查该地址空间,寻找原始页面周围的其他Modified页面。

换句话说,Modified List上的页面可以来自任何地址空间和虚拟地址范围,但MPW必须有办法按照地址空间和地址范围组织它们,以优化分页文件分配。

有资料说MPW通过查看_MMPFN.OriginalPte获取这一信息,但这不可能是正确的:在这个阶段,OriginalPte只存储了一个_MMPTE的实例,其中_MMPTE.u.Soft.Protection设置为页面保护,其他字段为零。没有关于该页被映射的虚拟地址的信息,更没有关于它属于哪个地址空间的信息。

解决方案似乎在于_MMPFN.u4.PteFrame成员:我们已经知道,它在映射中存储了父分页结构的PFN,这意味着以下几点:

  • 对于一个映射虚拟页的页面,它存储了页表的PFN
  • 对于一个页表,它存储了页目录的PFN
  • 对于一个页目录,它存储页面目录指针表的PFN
  • 对于一个页面目录指针表,它存储的是四级页面映射的PFN

注意:在Modified list上的一个页面实际上没有映射VA,因为引用它的PTE已经失效,即它的P位被清0。然而,该PTE仍然存储着该页的PFN,如果发生软错误,可以再次使之有效。这样的页面的_MMPFNu4.PteFrame中仍然保存着页表的PFN。

根据设计,一个PFN也是该页的_MMPFN实例的索引,它位于:

0xFFFFFA8000000000 + PFN * 0x30

所以VMM可以访问所有映射特定页面的分页结构的_MMPFN

我们知道_MMPFN.u1根据页面的状态有不同的含义,例如:

  • 对于一个工作集中的页面,他存的就是WS index (u1.WsIndex)
  • 对于在链表上的页面,他存的时下一个元素的指针 (u1.Flink)

事实证明,这个成员在PML4_MMFPN中以一种特殊的方式被使用:它存储了拥有这个地址空间的_EPROCESS的地址。这是有道理的,因为通常存储在u1中的信息对PML4来说是不需要的:它有一个固定的WS索引,它从不被放入一个列表中。

另外,一个PML4_MMPFN很容易从其至少两个成员中识别出来:

  • PteAddress被设置为0xFFFFF6FB'7DBEDF68,即auto-entry的地址
  • u4.PteFrame 被设置为该项本身的PFN。当然,这个成员是可用的,因为没有上层的分页结构可以再指向了。

总之,给定一个Modified list上的_MMPFNMPW可以很容易地提取到拥有该页的_EPROCESS的指针。

顺便说一下,这给出了一种方法,可以知道PFN数据库中处于活动状态页面的地址空间。

然后MPWrcx中的_EPROCESS指针调用MiForceAttachProcess,将自己附加到进程地址空间。现在它可以访问进程所有的PTE;它还拥有我们开始的页面的PTE的地址,来自_MMPFN.PteAddress,所以它可以检查周围的PTEs,建立一个要写入的页面列表。

简而言之,MPW使用了与工作集管理器相同的技巧:它附加在地址空间上;为了达到这个目的,它遵循_MMPFN的层次结构来获得_PROCESS指针。

接下来的章节将描述如何验证这一行为。

3.2.6 观察MPW线程的工作情况

本节展示了如何用WinDbg来观察迄今为止所陈述的概念。它给出了支撑前几节陈述的证据,并提供了关于MPW的额外细节。

#### 从Modified 页面获取_EPROCESS

在本节中,我们将探索在MmModifiedProcessByColor列表中第一页的_EPROCESS

首先,我们获取从链表头获取PFN:

1: kd> dt nt!_mmpfnlist Flink @@masm(nt!MmModifiedProcessListByColor)
   +0x010 Flink : 0x551b9

我们使用Flink中发现的PFN作为进入PFN数据库的索引,并检查页面的_MMPFN中的几个字段:

1: kd> dt nt!_MMPFN PteAddress u3.e1. OriginalPte.u. u4.PteFrame @@masm(FFFFFA8000000000 + 0x551b9*30)
   +0x010 PteAddress     : 0xfffff683`ff7e1138 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y011
         +0x000 WriteInProgress : 0y0
         +0x000 Modified       : 0y1
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x80
         +0x000 VolatileLong   : 0x80
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST
   +0x028 u4             :
      +0x000 PteFrame       : 0y0000000000000000000000000000000001010101010110110010 (0x555b2)

u4.PteFrame得到PFN后,现在我们使用同样的命令,一直重复这样做,直到我们到达PML4的_MMPFN

PT的_MMPFN

1: kd> dt nt!_MMPFN u4.PteFrame @@masm(FFFFFA8000000000 + 0x555b2*30)
   +0x028 u4          :
      +0x000 PteFrame    : 0y0000000000000000000000000000000001010101111111001011 (0x55fcb)

PD的_MMPFN

1: kd> dt nt!_MMPFN u4.PteFrame @@masm(FFFFFA8000000000 + 0x55fcb*30)
   +0x028 u4          :
      +0x000 PteFrame    : 0y0000000000000000000000000000000001010111101010100111 (0x57aa7)

PDPT的_MMPFN:

1: kd> dt nt!_MMPFN u4.PteFrame @@masm(FFFFFA8000000000 + 0x57aa7*30)
   +0x028 u4          :
      +0x000 PteFrame    : 0y0000000000000000000000000000000001011000101110100000 (0x58ba0)

PML4的_MMPFN

1: kd> dt nt!_MMPFN u4.PteFrame @@masm(FFFFFA8000000000 + 0x58ba0*30)
   +0x028 u4          :
      +0x000 PteFrame    : 0y0000000000000000000000000000000001011000101110100000 (0x58ba0)

可以看到,最后一项的_MMPFNu4.PteFrame设置为了自己的索引。所以这个实例的u1.Flink会设置为_EPROCESS的地址,接下来我们就可以使用!process指令了。

1: kd> dt nt!_MMPFN u1.Flink PteAddress u4.PteFrame @@masm(FFFFFA8000000000 + 0x58ba0*30)
   +0x000 u1          :
      +0x000 Flink       : 0xfffffa80`3225d060
   +0x010 PteAddress  : 0xfffff6fb`7dbedf68 _MMPTE
   +0x028 u4          :
      +0x000 PteFrame    : 0y0000000000000000000000000000000001011000101110100000 (0x58ba0)
1: kd> !process 0xfffffa80`3225d060
PROCESS fffffa803225d060
    SessionId: 0  Cid: 0ed4    Peb: 7fffffdf000  ParentCid: 01e4
    DirBase: 58ba0000  ObjectTable: fffff8a0021cd160  HandleCount: 652.
    Image: SearchIndexer.exe
    VadRoot fffffa80326e1880 Vads 256 Clone 0 Private 2102. Modified 2745. Locked 1.
    DeviceMap fffff8a000008c10
    Token                             fffff8a0026e4060
    ElapsedTime                       00:17:47.570
    UserTime                          00:00:00.280
    KernelTime                        00:00:01.154
    QuotaPoolUsage[PagedPool]         166736
    QuotaPoolUsage[NonPagedPool]      30968
    Working Set Sizes (now,min,max)  (3370, 50, 345) (13480KB, 200KB, 1380KB)
    PeakWorkingSetSize                3550
    VirtualSize                       125 Mb
    PeakVirtualSize                   128 Mb
    PageFaultCount                    11508
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      4558

现在我们找到了SearchIndexer.exe的一个页面。

我们可以进一步验证我们的结论是正确的;首先,我们切换到这个进程的上下文:

然后我们把指向Modified页面的PTE地址输入到!pte命令中。

这个地址在页面的_MMPFNPteAddress成员中:

1: kd> .cache forcedecodeptes

Max cache size is       : 1048576 bytes (0x400 KB)
Total memory in cache   : 0 bytes (0 KB)
Number of regions cached: 0
0 full reads broken into 0 partial reads
    counts: 0 cached/0 uncached, 0.00% cached
    bytes : 0 cached/0 uncached, 0.00% cached
** Transition PTEs are implicitly decoded
** Virtual addresses are translated to physical addresses before access
** Prototype PTEs are implicitly decoded
1: kd> .process fffffa803225d060
Implicit process is now fffffa80`3225d060
1: kd> dt nt!_MMPFN PteAddress u3.e1. OriginalPte.u. u4.PteFrame @@masm(FFFFFA8000000000 + 0x551b9*30)
   +0x010 PteAddress     : 0xfffff683`ff7e1138 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y011
         +0x000 WriteInProgress : 0y0
         +0x000 Modified       : 0y1
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x80
         +0x000 VolatileLong   : 0x80
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST
   +0x028 u4             :
      +0x000 PteFrame       : 0y0000000000000000000000000000000001010101010110110010 (0x555b2)

1: kd> !pte 0xfffff683`ff7e1138
                                           VA 000007fefc227000
PXE at FFFFF6FB7DBED078    PPE at FFFFF6FB7DA0FFD8    PDE at FFFFF6FB41FFBF08    PTE at FFFFF683FF7E1138
contains 0080000057AA7867  contains 7AF0000055FCB867  contains 00600000555B2867  contains DD800000551B9886
pfn 57aa7     ---DA--UWEV  pfn 55fcb     ---DA--UWEV  pfn 555b2     ---DA--UWEV  not valid
                                                                                  Transition: 551b9
                                                                                  Protect: 4 - ReadWrite

我们可以看到PTE是无效的,处于Transition状态,PFN是0x551b9,这是我们在开始时从Modified列表头中获取的PFN。这证实了我们为该页找到了正确的地址空间。

#### 观察MPW附加进程

书上讲通过对_MMPFN.u1.Flink下访问断点。捕获MPW从_MMPFN读取_EPROCESS的事实。但是这个复现起来有难度。现在考虑采用IDA反汇编技术,根据书上的输出例子定位代码位置进行下断。

1: kd> u nt!MiBuildPageFileCluster+0x133
nt!MiBuildPageFileCluster+0x133:
fffff800`0459a6d3 488bc1          mov     rax,rcx
fffff800`0459a6d6 49c1e504        shl     r13,4
fffff800`0459a6da 4c2bea          sub     r13,rdx
fffff800`0459a6dd 498b4d28        mov     rcx,qword ptr [r13+28h]
fffff800`0459a6e1 4923c9          and     rcx,r9
fffff800`0459a6e4 483bc8          cmp     rcx,rax
fffff800`0459a6e7 75e6            jne     nt!MiBuildPageFileCluster+0x12f (fffff800`0459a6cf)
fffff800`0459a6e9 498b7d00        mov     rdi,qword ptr [r13]
1: kd> bp fffff800`0459a6e9

接下来启动MemTests.exe,分配足够的内存,把系统分配的差不多。接下来启动另外一个MemTests.exe分配同样大小的内存。就能触发断点了,这里有些技巧,就是把虚拟机内存调小些,不要太高了

Breakpoint 3 hit
nt!MiBuildPageFileCluster+0x149:
fffff800`0459a6e9 498b7d00        mov     rdi,qword ptr [r13]
0: kd> r r13
r13=fffffa8000752d30
0: kd> dt nt!_mmpfn u1.Flink fffffa8000752d30
   +0x000 u1       : 
      +0x000 Flink    : 0xfffffa80`32b139c0
0: kd> !process 0xfffffa80`32b139c0
PROCESS fffffa8032b139c0
    SessionId: 0  Cid: 1320    Peb: 7fffffd8000  ParentCid: 01e4
    DirBase: 270f1000  ObjectTable: fffff8a002663630  HandleCount: 326.
    Image: svchost.exe
    VadRoot fffffa8030e79010 Vads 337 Clone 0 Private 14106. Modified 41227. Locked 0.
    DeviceMap fffff8a000008c10
    Token                             fffff8a002b05060
    ElapsedTime                       05:29:04.744
    UserTime                          00:00:03.244
    KernelTime                        00:00:00.468
    QuotaPoolUsage[PagedPool]         145152
    QuotaPoolUsage[NonPagedPool]      43776
    Working Set Sizes (now,min,max)  (3070, 50, 345) (12280KB, 200KB, 1380KB)
    PeakWorkingSetSize                22131
    VirtualSize                       130 Mb
    PeakVirtualSize                   148 Mb
    PageFaultCount                    79136
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      15837

可以看到,这里的r13就是_MMPFN的一个实例。让我们看看使用!thread看看现在的线程,可以发现他还没有附加到任何进程。

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      80387          Ticks: 0
Context Switch Count      9              IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:00.998
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff88004740940
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3247d5a8 fffffa80`00000000 00000000`0000468c fffffa80`00000100 : nt!MiBuildPageFileCluster+0x149
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0000468c 00000000`00000000 fffffa80`0000468c 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

单步跟踪一会后,我们来到了调用MiForceAttachProcess的地方。rcx存放的是_EPROCESS的地址,然后作为第一个参数传递给这个函数。再这个调用完成后,MPW完成了附加操作。

0: kd> p
nt!MiBuildPageFileCluster+0x1dd:
fffff800`0459a77d e8be04f2ff      call    nt!MiForceAttachProcess (fffff800`044bac40)
0: kd> r rcx
rcx=fffffa8032b139c0
0: kd> p
nt!MiBuildPageFileCluster+0x1e2:
fffff800`0459a782 413bc7          cmp     eax,r15d
0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032b139c0       Image:         svchost.exe
Wait Start TickCount      80459          Ticks: 4 (0:00:00:00.062)
Context Switch Count      11             IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:01.653
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473f300
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3247d5a8 fffffa80`00000000 00000000`0000468c fffffa80`00000100 : nt!MiBuildPageFileCluster+0x1e2
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0000468c 00000000`00000000 fffffa80`0000468c 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

#### 观察MPW线程写Modified页面

我们可以在MPW写Modified页时拦截它,在MiBuildPageFileCluster+0x62e处设置一个断点。实际上,调用栈显示的是MiBuildPageFileCluster+0x633,因为它显示的是调用的返回地址;+0x62e处是对MiReferencePogeForCluster的调用,rbprcx都存储了即将写入的页面的_MMPFN地址。使用/t选项,我们设置一个断点,这个断点只能由MPW触发

0: kd> bp /t fffffa8030f55680 nt!MiBuildPageFileCluster+0x62e
0: kd> g
Breakpoint 4 hit
nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)

断下后,rbp存放着_MMPFN的地址

0: kd> r
rax=0000000000000080 rbx=cd300000193de886 rcx=fffffa80004bb9a0
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8000459abce rsp=fffff880047401e0 rbp=fffffa80004bb9a0
 r8=00000000000193de  r9=fffff6800001d3e0 r10=0000000000000000
r11=0000000003a88000 r12=0000000000000000 r13=0000000000000001
r14=fffff6800001d440 r15=00000000000193de
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)

我们可以看到,MPW附加在svchost.exe上:

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032b139c0       Image:         svchost.exe
Wait Start TickCount      80459          Ticks: 13 (0:00:00:00.202)
Context Switch Count      11             IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:01.669
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473f300
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3247d5a8 fffffa80`00000000 00000000`0000468c fffffa80`00000100 : nt!MiBuildPageFileCluster+0x62e
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0000468c 00000000`00000000 fffffa80`0000468c 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

查看_MMPFN,可以看到PageLocation = 3 (_MMLIST.ModifidedPageList) 以及PTE的地址存在PteAddress

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. @rbp
   +0x000 u1                  :
      +0x000 Flink               : 0x1cb7e
   +0x008 u2                  :
      +0x000 Blink               : 0x1ed35
   +0x010 PteAddress          : 0xfffff680`0001d441 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y011
         +0x000 WriteInProgress     : 0y0
         +0x000 Modified            : 0y1
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000000000000000000 (0)

注意_MMPTE.u3.e1.Modified = 1,也就是他的页面是脏的。

我们还可以通过观察索引等于FlinkBlink_MMPFN指向上面的实例来验证_MMPFN是链接到一个链表的。首先,我们计算上面的_MMPFN的索引,其地址是房租rbp

0: kd> ? (@rbp - FFFFFA8000000000)/30
Evaluate expression: 103390 = 00000000`000193de

然后,我们看一下当前Biink所指向的实例,并验证它是否将Flink设置为上面计算的索引:

0: kd> dt nt!_mmpfn u1.Flink @@masm(FFFFFA8000000000 +30*0x1ed35)
   +0x000 u1       :
      +0x000 Flink    : 0x193de

对当前Flink所指向的实例进行同样的检查:

0: kd> dt nt!_mmpfn u2.Blink @@masm(FFFFFA8000000000 +30*0x1cb7e)
   +0x008 u2       :
      +0x000 Blink    : 0x193de

还要注意观察PteAddress的第0位置1了;它被用作锁来独占访问_MMPFN。MPW锁住_MMPFN后,其他需要更新它的线程将在一个循环中忙于等待,直到该位被清除。

_MMPTE.OriginalPte.PageFileHigh是0,这告诉我们页面进入分页文件的偏移量还没有确定。

由于MPW附加到进程了,那么地址空间是活动的,所以我们可以检查PTE的内容:

0: kd> !pte 0xfffff680`0001d440
                                           VA 0000000003a88000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB400000E8    PTE at FFFFF6800001D440
contains 02A0000024798867  contains 034000002509B867  contains 5BC000001A70A867  contains CD300000193DE886
pfn 24798     ---DA--UWEV  pfn 2509b     ---DA--UWEV  pfn 1a70a     ---DA--UWEV  not valid
                                                                                  Transition: 193de
                                                                                  Protect: 4 - ReadWrite

上面的值对应于一个无效的PTE,其中包括

_MMPTE.u.Trans.Protection = 4
_MMPTE.u.Trans.Transition = 1
_MMPTE.u.Trans.PageFrameNumber = 0x193de

注意PageFrameNumber如何被设置为_MMPFN实例的索引。

我们可以在这个页面被写入分页文件时进一步跟踪它。第一步是在_MMPFN.OriginalPte时设置一个写访问断点,MPW将要写入分页文件的偏移。我们在MiUpdatePfnBackingStore+0x85处停止,_MMPFN地址
rbx中。

0: kd> ba w 8 /t fffffa8030f55680 0xfffffa80`004bb9a0+20
nt!MiUpdatePfnBackingStore+0x85:
fffff800`0445f125 f0836310fe      lock and dword ptr [rbx+10h],0FFFFFFFEh ds:002b:fffffa80`004bb9b0=0001d441
0: kd> r
rax=0000468c00000000 rbx=fffffa80004bb9a0 rcx=0000468c00000080
rdx=0000000000000000 rsi=000000000000468c rdi=0000000000000000
rip=fffff8000445f125 rsp=fffff880047401b0 rbp=0000000000000000
 r8=000000000000468c  r9=0000000000000000 r10=0000000003b32000
r11=0000000003b44000 r12=0000000000000000 r13=0000000000000001
r14=fffff6800001da20 r15=fffffa803247d5d8
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
nt!MiUpdatePfnBackingStore+0x85:
fffff800`0445f125 f0836310fe      lock and dword ptr [rbx+10h],0FFFFFFFEh ds:002b:fffffa80`004bb9b0=0001d441

_MMPFN被设置成了下面的结果:

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. @rbx
   +0x000 u1                  :
      +0x000 Flink               : 0
   +0x008 u2                  :
      +0x000 Blink               : 0
   +0x010 PteAddress          : 0xfffff680`0001d441 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y011
         +0x000 WriteInProgress     : 0y1
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

我们看到现在 Modified = 0 以及 WriteInProgress = 1。后者将在MPW释放了_MMPFN上的锁后重新设置,这个成员用来跟踪写操作正在进行。OriginalPte现在包括分页文件的偏移量。_MMPFN仍然被锁定,因为PteAddress是一个奇数值。FlintBlink被设置为0,所以_MMPFN不再被链接到链表中。我们虽然没有去看PTE,但它是不变的。下面是MPW的状态:

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032b139c0       Image:         svchost.exe
Wait Start TickCount      80504          Ticks: 36 (0:00:00:00.561)
Context Switch Count      12             IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:02.059
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473fed0
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401b0 fffff800`0459ae1c     : 00000000`00000000 00000580`00000000 00000000`00000000 00000000`0000468c : nt!MiUpdatePfnBackingStore+0x85
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3247d5a8 fffffa80`00000000 00000000`0000468c fffffa80`00000100 : nt!MiBuildPageFileCluster+0x87c
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0000468c 00000000`00000000 fffffa80`0000468c 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

MiBuildPageFileCluster+0x87c处设置一个断点,这是堆栈上的第一个返回地址,我们在返回调用者的过程中让MPW断下

Breakpoint 6 hit
nt!MiBuildPageFileCluster+0x87c:
fffff800`0459ae1c 4103dd          add     ebx,r13d

我们可以看到,现在_MMPFN被解锁了(PteAddress的第0位被清除),并且仍然WriteInProgress是置1的。其他VMM组件可以访问_MMPFN,但是他们会知道这个页面有一个I/O正在进行中。

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0
   +0x008 u2                  :
      +0x000 Blink               : 0
   +0x010 PteAddress          : 0xfffff680`0001d440 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y011
         +0x000 WriteInProgress     : 0y1
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

通过在堆栈的返回地址上设置断点,我们最终在MiModifiedPageWriter+0x1bb处停止。

0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff880`047401e0 fffff800`0445eb09     nt!MiBuildPageFileCluster+0x87c
01 fffff880`04740ac0 fffff800`0445f3eb     nt!MiGatherPagefilePages+0x269
02 fffff880`04740ba0 fffff800`0476ccce     nt!MiModifiedPageWriter+0x1bb
03 fffff880`04740c00 fffff800`044c0fe6     nt!PspSystemThreadStartup+0x5a
04 fffff880`04740c40 00000000`00000000     nt!KiStartSystemThread+0x16
Breakpoint 7 hit
nt!MiModifiedPageWriter+0x1bb:
fffff800`0445f3eb 833d4a0d2a0000  cmp     dword ptr [nt!MmSystemShutdown (fffff800`0470013c)],0
0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
IRP List:
    fffffa8032a636c0: (0006,0358) Flags: 00060003  Mdl: fffffa803247d5a8
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      80577          Ticks: 0
Context Switch Count      14             IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:02.418
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff880066db560
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`0473fea0 00000000`00000000 : nt!KiStartSystemThread+0x16

当我们在这里时,_MMPFN仍然有WritelnProgress = 1并且被解锁:

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0
   +0x008 u2                  :
      +0x000 Blink               : 0
   +0x010 PteAddress          : 0xfffff680`0001d440 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y011
         +0x000 WriteInProgress     : 0y1
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

事实上,MPW已经走到并返回到了顶层函数,同时WritelnProgress仍为1,这一事实告诉我们,MPW发出了一个异步I/O写,并在操作过程中继续执行代码。

我们可以通过在我们的_MMPFN实例上设置一个u3.e1的写访问断点来观察写的最后步骤,当MPW更新WritelnProgress的时候捕捉它。当断点被击中时,rbx被设置为_MMPFN的地址:

0: kd> ba w 4 fffffa80004bb9a0+0x18
0: kd> g
Breakpoint 8 hit
nt!MiWriteCompletePfn+0x43:
fffff800`045094bb 0fb74318        movzx   eax,word ptr [rbx+18h]
0: kd> r
rax=0000000000000001 rbx=fffffa80004bb9a0 rcx=fffffa80004bb9a0
rdx=0000000000000000 rsi=fffffa803247ddd8 rdi=0000000000000001
rip=fffff800045094bb rsp=fffff8800473fce0 rbp=fffffa803247d5e0
 r8=0000000000000000  r9=0000000000000000 r10=0000000000000000
r11=fffff8000467df20 r12=0000000000000000 r13=0000000000000002
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
nt!MiWriteCompletePfn+0x43:
fffff800`045094bb 0fb74318        movzx   eax,word ptr [rbx+18h] ds:002b:fffffa80`004bb9b8=0001

线程状态如下:

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      80577          Ticks: 22 (0:00:00:00.343)
Context Switch Count      14             IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:02.449
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff880066db560
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`0473fce0 fffff800`045099b4     : 000005e4`00000001 00001f80`00000000 00000000`00000000 00000000`00000000 : nt!MiWriteCompletePfn+0x43
fffff880`0473fd50 fffff800`044b9297     : fffffa80`3247d540 41764d56`00000000 00000038`00000000 fffffa80`3247d5d8 : nt!MiWriteComplete+0x1b4
fffff880`0473fe10 fffff800`044c2cf7     : fffffa80`30f55680 fffffa80`30f556d0 4ae72606`734a4e16 00010000`00000000 : nt!IopCompletePageWrite+0x57
fffff880`0473fe40 fffff800`044c2fa7     : fffff880`04740101 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiDeliverApc+0x1c7
fffff880`0473fec0 fffff800`0456b7db     : fffff880`047409e0 fffff880`04740a00 00000000`00000001 00000000`00000000 : nt!KiApcInterrupt+0xd7 (TrapFrame @ fffff880`0473fec0)
fffff880`04740050 fffff800`049506aa     : 00030010`00000000 7500002a`00000001 850021f0`00000001 fffff880`04740101 : nt!KeThawExecution+0x26b
fffff880`047400d0 fffff800`04530941     : fffff880`04740100 fffff880`04740968 fffff800`04641e80 00000000`00000000 : nt!KdExitDebugger+0x7a
fffff880`04740100 fffff800`0494f03b     : fffff880`047401c0 fffff880`047406c0 fffff880`04740a10 fffff800`044f7ac5 : nt! ?? ::FNODOBFM::`string'+0x19d81
fffff880`04740140 fffff800`0450adb2     : fffff880`04740968 fffff880`04740a10 fffff880`04740a10 fffffa80`3247d540 : nt!KdpTrap+0x2b
fffff880`04740190 fffff800`044cecc2     : fffff880`04740968 fffffa80`30f55680 fffff880`04740a10 fffffa80`30f23040 : nt!KiDispatchException+0x126
fffff880`04740830 fffff800`044ccaf4     : fffffa80`31700cb0 fffffa80`324cd370 fffffa80`31700c00 fffffa80`32a636c0 : nt!KiExceptionDispatch+0xc2
fffff880`04740a10 fffff800`0445f3ec     : fffffa80`0000478c 00000000`00000000 fffffa80`0000468c 00000000`00000000 : nt!KiBreakpointTrap+0xf4 (TrapFrame @ fffff880`04740a10)
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bc
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`0473fea0 00000000`00000000 : nt!KiStartSystemThread+0x16

当前线程仍然是fffffa8030f55680,即MPW;它不再附加进程,并且正在从KiDeliverApc调用的函数中更新WritelnProgress。这意味着I/O完成是由APC发出信号的。不幸的是,调用栈有点脏,有一个由KiBreakpointTrap创建的陷阱帧,这似乎是由于调试器造成的。这可能是由于在调试器从上一个断点返回控制权之前,写操作引发了APC中断,所以我们看到APC中断的同时线程上下文正在被恢复。

现在_MMPFN再次被锁住,因为MPW正在更新他的值,WriteInProcess变为了0。

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0
   +0x008 u2                  :
      +0x000 Blink               : 0
   +0x010 PteAddress          : 0xfffff680`0001d441 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y011
         +0x000 WriteInProgress     : 0y0
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

再第一个返回地址上下断,断下我们发现_MMPFN还是锁着的。u3.e1.PageLocation被设置为2 (_MMLISTS.StandByPageList)。 FlinkBlink将实例链到一个链表里。MPW还在执行,将_MMPFN变为Standby状态,但还没有释放它。

Breakpoint 9 hit
nt!MiWriteComplete+0x1b4:
fffff800`045099b4 4533ff          xor     r15d,r15d
0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0xffffffff`ffffffff
   +0x008 u2                  :
      +0x000 Blink               : 0x12f5b
   +0x010 PteAddress          : 0xfffff680`0001d441 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y010
         +0x000 WriteInProgress     : 0y0
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

下一个栈上的返回地址是nt!IopCompletePageWrite+0x57,此时_MMPFN解锁了。

0: kd> bd 9
0: kd> g
Breakpoint 10 hit
nt!IopCompletePageWrite+0x57:
fffff800`044b9297 488b5c2430      mov     rbx,qword ptr [rsp+30h]
0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0x18dea
   +0x008 u2                  :
      +0x000 Blink               : 0x12f5b
   +0x010 PteAddress          : 0xfffff680`0001d440 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y010
         +0x000 WriteInProgress     : 0y0
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

我们可以看一下PTE的最终状态,但我们必须切换到MPW最初附加的进程的上下文(它现在已经脱离了)。在这个特定的调试会话中,.cache forcedecodeptes不能正常工作,所以使用了.process /i

0: kd> g
Breakpoint 7 hit
nt!MiModifiedPageWriter+0x1bb:
fffff800`0445f3eb 833d4a0d2a0000  cmp     dword ptr [nt!MmSystemShutdown (fffff800`0470013c)],0
0: kd> bd 7
0: kd> g
Breakpoint 10 hit
nt!IopCompletePageWrite+0x57:
fffff800`044b9297 488b5c2430      mov     rbx,qword ptr [rsp+30h]
0: kd> bd 10
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`044c7490 cc              int     3

因为我们执行了g,所以最好确认一下_MMPFN没有改变:

0: kd> dt nt!_mmpfn u1.Flink u2.Blink u3.e1. PteAddress OriginalPte.u.Soft. fffffa80004bb9a0
   +0x000 u1                  :
      +0x000 Flink               : 0x18dea
   +0x008 u2                  :
      +0x000 Blink               : 0x12f5b
   +0x010 PteAddress          : 0xfffff680`0001d440 _MMPTE
   +0x018 u3                  :
      +0x002 e1                  :
         +0x000 PageLocation        : 0y010
         +0x000 WriteInProgress     : 0y0
         +0x000 Modified            : 0y0
         +0x000 ReadInProgress      : 0y0
         +0x000 CacheAttribute      : 0y01
         +0x001 Priority            : 0y001
         +0x001 Rom                 : 0y0
         +0x001 InPageError         : 0y0
         +0x001 KernelStack         : 0y0
         +0x001 RemovalRequested    : 0y0
         +0x001 ParityError         : 0y0
   +0x020 OriginalPte         :
      +0x000 u                   :
         +0x000 Soft                :
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000100011010001100 (0x468c)

_MMPFN有相同的PteAddressPageFileHigh,所以我们可以安全地假设页面状态没有改变。看一下PTE,它在整个过程中根本没有被改变过:

0: kd> !pte 0xfffff680`0001d440
                                           VA 0000000003a88000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB400000E8    PTE at FFFFF6800001D440
contains 02A0000024798867  contains 034000002509B867  contains 5BC000001A70A867  contains CD300000193DE886
pfn 24798     ---DA--UWEV  pfn 2509b     ---DA--UWEV  pfn 1a70a     ---DA--UWEV  not valid
                                                                                  Transition: 193de
                                                                                  Protect: 4 - ReadWrite

因此,就PTE而言,页面状态是Modified还是Standby并不重要。

总之,我们观察到Modified list上的一个页面被MPW处理,进入WritelnProgress = 1的Transition状态,最后成为一个Standby页,其分页文件的偏移量被保存在OriginalPte中。

#### 观察MPW线程处理连续的页面块

可以在MPW构建页面块的时候观察它,方法是在MiBuildPageFileCluster+0x62e放置一个断点。当断点被击中时,!thread命令显示MPW附加到现有的一个进程中,rcx持有被添加到列表中的modified页的_MMPFN的地址,所以我们可以从中获得PTE地址。

我们看到断点被多次击中,连续的PTEs_MMPFNs被处理。下面是在MPW附加到MemTests.exe时观察到的PTE范围的一个例子:

0: kd> bp /t fffffa80`30f55680 nt!MiBuildPageFileCluster+0x62e ".if(@$t0<6){.printf \"hits #%d: \",$t0; r @$t0=@$t0+1;dt nt!_mmpfn PteAddress @rcx;g;};.else{}"
0: kd> r @$t0=0
0: kd> g
hits #0:    +0x010 PteAddress : 0xfffff680`000014c9 _MMPTE
hits #1:    +0x010 PteAddress : 0xfffff680`000014d1 _MMPTE
hits #2:    +0x010 PteAddress : 0xfffff680`000014d9 _MMPTE
hits #3:    +0x010 PteAddress : 0xfffff680`000014e1 _MMPTE
hits #4:    +0x010 PteAddress : 0xfffff680`000014e9 _MMPTE
hits #5:    +0x010 PteAddress : 0xfffff680`000014f1 _MMPTE
nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)
0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000dd4050
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032a689a0       Image:         MemTests.exe
Wait Start TickCount      80976          Ticks: 129 (0:00:00:02.012)
Context Switch Count      19             IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:07.503
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff88004740940
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3248d7c8 fffffa80`00000000 00000000`0000513e fffffa80`00000100 : nt!MiBuildPageFileCluster+0x62e
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`00005131 00000000`00000000 fffffa80`00005131 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`0473fed0 00000000`00000000 : nt!KiStartSystemThread+0x16

实际上PTE的地址应该是8的倍数,然后第0位清0。但是Windbg输出的结果中_MMPFN.PteAddress是第0位设置为1的,因为_MMPFN被锁上了。

_MMPFN.u.Soft.OriginalPte.PageFileHigh存储了已经写入分页文件的分页文件偏移量,在这个阶段,这个成员仍然被设置为0。 我们可以通过放置一个写入它的断点来捕捉MPW对它的更新。我们发现写在MiUpdatePfnBackingStore+0x85处(实际上是在MiUpdatePfnBackingStore+0x81处,因为访问断点是在访问后的指令上击中的)。通过在这里放置一个断点,我们可以看到多次命中,每次都将...PageFileHigh设置为连续的偏移量(这里_MMPFN的地址被存储到rbx)。

0: kd> ba w 8 /t fffffa80`30f55680 @rcx+20
0: kd> g
nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)
0: kd> bd 0
0: kd> g
Breakpoint 1 hit
nt!MiUpdatePfnBackingStore+0x85:
fffff800`0445f125 f0836310fe      lock and dword ptr [rbx+10h],0FFFFFFFEh
0: kd> ub
nt!MiUpdatePfnBackingStore+0x6a:
fffff800`0445f10a 25e0030000      and     eax,3E0h
fffff800`0445f10f 4803c9          add     rcx,rcx
fffff800`0445f112 480bc8          or      rcx,rax
fffff800`0445f115 488bc6          mov     rax,rsi
fffff800`0445f118 48c1e020        shl     rax,20h
fffff800`0445f11c 8bc9            mov     ecx,ecx
fffff800`0445f11e 480bc8          or      rcx,rax
fffff800`0445f121 48894b20        mov     qword ptr [rbx+20h],rcx
0: kd> u fffff800`0445f121
nt!MiUpdatePfnBackingStore+0x81:
fffff800`0445f121 48894b20        mov     qword ptr [rbx+20h],rcx
fffff800`0445f125 f0836310fe      lock and dword ptr [rbx+10h],0FFFFFFFEh
fffff800`0445f12a 410fb6c4        movzx   eax,r12b
fffff800`0445f12e 440f22c0        mov     cr8,rax
fffff800`0445f132 488b5c2430      mov     rbx,qword ptr [rsp+30h]
fffff800`0445f137 488b6c2438      mov     rbp,qword ptr [rsp+38h]
fffff800`0445f13c 488b742440      mov     rsi,qword ptr [rsp+40h]
fffff800`0445f141 488b7c2448      mov     rdi,qword ptr [rsp+48h]
0: kd> dt nt!_mmpfn OriginalPte.u.Soft. @rbx
   +0x020 OriginalPte         : 
      +0x000 u                   : 
         +0x000 Soft                : 
            +0x000 Valid               : 0y0
            +0x000 PageFileLow         : 0y0000
            +0x000 Protection          : 0y00100 (0x4)
            +0x000 Prototype           : 0y0
            +0x000 Transition          : 0y0
            +0x000 UsedPageTableEntries : 0y0000000000 (0)
            +0x000 InStore             : 0y0
            +0x000 Reserved            : 0y000000000 (0)
            +0x000 PageFileHigh        : 0y00000000000000000101000101001101 (0x514d)
0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff880`047401b0 fffff800`0459ae1c     nt!MiUpdatePfnBackingStore+0x85
01 fffff880`047401e0 fffff800`0445eb09     nt!MiBuildPageFileCluster+0x87c
02 fffff880`04740ac0 fffff800`0445f3eb     nt!MiGatherPagefilePages+0x269
03 fffff880`04740ba0 fffff800`0476ccce     nt!MiModifiedPageWriter+0x1bb
04 fffff880`04740c00 fffff800`044c0fe6     nt!PspSystemThreadStartup+0x5a
05 fffff880`04740c40 00000000`00000000     nt!KiStartSystemThread+0x16
0: kd> bp nt!MiUpdatePfnBackingStore+0x85 ".if(@$t0<4){.printf \"hit #%d\n\",@$t0;r @$t0=@$t0+1;dt nt!_mmpfn OriginalPte.u.Soft.PageFileHigh @rbx;g};.else{}"
0: kd> r @$t0=0
0: kd> g
hit #0    +0x020 OriginalPte                     : 
      +0x000 u                               : 
         +0x000 Soft                            : 
            +0x000 PageFileHigh                    : 0y00000000000000000101000101010011 (0x5153)
hit #1    +0x020 OriginalPte                     : 
      +0x000 u                               : 
         +0x000 Soft                            : 
            +0x000 PageFileHigh                    : 0y00000000000000000101000101010100 (0x5154)
hit #2    +0x020 OriginalPte                     : 
      +0x000 u                               : 
         +0x000 Soft                            : 
            +0x000 PageFileHigh                    : 0y00000000000000000101000101010101 (0x5155)
hit #3    +0x020 OriginalPte                     : 
      +0x000 u                               : 
         +0x000 Soft                            : 
            +0x000 PageFileHigh                    : 0y00000000000000000101000101010110 (0x5156)

3.2.7 将Active页面写入分页文件

#### 基本概念

在进行上一节的分析时,有可能观察到MPW处理有效的PTEs,即主动映射VAPTEs。这可能是为了处理这样一种情况:MPW发现一个连续的Modified页块,其中夹杂着一些Active页。如果MPW将自己限制在严格的写入Modified页,对于分页文件的分散区域它将不得不发出多次写入。通过同时写入Active页面,MPW可以执行一次写入操作,并为整个页面范围分配一个连续的分页文件块。

VMM认为已经写入分页文件的Active页是干净的:它可以被移到Standby列表中,并最终被重新利用。这在与该页相关的一些控制位中有记录。PTE会是这样的:

_MMPFN.u.Hard.Valid = 1

_MMPFN.u.Hard.Dirty1 = 0

_MMPFN.u.Hard.Dirty = 0

_MMPFN.u.Hard.Write = 1

注意,Dirty1是第1位,即处理器的硬件R/W位;清除它意味着将该页设置为只读,因此,对该页的写入将导致页面错误。我们将在后面讨论这个问题,但是我们可以理解,这是VMM在页面被修改和分页文件中的拷贝变得过时时进行控制的一种方式。

Dirty是实际的处理器脏位,被清0后表示页面是干净的。

Write被处理器忽略,并被VMM用来识别该页是可写的。对该页的第一次写入将导致页面错误,因为Dirty1是清零的,但是VMM将从Writable的设置中知道该页实际上是可写的。

_MMPFN的情况是这样的:

_MMPFN.u3.e1.Modified = 0

当一个页面第一次被映射以处理demand-zero错误时,这三个位都被设置为1,因为页面开始时是 "脏 "的;现在我们已经看到改变其状态为clean的事件之一。

另一个可能更常见的导致相同状态的事件是当Standby列表上的一个页面被faulted back到工作集时。

#### 来自调试会话的证据

本节旨在为上一节所解释的概念提供证据,因此读者可以选择跳过这一节。

为了在MPW写入Active页面时捕获它,我们在MiBuildPageFileCluster+0x62e处再次设置断点,在这里rbprcx都存储了即将写入的页面的_MMPFN地址。这一次,我们设置了一个条件断点,查看rbp->u3.e1.PageLocation的值。当它不是_MMLISTS.ActiveAndValid(即6)时恢复执行。这样的断点可以通过以下命令来设置:

0: kd> bp /t fffffa8030f55680 nt!MiBuildPageFileCluster+0x62e "j ((poi(@rbp+1a)&7) == 6) ''; 'gc'"`

当调试器断下是rbp存储的就是_MMPFN的地址:

nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)
0: kd> r
rax=000000012a5a4003 rbx=bb00000097a41805 rcx=fffffa8001c6ec30
rdx=0000000000000001 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8000459abce rsp=fffff880047401e0 rbp=fffffa8001c6ec30
 r8=0000000000000002  r9=0000000000000000 r10=000000012a5a4002
r11=000000012a5a7000 r12=0000000000000013 r13=0000000000000001
r14=fffff68000952d38 r15=0000000000097a41
iopl=0         nv up ei ng nz ac po cy
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000297
nt!MiBuildPageFileCluster+0x62e:
fffff800`0459abce e88d45ecff      call    nt!MiReferencePageForCluster (fffff800`0445f160)

可以看到MPW附加在MemTests.exe上:

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000dd4050
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032a689a0       Image:         MemTests.exe
Wait Start TickCount      93997          Ticks: 6 (0:00:00:00.093)
Context Switch Count      5277           IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:11.216
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473fed0
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3248d7c8 fffffa80`00000000 00000000`0003e8e2 fffffa80`00000100 : nt!MiBuildPageFileCluster+0x62e
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0003e8e2 00000000`00000000 fffffa80`0003e8e2 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

通过查看_MMPFN,我们可以看到PageLocation是6,然后PTE的地址存放在PteAddress里:

0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. @rbp
   +0x010 PteAddress     : 0xfffff680`00952d39 _MMPTE
   +0x018 u3             : 
      +0x002 e1             : 
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y0
         +0x000 Modified       : 0y1
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    : 
      +0x000 u              : 
         +0x000 Long           : 0x80
         +0x000 VolatileLong   : 0x80
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

PTE地址的第0位被置1了,说明他正在作为锁位独占访问_MMPFN。由于MPW附加在进程,后者的地址空间是活动的,所以我们可以检查PTE的内容:

0: kd> !pte 0xfffff680`00952d38
                                           VA 000000012a5a7000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00020    PDE at FFFFF6FB40004A90    PTE at FFFFF68000952D38
contains 02B0000014377867  contains 02C0000022D78867  contains 1B5000006979F867  contains BB00000097A41805
pfn 14377     ---DA--UWEV  pfn 22d78     ---DA--UWEV  pfn 6979f     ---DA--UWEV  pfn 97a41     -------UR-V

PTE的值对应着一个有效PTE

_MMPTE.u.Hard.Valid = 1

_MMPTE.u.Hard.Dirty1 = 0

_MMPTE.u.Hard.Dirty = 0

_MMPTE.u.Hard.Write = 1

这证实了MPW正在向分页文件写入一个Active页,也表明它已经将PTE标记为干净,这意味着将其设置为只读(对处理器而言)。

我们可以在这个页面被写入分页文件时进一步跟踪它。第一步是在_MMPFN.OriginalPte设置一个写访问断点,即MPW要写入分页文件的偏移量的时候。我们在MiUpdatePfnBackingStore+0x85处停止,_MMPFN地址为在rbx中。

0: kd> r
rax=0003e8f500000000 rbx=fffffa8001c6ec30 rcx=0003e8f500000080
rdx=0000000000000000 rsi=000000000003e8f5 rdi=0000000000000000
rip=fffff8000445f125 rsp=fffff880047401b0 rbp=0000000000000000
 r8=000000000003e8f5  r9=0000000000000013 r10=000000012a5a40ee
r11=000000012a693000 r12=0000000000000000 r13=0000000000000001
r14=fffff68000953498 r15=fffffa803248d890
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
nt!MiUpdatePfnBackingStore+0x85:
fffff800`0445f125 f0836310fe      lock and dword ptr [rbx+10h],0FFFFFFFEh ds:002b:fffffa80`01c6ec40=00952d39

_MMPFN的设置情况如下:

0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. @rbx
   +0x010 PteAddress     : 0xfffff680`00952d39 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y1
         +0x000 Modified       : 0y0
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x0003e8f5`00000080
         +0x000 VolatileLong   : 0x0003e8f5`00000080
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

我们可以看到,现在Modified是0,WritelnProgress是1。后者在MPW释放了_MMPFN上的锁后仍将保持设置,用来表示写仍在继续。OriginalPte现在包括分页文件的偏移量。因为OriginalPte是一个奇数,所以_MMPFN仍然被锁定。PTE我们没有去查看,但它是没有变化。下面是MPW的状态。

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000dd4050
Owning Process            fffffa8030f23040       Image:         System
Attached Process          fffffa8032a689a0       Image:         MemTests.exe
Wait Start TickCount      94021          Ticks: 0
Context Switch Count      5280           IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:11.356
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473fed0
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`047401b0 fffff800`0459ae1c     : 00000000`00000013 00000580`00000000 00000000`00000000 00000000`0003e8f5 : nt!MiUpdatePfnBackingStore+0x85
fffff880`047401e0 fffff800`0445eb09     : fffffa80`3248d7c8 fffffa80`00000000 00000000`0003e8e2 fffffa80`00000100 : nt!MiBuildPageFileCluster+0x87c
fffff880`04740ac0 fffff800`0445f3eb     : fffffa80`0003e8e2 00000000`00000000 fffffa80`0003e8e2 00000000`00000000 : nt!MiGatherPagefilePages+0x269
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`047406d0 00000000`00000000 : nt!KiStartSystemThread+0x16

在返回地址上下断,即nt!MiBuildPageFileCluster+0x87c处,我们能看到MPW返回到了他的调用者:

0: kd> bp fffff800`0459ae1c
0: kd> g
Breakpoint 5 hit
nt!MiBuildPageFileCluster+0x87c:
fffff800`0459ae1c 4103dd          add     ebx,r13d

我们可以看到,现在_MMPFN被解锁了(PteAddress的第0位被清除),并且仍然设置了WritelnProgress。其他VMM组件可以访问它,但他们会知道该页有一个I/O正在进行中。

0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. fffffa8001c6ec30
   +0x010 PteAddress     : 0xfffff680`00952d38 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y1
         +0x000 Modified       : 0y0
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x0003e8f5`00000080
         +0x000 VolatileLong   : 0x0003e8f5`00000080
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

通过对栈上的返回地址下断,我们最终会停在nt!MiModifiedPageWriter+0x1bb

0: kd> bp fffff800`0445f3eb
0: kd> g
Breakpoint 5 hit
nt!MiBuildPageFileCluster+0x87c:
fffff800`0459ae1c 4103dd          add     ebx,r13d
0: kd> g
Breakpoint 5 hit
nt!MiBuildPageFileCluster+0x87c:
fffff800`0459ae1c 4103dd          add     ebx,r13d
0: kd> bd 5
0: kd> g
Breakpoint 6 hit
nt!MiModifiedPageWriter+0x1bb:
fffff800`0445f3eb 833d4a0d2a0000  cmp     dword ptr [nt!MmSystemShutdown (fffff800`0470013c)],0
0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
IRP List:
    fffffa80322c5820: (0006,0358) Flags: 00060003  Mdl: fffffa803248d7c8
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      94047          Ticks: 1 (0:00:00:00.015)
Context Switch Count      5281           IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:11.512
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473ff50
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`0473ff50 00000000`00000000 : nt!KiStartSystemThread+0x16

到这里时,_MMPFNWriteInProgress 依旧是 1和解锁状态。

0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. fffffa8001c6ec30
   +0x010 PteAddress     : 0xfffff680`00952d38 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y1
         +0x000 Modified       : 0y0
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x0003e8f5`00000080
         +0x000 VolatileLong   : 0x0003e8f5`00000080
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

WritelnProgress仍为1时,MPW已继续向前执行并返回其顶层函数,这一事实告诉我们,MPW发出了一个异步I/O写,并在操作过程中继续执行代码。

我们可以通过在我们的_MMPFN实例上设置一个写访问u3.e1的断点,在MPW清除WritelnProgress时捕捉它,来观察写的最后步骤。当断点被击中时,rbx被设置为_MMPFN的地址:

0: kd> ba w 8 /t fffffa8030f55680 fffffa8001c6ec30+18
0: kd> g
Breakpoint 7 hit
nt!MiWriteCompletePfn+0x43:
fffff800`045094bb 0fb74318        movzx   eax,word ptr [rbx+18h]
0: kd> r
rax=0000000000000001 rbx=fffffa8001c6ec30 rcx=fffffa8001c6ec30
rdx=0000000000000000 rsi=fffffa803248dff8 rdi=0000000000000001
rip=fffff800045094bb rsp=fffff8800473fce0 rbp=fffffa803248d898
 r8=fffff80004641e80  r9=0000000000000100 r10=fffff80004641e80
r11=fffff8800473fc98 r12=0000000000000000 r13=0000000000000002
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
nt!MiWriteCompletePfn+0x43:
fffff800`045094bb 0fb74318        movzx   eax,word ptr [rbx+18h] ds:002b:fffffa80`01c6ec48=0002

线程的状态如下:

0: kd> !thread @$thread
THREAD fffffa8030f55680  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8030f23040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      94047          Ticks: 11 (0:00:00:00.171)
Context Switch Count      5281           IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:11.544
Win32 Start Address nt!MiModifiedPageWriter (0xfffff8000445f230)
Stack Init fffff88004740c70 Current fffff8800473ff50
Base fffff88004741000 Limit fffff8800473b000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`0473fce0 fffff800`045099b4     : 0000068d`00000001 fffffa80`00000000 00000000`00000000 00000000`046597d4 : nt!MiWriteCompletePfn+0x43
fffff880`0473fd50 fffff800`044b9297     : fffffa80`3248d760 41764d56`00000000 00000038`00000000 fffffa80`3248d890 : nt!MiWriteComplete+0x1b4
fffff880`0473fe10 fffff800`044c2cf7     : fffffa80`30f55680 fffffa80`30f556d0 577df9b8`d686ef5a 00010000`00010000 : nt!IopCompletePageWrite+0x57
fffff880`0473fe40 fffff800`044c2fa7     : fffff880`04740101 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiDeliverApc+0x1c7
fffff880`0473fec0 fffff800`0456b7db     : fffff880`047409e0 00000000`00000000 00000000`00000001 00000000`00000000 : nt!KiApcInterrupt+0xd7 (TrapFrame @ fffff880`0473fec0)
fffff880`04740050 fffff800`049506aa     : 00030010`00000000 7500002a`00000001 850021f0`00000001 fffff880`04740101 : nt!KeThawExecution+0x26b
fffff880`047400d0 fffff800`04530941     : fffff880`04740100 fffff880`04740968 fffff800`04641e80 fffffa80`30f55680 : nt!KdExitDebugger+0x7a
fffff880`04740100 fffff800`0494f03b     : fffff880`047401c0 fffff880`047406c0 fffff880`04740a10 fffff800`044f7ac5 : nt! ?? ::FNODOBFM::`string'+0x19d81
fffff880`04740140 fffff800`0450adb2     : fffff880`04740968 fffff880`04740a10 fffff880`04740a10 fffffa80`3248d760 : nt!KdpTrap+0x2b
fffff880`04740190 fffff800`044cecc2     : fffff880`04740968 fffffa80`30f55680 fffff880`04740a10 fffffa80`30f23040 : nt!KiDispatchException+0x126
fffff880`04740830 fffff800`044ccaf4     : fffffa80`31700cb0 fffffa80`324cd370 fffffa80`31700c00 fffffa80`322c5820 : nt!KiExceptionDispatch+0xc2
fffff880`04740a10 fffff800`0445f3ec     : fffffa80`0003e9e2 00000000`00000000 fffffa80`0003e8e2 00000000`00000000 : nt!KiBreakpointTrap+0xf4 (TrapFrame @ fffff880`04740a10)
fffff880`04740ba0 fffff800`0476ccce     : fffffa80`30f55680 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bc
fffff880`04740c00 fffff800`044c0fe6     : fffff800`04641e80 fffffa80`30f55680 fffff800`0464fcc0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04740c40 00000000`00000000     : fffff880`04741000 fffff880`0473b000 fffff880`0473ff50 00000000`00000000 : nt!KiStartSystemThread+0x16

当前的线程是fffffa8030f55680  也就是MPW。他已经没有附加任何进程,并且正在从KiDeliverApc调用的函数中更新WritelnProgress。这意味着I/O完成是由APC发出信号的,这是I/O管理器的正常行为。不幸的是,调用堆栈有点脏,有一个由KiBreakpointTrap创建的陷阱帧,这似乎是调试器导致的。这可能是由于在调试器从上一个断点返回控制权之前,写操作引发了APC中断,所以我们看到APC中断的同时线程上下文正在被恢复。

现在_MMPFN又被锁住了,因为MPW正在更新的他成员,而且此时WriteInProgress是0.

0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. fffffa8001c6ec30
   +0x010 PteAddress     : 0xfffff680`00952d39 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y0
         +0x000 Modified       : 0y0
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x0003e8f5`00000080
         +0x000 VolatileLong   : 0x0003e8f5`00000080
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

如果我们沿着堆栈上保存的返回地址停下来,我们看到,当我们到达IopCompletePageWrite+0x57时,_MMPFN已经被解锁:

Breakpoint 8 hit
nt!IopCompletePageWrite+0x57:
fffff800`044b9297 488b5c2430      mov     rbx,qword ptr [rsp+30h]
0: kd> dt nt!_mmpfn u3.e1. PteAddress OriginalPte.u. fffffa8001c6ec30
   +0x010 PteAddress     : 0xfffff680`00952d38 _MMPTE
   +0x018 u3             :
      +0x002 e1             :
         +0x000 PageLocation   : 0y110
         +0x000 WriteInProgress : 0y0
         +0x000 Modified       : 0y0
         +0x000 ReadInProgress : 0y0
         +0x000 CacheAttribute : 0y01
         +0x001 Priority       : 0y101
         +0x001 Rom            : 0y0
         +0x001 InPageError    : 0y0
         +0x001 KernelStack    : 0y0
         +0x001 RemovalRequested : 0y0
         +0x001 ParityError    : 0y0
   +0x020 OriginalPte    :
      +0x000 u              :
         +0x000 Long           : 0x0003e8f5`00000080
         +0x000 VolatileLong   : 0x0003e8f5`00000080
         +0x000 Hard           : _MMPTE_HARDWARE
         +0x000 Flush          : _HARDWARE_PTE
         +0x000 Proto          : _MMPTE_PROTOTYPE
         +0x000 Soft           : _MMPTE_SOFTWARE
         +0x000 TimeStamp      : _MMPTE_TIMESTAMP
         +0x000 Trans          : _MMPTE_TRANSITION
         +0x000 Subsect        : _MMPTE_SUBSECTION
         +0x000 List           : _MMPTE_LIST

我们可以去看PTE的最终状态,但我们必须将进程上下文设置为MPW最初附加的MemTests.exe实例,因为现在,它已经脱离了。

0: kd> .cache forcedecodeptes

Max cache size is       : 1048576 bytes (0x400 KB)
Total memory in cache   : 0 bytes (0 KB)
Number of regions cached: 0
0 full reads broken into 0 partial reads
    counts: 0 cached/0 uncached, 0.00% cached
    bytes : 0 cached/0 uncached, 0.00% cached
** Transition PTEs are implicitly decoded
** Virtual addresses are translated to physical addresses before access
** Prototype PTEs are implicitly decoded
0: kd> .process fffffa8032a689a0
Implicit process is now fffffa80`32a689a0
0: kd> !pte 0xfffff680`00952d38
                                           VA 000000012a5a7000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00020    PDE at FFFFF6FB40004A90    PTE at FFFFF68000952D38
contains 02B0000014377867  contains 02C0000022D78867  contains 1B5000006979F867  contains BB00000097A41805
pfn 14377     ---DA--UWEV  pfn 22d78     ---DA--UWEV  pfn 6979f     ---DA--UWEV  pfn 97a41     -------UR-V

PTE确实是没有改变的。

总之,我们观察到一个active modifed页面被MPW处理,进入WritelnProgress = 1的Tansition状态,最后成为一个干净的页面,其分页文件的偏移量被保存在OriginalPte中。

3.2.8 关于在写入过程中的并发页面访问的说明

当写I/O正在进行时,系统中的其他一切仍在进行,所以其他线程可以引用正在写入分页文件的页面。这种情况将在后续描述。

4 Standby 页面的重复利用

以前我们说过,一个页面可以从Standby列表中取出并重新使用,以解决来自不同地址空间的页面错误,这个过程称为repurposing.。因此,通过跟踪一个页面进入Standby状态所经历的转换,我们已经涵盖了它的生命周期,直到它可以被重新使用。

然而,Standby并不是一个页面仅仅等待被重新利用的状态。处于这种状态的页可以被返回到它们被移除的地址空间--我们称之为软错误。下一章将讨论这种情况,以及Modified页的软故障。

5 软错误

在前几章中,我们看到了一个物理页是如何第一次被添加到地址空间中的;当它被移除并进入Modified状态时会发生什么;它是如何从Modified状态移到Standby状态的。

在我们对页的生命周期的分析中,下一个合乎逻辑的步骤是描述当一个页从StandbyModified状态返回到地址空间时会发生什么,这个事件被称为软错误,因为它开始时是一个页错误,但在不读分页文件的情况下被解决。

需要记住的是,一个页面被移到StandbyModified,他的PTE在第12-48位仍有页面PFN,所以VMM可以很容易地访问该页面的_MMPFN

5.1 来自Standby状态页的软故障

这种状态转换在下图中用粗箭头表示:

Untitled 9.png

该错误可能是由于试图从该页读出或写入该页而引起的,这导致VMM采取不同的行动。

5.1.1 读取访问

涉及的VMM函数是MmAccessFaultMiDispatchFaultMiResolveTransitionFault,其工作是:

  • Standby链表中取消对_MMPFN的链接
  • 更新_MMPFN的内容,以表示该页面是Active的。
  • 更新PTE,使其将VA映射到物理页上
  • 在工作集链表中添加该页的项。

最后的PTE值必须用_MMPTE.u.Hard成员来解释,因为它是一个有效的PTE,而且它有:

_MMPTE.u.Hard.Valid = 1

_MMPTE.u.Hard.Dirty1 = 0

_MMPTE.u.Hard.Accessed = 1

_MMPTE.u.Hard.Dirty = 0

_MMPTE.u.Hard.Write = 1

Dirty1DirtyWrite的值与干净页面的值相同。这是值得注意的事情:当一个页面因为读访问而从Standby状态下faulted back时,它是干净的,因为它的有效副本存在于分页文件中。如果VMM决定削减工作集,这样的页面可以从地址空间中删除,并直接放入Standby状态,而不需要经过Modified,也不需要再次写入分页文件。在这里Accessed被设置为1,这与该页被faulted back的事实是一致的,因为对它的访问已被尝试过。

最终页面的_MMPFN是这样的:

_MMPFN.u1.WslIndex = 工作集索引项

_MMPFN.u1.ShareCount = 1

_MMPFN.u3.ReferenceCount = 1

_MMPFN.u3.e1.PageLocation = _MMLIST.ActiveAndValid,即 6

_MMPFN.u3.e1.Modified = 0,和干净的页面一样

_MMPFN.OriginalPte.u.Soft.Protection 设置为VMM定义的保护掩码

_MMPFN.OrignalPte.u.Soft.PageFileHigh = 分页文件中的偏移量,以页为单位,即1表示从字节0开始的1 x 4096字节。

值得注意的是,分页文件中的偏移量是如何被保留在_MMPFN中的,它记录了页面拷贝的位置,并使得将这样的页面直接移动到Standby状态成为可能。相比之下,一个由demand-zero错误添加到工作集的新页是脏的,并且PageFileHigh被设置为0。

5.1.2 写入访问

当错误访问是一个内存写入时,它的处理方式类似,但有几个重要的区别。

_MMPFN.OriginalPte.u.Soft.PageFileHigh被设置为0,这意味着物理页与它在分页文件中的副本脱离了关系,而分页文件即将过时了。扔掉PageFileHigh意味着VMM甚至没有记录拷贝位置,也就是说,这个页面的分页文件槽被释放了。这种行为的进一步证据是MiResolveTransitionFault对一个名为MiReleaseConfirmedPageFileSpace的函数的调用,rcx被设置为OriginalPte,并带有PageFileHigh的旧值:可能,这个函数更新保持记录分页文件哪些部分的数据结构正在被使用。

最终的PTE是这样的:

_MMPTE.u.Hard.Valid = 1

_MMPTE.u.Hard.Dirty1 = 1

_MMPTE.u.Hard.Accessed = 1

_MMPTE.u.Hard.Dirty = 1

_MMPTE.u.Hard.Write = 1

Dirty1= 1意味着处理器允许对该页进行写入,所以在写入时不会出现页面错误,VMM不会重新获得控制权。这不是一个问题,因为VMM已经在页面变脏的情况下持续更新了它的数据。

最后的_MMPFN和读的例子类似,只是有下面些许差别:

_MMPFN.u3.e1.Modified = 1

_MMPFN.OriginalPte.u.Soft.PageFileHigh = 0

PTE_MMPFN的设置与demand-zero fault的情况一样:该页已经失去了它的分页文件副本,所以它就像刚刚分配的一样。

5.2 来自Modified 页面的软错误

Modified list中的一个页面在Modiifed页面写入者(MPW)将其内容写入分页文件之前被引用时,就会发生这种转换,在下图中用粗箭头表示:

Untitled 10.png

由于该页已被修改,还没有为其分配分页文件空间,所以_MMPFN.u.Soft.OriginalPte.PageFileHigh在错误发生时被设置为0,并且在之后保持不变。该页变成Active,保留了它的dirty状态,最后的PTE_MMPFN如下所示

_MMPTE.u.Hard.Valid = 1

_MMPTE.u.Hard.Dirty1 = 1

_MMPTE.u.Hard.Accessed = 1

_MMPTE.u.Hard.Dirty = 1

_MMPTE.u.Hard.Write = 1

_MMPFN.u1.WslIndex = 工作集索引项

_MMPFN.u1.ShareCount = 1

_MMPFN.u3.ReferenceCount = 1

_MMPFN.u3.e1.PageLocation = _MMLIST.ActiveAndValid,即 6

_MMPFN.u3.e1.Modified = 0,和脏页面一样

_MMPFN.OriginalPte.u.Soft.Protection 设置为VMM定义的保护掩码

_MMPFN.OrignalPte.u.Soft.PageFileHigh = 0

另外,MiReleaseConfirmedPageFileSpace没有被调用。对页面的读和写访问的行为都是一样的。对于一个Standby页,写需要额外的处理,因为该页从干净的变成了脏的,但是一个Modified页首先就已经是脏的。

免费评分

参与人数 4吾爱币 +5 热心值 +4 收起 理由
txm000 + 1 + 1 热心回复!
sam喵喵 + 1 + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
junjia215 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

ljzbox 发表于 2022-4-11 13:35
一头雾水的进来 一头雾水的出去
IBinary 发表于 2022-4-11 14:59
Georgia 发表于 2022-4-11 15:03
wwwfyw 发表于 2022-4-25 17:13
感谢楼主分享
hu881013 发表于 2022-5-1 20:03
一头雾水的进来 一头雾水的出去
wulei1873 发表于 2022-5-2 01:42
大神请接受膜拜
bbgdlk 发表于 2022-5-6 10:56
看VT  VMM  VMX 真头大
jsncy 发表于 2022-5-15 19:15
学到了,谢谢分享。
cscscscs 发表于 2022-7-24 21:12
超出知识范围了看不懂了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-5-5 18:21

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表