吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8538|回复: 23
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
BeneficialWeb 发表于 2022-4-29 17:29 回帖奖励

用户范围内存管理 Part3

1. 访问一个Write in Progress的页面

因为把一个页面写入分页文件需要花费一定的时间,那么就可能存在线程访问这个页面时,他正处于写入分页文件的过程中,因此我们接下来我们看看VMM是怎么处理这种情况的。

涉及的主要函数是MiWriteCompleteMiWriteCompletePfn,它们在写操作完成时被APC触发调用。无论页面是否被重新访问,MiWriteCompletePfn都会清除_MMPFN.u3.e1.WritelnProgress,它是由Modified页面写入者在一开始时设置的。

1.1 访问的是Modifed页面

1.1.1 读取访问

可以观察到无论是否正在进行的写操作,Modified页面都可以被软错误返回其地址空间:由于其内容仍然有效,没有理由让错误线程继续等待。只是当写入完成后,VMM会做一些不同的事情。

MiWriteCompletePfn查看页面引用计数(_MMPFN.u3. ReferenceCount)以检查该页面是否被访问。当Modified页面写入器开始写入时,它增加了这个引用计数。如果该页没有被其他任何人引用过,则计数为1;如果它已经被映射到一个地址空间,则计数变为2。

MiWriteCompletePfn会递减引用计数,只有当它下降到0时才会将页面链到Standby list中,调用MiPfnReferenceCountlsZero来完成这个工作。当这种情况发生时,_MMPFN.u3.e1.PageLocation也被设置为_MMLISTS.StandbyPageList

另一方面,如果最终的引用计数不是0,MiWriteCompletePfn会跳过对MiPfnReferenceCountlsZero的调用,这意味着用于链接到链表的_MMPFN成员(u1u2)以及u3.e1.PageLocation都没有变化。值得注意的是,这些成员必须存储与被映射的页面一致的值:

  • _MMPFN.u1.WsIndex 必须设置为工作集索引
  • _MMPFN.u2.ShareCount 必须设置为1 (对于私有内存是这样的,共享内存我们以后再提)
  • _MMPFN.u3.e1.PageLocation 必须设置为 _MMLIST.ActiveAndValid

这些值是由VMM设置的,同时它处理软错误,让页面被重新映射,并在写入完成后简单地保持不变。

此外,对于内存的读取访问,复制到分页文件中的页面仍然有效:该页面再次被映射到一个地址空间中,但它是干净的,而且将其写入分页文件的努力也没有白费。MiWriteCompletePfn并不更新
_MMPFN.u.Soft.PagingFileHigh,这是在开始写的时候已经存储的分页文件偏移。

1.1.2 写访问

这种情况必须以不同的方式处理,因为分页文件中的页面拷贝不再有效:没有办法知道写操作是在更新之前还是之后存储了页面内容。

MiWriteComplete能检测到这种情况,因为它发现了_MMPFN.u3.e1.Modified置1了。MPW设置写操作时在MiUpdatePfnBackingStore里把这个位被清0了;如果后来发现它被置1了,这意味着在此期间已经处理了一个对页面的写访问错误。当这种情况发生时,MiWriteComplete调用MiWriteCompletePfn,第二个参数设置为2,这使得后者的函数调用MiReleaseConfirmedPageFileSpace并设置_MMPFN..OriginalPte.u.Soft.PageFileHigh 为0,这就释放了该页的分页文件槽,并使_MMPFN与脏页情况相一致。在其他方面,MiWriteCompletePfn的行为与读访问的情况一样。

当页面错误处理例程检测到是对这个页面的写访问时会设置_MMPFN.u3.e1.Modified,但不能调用MiReleaseConfirmedPageFileSpace,因为写入仍在进行中,所以这最后一步留给了写入完成代码。

1.2 访问的是Active 页面 - 为什么Clean页面是只读的一个原因

以前我们看到脏页在Active时可以被写入分页文件,即使它们没有被移到Modified链表中。

我们现在可以更好地理解MPW是如何将这些写访问与Modified页面的写访问区分开来的:当写入分页文件正在进行时,Active页面的引用计数被设置为2,当完成写入分页文件时,它将下降到1,而不是0。MiCompleteWritePfn不会调用MiPfnReferenceCountlsZero,该页也不会被链接到Standby链表中。

这些写操作面临着与Modifed页写访问相同的问题,即在写入分页文件的过程中可以访问该页。

读取访问实际上不需要任何特殊的处理:写入的副本是有效的,并且页面是activeclean的。页面的写入分页文件的过程不会影响页面被读取。

然而,写访问必须导致分页文件副本被丢弃,就像发生在Modified页面一样,所以VMM必须能够检测到这样的访问。但是,这种情况和Modified页面有一个明显的区别:后者的PTE是无效的(P=0),所以对该页面的任何访问都会导致页面错误。这意味着VMM可以控制并检查fault错误代码,检测写访问并采取相应行动。

另一方面,Active页有一个有效的PTE,所以,一般来说,对该页的访问不会引起错误,VMM没有办法控制这种情况。这就是将clean页面的PTE设置为只读的原因:对该页面的写入将导致页面错误,不是因为PTE无效,而是因为就处理器而言,该页面不能被写入。

MPW在设置页面写入时,清除了_MMPTE.u.Hard.Dirty1,它实际上是处理器的R/W位,在设置页面写入时就提前清空了,所以当写入分页文件正在进行时,页面已经被标记为只读,一个写入访问会导致了页面错误,VMM有机会设置_MMPFN.u3.e1.Modified,这将通知MiWriteCompleteMPW的努力已经白费了,必须放弃写入。注意,_MMPFN.u3.e1.Modified在设置写入时被clear,所以如果页面没有被写入,那么MiWriteComplete会发现它是clear的。

2 Active Clean页面状态的变化以及PTE的Dirty位

在完成了对一个Active页面如何变clean的分析之后,我们现在可以考虑一个页面可能经历这样一个转换。

2.1 从工作集里移除Clean页面

一个clean页面可以像一个dirty页面一样从WS中删除。这种转换在下图中用粗的箭头表示:

与移除一个dirty页相比,主要区别在于该页直接进入Standby状态。

以前我们讲过Standby页面的PTE_MMPFN,在这里也是适用的,只是说页面不会经理Modified状态,因为他是clean的。

2.2 向Clean Active页面写入

对一个clean页面的写访问会使该页面dirty。我们现在知道,一个clean页面有一个处理器R/W位清零的PTE,标志着它是只读的,因此写会导致页面错误,VMM会拦截到这个错误。

VMM做的主要事情是释放页面的分页文件副本,因为它即将变为无效的。这相当于调用MiReleaseConfirmedPageFileSpace并设置_MMPFN.u.Soft.OriginalPte.PageFileHigh等于0。之后,VMM设置PTE让这个页面允许写入,并重新执行错误指令。

我们可以通过分配一块内存,然后再分配另一块内存来迫使VMM将第一块内存分页出来,之后再读取第一块内存让他从分页文件中回来。这样就能观察到VMM的工作行为。通过这种方式,我们最终得到了一个Active Clean页面,并且可以看到当我们向它写的时候会发生什么。在执行写操作之前,我们在_MMPFN.OriginalPte上设置了一个写入断点,在MiReleaseConfirmedPageFileSpace上也设置一个断点,注意限制在我们的测试进程中(这个函数会被系统的很多线程执行)。然后我们尝试对该页面进行写访问。

下面是实验环境记录

虚拟机 4GB
size 2886729728

MPW fffffa80`30f55680
p1
000000007FFF0000 --  000000012C0F0000
2147418112 -- 5034147840

read

p2
13FAB0000-00000001EBBB0000
read

MemTests.exe fffffa8032635b30

0: kd> !process 0 0 MemTests.exe
PROCESS fffffa8032635b30
    SessionId: 1  Cid: 0a8c    Peb: 7fffffd9000  ParentCid: 07cc
    DirBase: 146bf000  ObjectTable: fffff8a002cb2010  HandleCount:  11.
    Image: MemTests.exe

0: kd> .process /i fffffa8032635b30
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`044c7490 cc              int     3
0: kd> !pte 000000007FFF0000
                                           VA 000000007fff0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001FF8    PTE at FFFFF680003FFF80
contains 00C00000ADEC8867  contains 0280000012ED7867  contains 0A6000000C9E6867  contains 9610000032565805
pfn adec8     ---DA--UWEV  pfn 12ed7     ---DA--UWEV  pfn c9e6      ---DA--UWEV  pfn 32565     -------UR-V

0: kd> !pfn 32565
    PFN 00032565 at address FFFFFA80009702F0
    flink       00044961  blink / share count 00000001  pteaddress FFFFF680003FFF80
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 591100000080  containing page 00C9E6  Active             

0: kd> ba w 8 FFFFFA80009702F0+20
0: kd> bp /p fffffa8032635b30 nt!MiReleaseConfirmedPageFileSpace

我们的程序断在了写入_MMPFN.OriginalPte的地方

Breakpoint 0 hit
nt! ?? ::FNODOBFM::`string'+0x46ba6:
fffff800`04462bc1 f0836110fe      lock and dword ptr [rcx+10h],0FFFFFFFEh
1: kd> !process @$proc
PROCESS fffffa8032635b30
    SessionId: 1  Cid: 0a8c    Peb: 7fffffd9000  ParentCid: 07cc
    DirBase: 146bf000  ObjectTable: fffff8a002cb2010  HandleCount:  11.
    Image: MemTests.exe
    VadRoot fffffa8032948860 Vads 43 Clone 0 Private 1412461. Modified 1487. Locked 0.
    DeviceMap fffff8a000dd4050
    Token                             fffff8a002c95a90
    ElapsedTime                       00:08:09.831
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         22672
    QuotaPoolUsage[NonPagedPool]      5040
    Working Set Sizes (now,min,max)  (694200, 50, 345) (2776800KB, 200KB, 1380KB)
    PeakWorkingSetSize                962530
    VirtualSize                       5517 Mb
    PeakVirtualSize                   5517 Mb
    PageFaultCount                    2117494
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      1412478

        THREAD fffffa8032724980  Cid 0a8c.0494  Teb: 000007fffffde000 Win32Thread: 0000000000000000 RUNNING on processor 1
        Not impersonating
        DeviceMap                 fffff8a000dd4050
        Owning Process            fffffa8032635b30       Image:         MemTests.exe
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      203227         Ticks: 0
        Context Switch Count      48701          IdealProcessor: 1
        UserTime                  00:00:00.390
        KernelTime                00:00:09.250
        Win32 Start Address 0x000000013fa97d1c
        Stack Init fffff8800b179c70 Current fffff8800b179530
        Base fffff8800b17a000 Limit fffff8800b174000 Call 0000000000000000
        Priority 11 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
        Child-SP          RetAddr               Call Site
        fffff880`0b179980 fffff800`044cd76e     nt! ?? ::FNODOBFM::`string'+0x46ba6
        fffff880`0b179ae0 00000001`3fa9173f     nt!KiPageFault+0x16e (TrapFrame @ fffff880`0b179ae0)
        00000000`0022fc10 00000001`3fa998c0     0x00000001`3fa9173f
        00000000`0022fc18 000007fe`f9943400     0x00000001`3fa998c0
        00000000`0022fc20 00000000`0022df18     0x000007fe`f9943400
        00000000`0022fc28 00000000`003f3c79     0x22df18
        00000000`0022fc30 00000000`0022fc68     0x3f3c79
        00000000`0022fc38 00000000`0022fc70     0x22fc68
        00000000`0022fc40 00000000`7fff0000     0x22fc70
        00000000`0022fc48 00000001`2c0f0000     0x7fff0000
        00000000`0022fc50 00000001`00000035     0x00000001`2c0f0000
        00000000`0022fc58 00000001`00000001     0x00000001`00000035
        00000000`0022fc60 00350006`00350001     0x00000001`00000001
        00000000`0022fc68 00000000`00000000     0x00350006`00350001

接下来让我们看看_MMPFN,我们看到_MMPFN.OriginalPte.u.Soft.PageFileHigh被设置为0。这个阶段的PTE还是只读的。

1: kd> dt nt!_MMPFN OriginalPte.u.Soft.PageFileHigh FFFFFA80009702F0
   +0x020 OriginalPte                     :
      +0x000 u                               :
         +0x000 Soft                            :
            +0x000 PageFileHigh                    : 0y00000000000000000000000000000000 (0)
1: kd> !pte 000000007FFF0000
                                           VA 000000007fff0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001FF8    PTE at FFFFF680003FFF80
contains 00C00000ADEC8867  contains 0280000012ED7867  contains 0A6000000C9E6867  contains 9610000032565805
pfn adec8     ---DA--UWEV  pfn 12ed7     ---DA--UWEV  pfn c9e6      ---DA--UWEV  pfn 32565     -------UR-V

PTE的第0-11位设置为805,也就是

0y1000 0000 0101

处理器的R/W位是第一位,他目前还是0。

接下来,我们断点命中了MiReleaseConfirmedPageFileSpace:

1: kd> g
Breakpoint 1 hit
nt!MiReleaseConfirmedPageFileSpace:
fffff800`04583220 48895c2408      mov     qword ptr [rsp+8],rbx
1: kd> !thread @$thread
THREAD fffffa8032724980  Cid 0a8c.0494  Teb: 000007fffffde000 Win32Thread: 0000000000000000 RUNNING on processor 1
Not impersonating
DeviceMap                 fffff8a000dd4050
Owning Process            fffffa8032635b30       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      203227         Ticks: 0
Context Switch Count      48701          IdealProcessor: 1
UserTime                  00:00:00.390
KernelTime                00:00:09.250
Win32 Start Address 0x000000013fa97d1c
Stack Init fffff8800b179c70 Current fffff8800b179530
Base fffff8800b17a000 Limit fffff8800b174000 Call 0000000000000000
Priority 11 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`0b179978 fffff800`04462bf9     : 00000000`00000000 fffffa80`32724a88 00000000`00000000 00000000`00000000 : nt!MiReleaseConfirmedPageFileSpace
fffff880`0b179980 fffff800`044cd76e     : 00000000`00000001 00000000`7fff0000 00000000`00000001 00000000`00000003 : nt! ?? ::FNODOBFM::`string'+0x46bde
fffff880`0b179ae0 00000001`3fa9173f     : 00000001`3fa998c0 000007fe`f9943400 00000000`0022df18 00000000`003f3c79 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`0b179ae0)
00000000`0022fc10 00000001`3fa998c0     : 000007fe`f9943400 00000000`0022df18 00000000`003f3c79 00000000`0022fc68 : 0x00000001`3fa9173f
00000000`0022fc18 000007fe`f9943400     : 00000000`0022df18 00000000`003f3c79 00000000`0022fc68 00000000`0022fc70 : 0x00000001`3fa998c0
00000000`0022fc20 00000000`0022df18     : 00000000`003f3c79 00000000`0022fc68 00000000`0022fc70 00000000`7fff0000 : 0x000007fe`f9943400
00000000`0022fc28 00000000`003f3c79     : 00000000`0022fc68 00000000`0022fc70 00000000`7fff0000 00000001`2c0f0000 : 0x22df18
00000000`0022fc30 00000000`0022fc68     : 00000000`0022fc70 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 : 0x3f3c79
00000000`0022fc38 00000000`0022fc70     : 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 00000001`00000001 : 0x22fc68
00000000`0022fc40 00000000`7fff0000     : 00000001`2c0f0000 00000001`00000035 00000001`00000001 00350006`00350001 : 0x22fc70
00000000`0022fc48 00000001`2c0f0000     : 00000001`00000035 00000001`00000001 00350006`00350001 00000000`00000000 : 0x7fff0000
00000000`0022fc50 00000001`00000035     : 00000001`00000001 00350006`00350001 00000000`00000000 000089e8`bf84dc74 : 0x00000001`2c0f0000
00000000`0022fc58 00000001`00000001     : 00350006`00350001 00000000`00000000 000089e8`bf84dc74 00000000`0022fcb8 : 0x00000001`00000035
00000000`0022fc60 00350006`00350001     : 00000000`00000000 000089e8`bf84dc74 00000000`0022fcb8 00000000`00000000 : 0x00000001`00000001
00000000`0022fc68 00000000`00000000     : 000089e8`bf84dc74 00000000`0022fcb8 00000000`00000000 00000000`00000000 : 0x00350006`00350001

当控制返回到测试程序重试执行导致错误写入的指令时,PTE已经被更新,他变成了允许写入访问。

1: kd> !pte 000000007FFF0000
                                           VA 000000007fff0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001FF8    PTE at FFFFF680003FFF80
contains 00C00000ADEC8867  contains 0280000012ED7867  contains 0A6000000C9E6867  contains 9610000032565867
pfn adec8     ---DA--UWEV  pfn 12ed7     ---DA--UWEV  pfn c9e6      ---DA--UWEV  pfn 32565     ---DA--UW-V

Bit 0-11:

867 = 0y1000 0110 0111

这时,R/W=1。

一个有趣的细节是,由于该页是脏的,似乎有可能发现它被设置了,但事实并非如此,_MMPFN.u3.e1.Modifiedclear的:

1: kd> dt nt!_MMPFN u3.e1.Modified FFFFFA80009702F0
   +0x018 u3             :
      +0x002 e1             :
         +0x000 Modified       : 0y0

重要的是,VMM必须知道该页是脏的,必须在重新使用前写入磁盘;VMM可以从以下方面推断

  • _MMPTE.OriginalPte.u.Soft.PageFileHigh 变为0。这里如果是clear的,那么说明不存在这个页面的副本。
  • _MMPTE.u.Hard.Dirty1_MMPTE.u.Hard.Dirty 置1(set)

2.3 PET Dirty位管理总结

总结一下VMM如何根据页面cleandirty状态设置PTE是很有用的。

首先,只有当一个页面的内容在分页文件中存在副本时,该页面才被视为clean

对于一个可写的clean页面来说,PTE有以下特征:

  • _MMPTE.u.Hard.Dirty1 = 0。
  • _MMPTE.u.Hard.Dirty = 0。这是实际的处理器dirty位。
  • _MMPTE.u.Hard.Write = 1。这是第11位,是处理器忽略的位。VMM用来记录页面时实际可写的。

当该页被写入时,由于R/W=0,发生了一个页面错误。Write被置1,意味着允许写入,所以它将页的状态改为dirty,并重新执行导致错误的写入指令。

对于一个dirty页的PTE来说:

  • _MMPTE.u.Hard.Dirty1 = 1。这使得页面实际可写
  • _MMPTE.u.Hard.Dirty = 1。这是处理器实际的dirty位。
  • _MMPTE.u.Hard.Write = 1。即不变。

一旦PTE设置了R/W,对该页的进一步写入就不会再引起错误。VMM拦截第一次写,以释放为该页分配的分页文件,因为它的副本不再有效。

我们看到过VMM拦截第一次写访问是如何用来处理正在向分页文件写入I/OActive页面的。

一个实际的只读页有_MMPFN.u.Hard.Dirty1和_MMPTE.u.Hard.Write两个值都是clear的。写入访问会导致错误,VMM看到Write位是clear的会向错误线程抛出一个异常。

3 硬错误

在前几章中,我们关注了一个物理页的生命周期,看到它是如何被映射到地址空间,从地址空间中删除,并写入分页文件,使其可以被重新使用。

我们还看到了它是如何在被重用之前被一个软错误映射回来的。

然而,如果一个页面被重复使用或重新使用,映射它的PTE会存储一个_MMPTE_SOFTWARE的实例,PageFileHigh被设置为分页文件中数据的偏移量,以页为单位表示,即字节偏移量为PageFileHigh x 0x1000。

VA被访问时,VMM必须从分页文件中读回数据,或者我们会说,执行页面置入I/O;这个事件被称为硬错误,在下图中用粗箭头表示;有多种可能的路径,因为物理页可能来自不同的状态。

1: kd> dt nt!_MMPTE_SOFTWARE
   +0x000 Valid            : Pos 0, 1 Bit
   +0x000 PageFileLow      : Pos 1, 4 Bits
   +0x000 Protection       : Pos 5, 5 Bits
   +0x000 Prototype        : Pos 10, 1 Bit
   +0x000 Transition       : Pos 11, 1 Bit
   +0x000 UsedPageTableEntries : Pos 12, 10 Bits
   +0x000 InStore          : Pos 22, 1 Bit
   +0x000 Reserved         : Pos 23, 9 Bits
   +0x000 PageFileHigh     : Pos 32, 32 Bits

3.1 簇操作

3.1.1 基本概念

VMM试图通过读取大于单页的分页文件块来提高页面置入I/O的效率。当硬错误发生时,VMM试图读取错误发生地址附近的几个页面交换出来的数据,这是因为一个进程可能会在错误发生的地址附近的虚拟地址访问更多的数据。簇是预取的一种形式,因为它意味着在实际需要之前读取数据,以试图提高性能。因此,我们将把额外读取的页面称为预取页面。

将簇定义为一组连续的PTE是很有用的。由于一个PTE和它所映射的VA之间存在着一对一的对应关系,一个簇对应于一个虚拟地址的区域。

3.1.2 决定簇的边界

簇操作的逻辑在一个名为MiResolvePageFileFault的函数中实现。VMM试图建立一个由发生错误的PTE和它上面的15个连续的PTE(即在更高的地址)组成的簇。在这样做的时候,它将簇限制在与错误PTE存储在同一页表中的PTE。例如,假设错误地址的PTE是页表的倒数第三个,如下图所示:

在这种情况下,VMM在簇中包括了错误点上方的2个PTE和后者下方的13个PTE,因此簇仍然涵盖16个页面,但没有跨越页表边界。

这种行为的原因可能是,在这个阶段,不能保证页表n+1在内存中被映射(分页结构本身是可分页的,后面我们会讲)。

另一个决定簇边界的因素是不能被簇读取更新的PTEs。为了理解为什么会发生这种情况,我们必须记住,簇的目的是在一次操作中从分页文件中读出被交换的数据,并更新簇的PTEs,使其指向数据被加载的物理页。靠近发生错误的PTEs可能处于这样的状态,即从分页文件中读取内容是无用的,甚至是没有意义的。在下文中,我们将把无法更新的PTE称为从簇中排除的PTE。从簇中排除一个PTE的原因如下:

  • 一个映射了dirty页的PTE必须排除在外,因为根据设计,这个页面再分页文件中根本不存在副本,所以都不知道去哪里读。
  • 一般来说,任何有效的PTE都被排除在外,包括clean页的PTE,因为读取已经在内存中的数据是没有意义的。
  • 当一个PTE指向其中一个Transition链表时,它必须被排除,这又意味着在内存中已经有一个页面内容的副本,所以它不会被再次读取。还要注意的是,如果该页在Modified列表中,它在分页文件中没有副本。
  • 在确定集群边界的同时,MiResolvePageFileFault也排除了_MMPTE_SOFTWARE.Protection值与发生错误的PTE不同的。
  • 当一个PTE映射的虚拟范围如果还没有预留和提交的话,也会被排除掉,因为他是完全无效的。
  • 最后,分页文件中的数据位置可能导致一个PTE被排除。根据设计,VMM在簇读取过程中读取一个连续的分页文件块,所以指向文件中其他分散位置的PTE被排除。这在下图中有所描述:

PTE 2(阴影部分)被排除在簇之外,因为它的分页文件偏移。但是请注意,PTE 3和PTE 4将是簇的一部分,因为正如我们将看到的,VMM使用一个技巧来 "跳过 "被排除的PTEs。还要注意的是,为了使PTE 3被包括在内,它的偏移量必须是1003,即从PTE 0的偏移量开始+3。 VMM将把PTE 0指向加载有第一个4 kB数据块的物理页,PTE 1指向第二个数据块,以此类推。PTE 3指向的数据将是在+3 x 4kB处读到的数据(记住PTE中的偏移量是除以4k后的值)。

我们现在可以理解,簇读取有两个目标:一个是预取正在发生错误的页面周围的页面,另一个是从分页文件中读取连续的数据块,减少由于磁盘搜索时间造成的延迟。尽管并非所有的PTE都是可更新的,而且数据可能分散在非连续的位置上,但VMM仍试图完成这一任务。以前我们页看到过MPW如何努力将数据分页到分页文件的连续块中,以支持簇页面置入。

在寻找簇边界时,MiResolvePageFileFault扫描了发生错误的PTE之上的15个PTE,如果遇到页表边界,则扫描的范围更小。在扫描的范围内,被排除的PTEs与可包含的PTEs穿插在一起,成为簇的一部分(它们将在后面处理);现在考虑下图中描述的情况:

在这个例子中,扫描被PT边界限制在9个PTEs,而最后三个PTEs都必须被排除(出于我们看到的任何一个原因)。当MiResolvePageFileFault意识到这一点时,它将集群的终点设置在PTE 6。虽然有一种方法可以处理被排除的PTEs在被包含的PTEs之间,但簇中在尾部包括被排除的PTEs是没有意义的。

如果上界是这样的,即该簇的范围小于16个PTEs(将排除的PTEs与包含的PTEs混合计算),则MiResolvePageFileFault会扫描出错的PTEs下面(指较低的地址)。如果该集群在发生错误的PTEs之上有n个PTEsMiResolvePageFileFault将扫描下面的15-n个PTEs(在上面的例子中,它将扫描另外10个PTEs)。同样,如果达到页表边界,扫描就会提前停止,簇头的一个被排除的PTE的连续块被丢弃。鉴于这种逻辑,最终的集群可能会拥有少于16个PTEs

3.2 为读取操作申请物理页面

3.2.1 List 搜索次序,页面着色以及NUMA支持

在设置了簇边界之后,MiResolvePageFileFault必须分配物理页,最终将读取的数据放在那里。

首先尝试在Free链表上进行分配--不需要Zeroed页,因为页面内容将被读取后覆盖。该函数首先查看MiFreePageSlist指向的单链表,如果前者是空的,则查看PFN链表(单链表和PFN链表之间的区别以前说过)。如果Free列表是空的,它将进入Zeroed列表,然后进入Standby列表。关于从Standby列表中分配物理内存的进一步细节,我们也讲过了。

由于我们谈论的是物理页,VMM必须考虑到页的颜色和NUMA节点,就像为一个deman-zero fault分配页时一样。特别是,MiResolvePageFileFaultMiResolveDemandZeroFault采取的相同步骤,以检查在分配虚拟内存范围时是否明确请求了NUMA节点。总之,VMM使用相同的逻辑来选择颜色和节点,无论是在物理页第一次被映射时,还是在用从分页文件重新加载的内容填充时。

3.2.2 建立MDL

内存描述符列表(Memory Descriptor List)是一个传递给I/O管理器的数据结构,用来请求读取第一个和最后一个簇PTE的偏移量所跨越的分页文件部分。它包括一个数组,存储将被读取的数据填满的页面的PFN;由第一个PFN选择的物理页面被加载前4 kB的数据,以此类推。当读取开始时,VMM用数组中的PFN更新簇的PTE:第一个PTE将其PFN位(即12-48位)设置为第一个数组元素,等等。

MiResolvePageFileFault检查集群所跨越的每个PTE,对于包含的PTE,用上一节所述的逻辑分配一个物理页,将PFN存储在相应的数组元素中。

与被排除的PTE对应的数组元素被设置为一个dummy页的PFN,其内容被系统的其他部分所忽略。这是一个实际的物理页,它只用来存储无用的数据。当读操作执行时,与被排除的PTE对应的分页文件部分的数据将最终出现在这个页面中,每一个4 kB的块将覆盖之前的内容。这样,I/O管理器仍然能够对整个文件部分发出一个单一的读操作。与被排除的PTE相对应的数据块会在单一的虚拟页中丢失,但无论如何它们都不会被使用。当VMM用数组中的PFN更新PTE时,它不会更新其对应的数组元素存储了dummy页的PFNPTE;这就是保留被排除的PTE的原因。对于更好奇的读者来说,dummy页的PFN存储在MiDummyPage中,它被MiResolvePageFileFault + 0xc9c所引用。

3.3 多个分页文件的支持

3.3.1 分页文件的选择

到目前为止,我们一直假设只有一个分页文件,但实际上并非如此,因为可以在不同的逻辑盘上配置多个文件(最多16个)。因此,MPW在写入一个页面块时,可以从一组分页文件中选择一个,但必须在某个地方存储一些信息,以识别哪个文件已经被选中。

我们现在知道,当一个页面被写入一个分页文件时,_MMPFN.OriginalPte被更新以记录其位置;更重要的是。_MMPFN.OriginalPte.u.Soft.PageFileHigh被设置为文件偏移量除以4 kB。另一个成员,_MMPFN .OriginalPte.u.Soft.PageFileLow记录了哪个文件已经被选中。

分页文件由整数标识,其含义最好在介绍了其他一些数据结构后再解释。

静态变量MmPagingFile是一个指针数组,每个分页文件有一个元素,未使用的元素设置为0。 每个元素指向_MMPAGING_FILE的一个实例。下面我们可以看到一个有只有一个分页文件数据结构的系统:

0: kd> dq nt!MmPagingFile
fffff800`0467f480  fffffa80`31f83f70 00000000`00000000
fffff800`0467f490  00000000`00000000 00000000`00000000
fffff800`0467f4a0  00000000`00000000 00000000`00000000
fffff800`0467f4b0  00000000`00000000 00000000`00000000
fffff800`0467f4c0  00000000`00000000 00000000`00000000
fffff800`0467f4d0  00000000`00000000 00000000`00000000
fffff800`0467f4e0  00000000`00000000 00000000`00000000
fffff800`0467f4f0  00000000`00000000 00000000`00000000

只有一个元素是非0的。下面我们解析一下这个指针:

0: kd> dt nt!_MMPAGING_FILE @@masm(poi nt!MmPagingFile)
   +0x000 Size             : 0xfff7e
   +0x008 MaximumSize      : 0x2ffe7a
   +0x010 MinimumSize      : 0xfff7e
   +0x018 FreeSpace        : 0xfff7d
   +0x020 PeakUsage        : 0
   +0x028 HighestPage      : 0
   +0x030 File             : 0xfffffa80`31659f20 _FILE_OBJECT
   +0x038 Entry            : [2] 0xfffffa80`3165a1b0 _MMMOD_WRITER_MDL_ENTRY
   +0x048 PageFileName     : _UNICODE_STRING "\??\C:\pagefile.sys"
   +0x058 Bitmap           : 0xfffffa80`32357000 _RTL_BITMAP
   +0x060 EvictStoreBitmap : (null)
   +0x068 BitmapHint       : 1
   +0x06c LastAllocationSize : 0
   +0x070 ToBeEvictedCount : 0
   +0x074 PageFileNumber   : 0y0000
   +0x074 BootPartition    : 0y1
   +0x074 Spare0           : 0y00000000000 (0)
   +0x076 AdriftMdls       : 0y1
   +0x076 Spare1           : 0y000000000000000 (0)
   +0x078 FileHandle       : 0xffffffff`80000298 Void
   +0x080 Lock             : 0
   +0x088 LockOwner        : (null)

我们可以看到,该结构以可读的形式存储文件名,在几百页windbg都只显示十六进制数据之后,这在某种程度上让人耳目一新。

我们现在能够理解,_MMPFN.OriginalPte.u.Soft.PageFileLow将分页文件的索引存入MmPagingFile数组。这可以通过用调试器拦截一个硬错误来验证。下面是一个调试会话的一些摘录,MemTests.exe在试图访问地址0x570000时发生了硬错误。第一个输出是通过对PTE的设置读访问断点获得的,这允许我们在PTE仍然指向分页文件的时候断到VMM:

1: kd> !pte 7FFF0000
                                           VA 000000007fff0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001FF8    PTE at FFFFF680003FFF80
contains 02C000003E666867  contains 019000003F471867  contains 0280000040C79867  contains 00029D9A00000080
pfn 3e666     ---DA--UWEV  pfn 3f471     ---DA--UWEV  pfn 40c79     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 29d9a
                                                                                  Protect: 4 - ReadWrite

可以注意到!pte扩展命令显示 PageFile: 0。PTE的值是00029D9A00000080也就是_MMPTE.u.Soft.PageFileLow = 0。

下面是VMM停止的地方:

1: kd> !thread @$thread
THREAD fffffa8031679060  Cid 0f90.068c  Teb: 000007fffffdd000 Win32Thread: 0000000000000000 RUNNING on processor 1
Not impersonating
DeviceMap                 fffff8a000f36be0
Owning Process            fffffa8032cfd0d0       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      136629         Ticks: 0
Context Switch Count      48083          IdealProcessor: 0
UserTime                  00:00:22.198
KernelTime                00:00:20.779
Win32 Start Address 0x000000013ff67d2c
Stack Init fffff88003a14c70 Current fffff88003a14530
Base fffff88003a15000 Limit fffff88003a0f000 Call 0000000000000000
Priority 11 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`03a14980 fffff800`0449976e     : 00000000`00000000 00000000`7fff0000 00000000`00000001 00000000`00000003 : nt!MmAccessFault+0x205
fffff880`03a14ae0 00000001`3ff61744     : 00000001`3ff698f0 000007fe`f9783400 00000000`0013df28 00000000`00213c78 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`03a14ae0)
00000000`0013fc20 00000001`3ff698f0     : 000007fe`f9783400 00000000`0013df28 00000000`00213c78 00000000`0013fc78 : 0x00000001`3ff61744
00000000`0013fc28 000007fe`f9783400     : 00000000`0013df28 00000000`00213c78 00000000`0013fc78 00000000`0013fc80 : 0x00000001`3ff698f0
00000000`0013fc30 00000000`0013df28     : 00000000`00213c78 00000000`0013fc78 00000000`0013fc80 00000000`7fff0000 : 0x000007fe`f9783400
00000000`0013fc38 00000000`00213c78     : 00000000`0013fc78 00000000`0013fc80 00000000`7fff0000 00000001`2c0f0000 : 0x13df28
00000000`0013fc40 00000000`0013fc78     : 00000000`0013fc80 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 : 0x213c78
00000000`0013fc48 00000000`0013fc80     : 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 00000001`00000001 : 0x13fc78
00000000`0013fc50 00000000`7fff0000     : 00000001`2c0f0000 00000001`00000035 00000001`00000001 00350006`00350001 : 0x13fc80
00000000`0013fc58 00000001`2c0f0000     : 00000001`00000035 00000001`00000001 00350006`00350001 00000000`00000000 : 0x7fff0000
00000000`0013fc60 00000001`00000035     : 00000001`00000001 00350006`00350001 00000000`00000000 000038b0`310fb3da : 0x00000001`2c0f0000
00000000`0013fc68 00000001`00000001     : 00350006`00350001 00000000`00000000 000038b0`310fb3da 00000000`0013fcc8 : 0x00000001`00000035
00000000`0013fc70 00350006`00350001     : 00000000`00000000 000038b0`310fb3da 00000000`0013fcc8 00000000`00000000 : 0x00000001`00000001
00000000`0013fc78 00000000`00000000     : 000038b0`310fb3da 00000000`0013fcc8 00000000`00000000 00000000`00000000 : 0x00350006`00350001

接下来,我们要验证读操作是否会针对MmPagingFile[0]所指向的文件。为了做到这一点,我们在loPageRead上设置一个断点,只针对当前进程。调用这个函数来启动读取操作,将rcx设置为要读取的文件的_FILE_OBJECT的地址。不要把_FILE_OBJECT结构与_MMPAGING_FILE混淆:它们是不同的类型,但是_MMPAGING_FILE.File存储了文件的_FILE_OBJECT的地址,也就是传递给IoPageRead的地址。

1: kd> r
rax=fffff98000000000 rbx=fffffa80317dcc70 rcx=fffffa8031659f21
rdx=fffffa80317dcce0 rsi=fffffa8031679060 rdi=fffffa80317dcc20
rip=fffff800044c33e0 rsp=fffff88003a148a8 rbp=fffff88003a14920
 r8=fffffa80317dcc80  r9=fffffa80317dcc40 r10=0000000000000000
r11=0000000000000001 r12=fffffa80317dcc40 r13=0000000000000000
r14=fffffa8032cfd468 r15=fffffa8031659f21
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!IoPageRead:
fffff800`044c33e0 fff3            push    rbx

现在,由于PageFileLow被设置为0,我们要将rcx与索引为0的文件对象进行比较:

1: kd> dt nt!_MMPAGING_FILE @@masm(poi nt!MmPagingFile)
   +0x000 Size             : 0xfff7e
   +0x008 MaximumSize      : 0x2ffe7a
   +0x010 MinimumSize      : 0xfff7e
   +0x018 FreeSpace        : 0x6d739
   +0x020 PeakUsage        : 0xa2616
   +0x028 HighestPage      : 0
   +0x030 File             : 0xfffffa80`31659f20 _FILE_OBJECT
   +0x038 Entry            : [2] 0xfffffa80`3165a1b0 _MMMOD_WRITER_MDL_ENTRY
   +0x048 PageFileName     : _UNICODE_STRING "\??\C:\pagefile.sys"
   +0x058 Bitmap           : 0xfffffa80`32357000 _RTL_BITMAP
   +0x060 EvictStoreBitmap : (null)
   +0x068 BitmapHint       : 0x25357
   +0x06c LastAllocationSize : 0x100
   +0x070 ToBeEvictedCount : 0
   +0x074 PageFileNumber   : 0y0000
   +0x074 BootPartition    : 0y1
   +0x074 Spare0           : 0y00000000000 (0)
   +0x076 AdriftMdls       : 0y0
   +0x076 Spare1           : 0y000000000000000 (0)
   +0x078 FileHandle       : 0xffffffff`80000298 Void
   +0x080 Lock             : 0
   +0x088 LockOwner        : 0xfffffa80`30f57b61 _ETHREAD

这两个值并不完全相等:唯一的区别是第0位,rcx中是置1的,而在File成员中是清零。我们可以通过再次进行测试来验证情况是否总是如此,所以我们可以假定_FILE_OBJECT是对齐的,所以第0位总是清零(实际上,我们可以观察到它在8个字节的边界上对齐,所以第0-3位都是清零),VMM为了某种目的把它作为一个额外的标志。以这种方式 "重载 "已对齐指针的位是VMM代码中经常发现的一种技巧。

撇开第0位不谈,这个测试确认了PageFileLow = 0的内分页的目标是MmPagingFile[0]。

PageFileLow是4位宽,所以它允许多达16个分页文件被寻址。

下面我们可以看到在loPageRead开始时的线程调用栈:

1: kd> !thread @$thread
THREAD fffffa8031679060  Cid 0f90.068c  Teb: 000007fffffdd000 Win32Thread: 0000000000000000 RUNNING on processor 1
Not impersonating
DeviceMap                 fffff8a000f36be0
Owning Process            fffffa8032cfd0d0       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      136629         Ticks: 1 (0:00:00:00.015)
Context Switch Count      48083          IdealProcessor: 0
UserTime                  00:00:22.198
KernelTime                00:00:20.794
Win32 Start Address 0x000000013ff67d2c
Stack Init fffff88003a14c70 Current fffff88003a14530
Base fffff88003a15000 Limit fffff88003a0f000 Call 0000000000000000
Priority 11 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr               : Args to Child                                                           : Call Site
fffff880`03a148a8 fffff800`044c3109     : 00000000`00000000 00000000`00000000 00000000`00000000 fffffa80`317dcc20 : nt!IoPageRead
fffff880`03a148b0 fffff800`044a9a8a     : 00000000`00000000 00000000`00000000 ffffffff`ffffffff 00000000`00000000 : nt!MiIssueHardFault+0x255
fffff880`03a14980 fffff800`0449976e     : 00000000`00000000 00000000`7fff0000 00000000`00000001 00000000`00000003 : nt!MmAccessFault+0x146a
fffff880`03a14ae0 00000001`3ff61744     : 00000001`3ff698f0 000007fe`f9783400 00000000`0013df28 00000000`00213c78 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`03a14ae0)
00000000`0013fc20 00000001`3ff698f0     : 000007fe`f9783400 00000000`0013df28 00000000`00213c78 00000000`0013fc78 : 0x00000001`3ff61744
00000000`0013fc28 000007fe`f9783400     : 00000000`0013df28 00000000`00213c78 00000000`0013fc78 00000000`0013fc80 : 0x00000001`3ff698f0
00000000`0013fc30 00000000`0013df28     : 00000000`00213c78 00000000`0013fc78 00000000`0013fc80 00000000`7fff0000 : 0x000007fe`f9783400
00000000`0013fc38 00000000`00213c78     : 00000000`0013fc78 00000000`0013fc80 00000000`7fff0000 00000001`2c0f0000 : 0x13df28
00000000`0013fc40 00000000`0013fc78     : 00000000`0013fc80 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 : 0x213c78
00000000`0013fc48 00000000`0013fc80     : 00000000`7fff0000 00000001`2c0f0000 00000001`00000035 00000001`00000001 : 0x13fc78
00000000`0013fc50 00000000`7fff0000     : 00000001`2c0f0000 00000001`00000035 00000001`00000001 00350006`00350001 : 0x13fc80
00000000`0013fc58 00000001`2c0f0000     : 00000001`00000035 00000001`00000001 00350006`00350001 00000000`00000000 : 0x7fff0000
00000000`0013fc60 00000001`00000035     : 00000001`00000001 00350006`00350001 00000000`00000000 000038b0`310fb3da : 0x00000001`2c0f0000
00000000`0013fc68 00000001`00000001     : 00350006`00350001 00000000`00000000 000038b0`310fb3da 00000000`0013fcc8 : 0x00000001`00000035
00000000`0013fc70 00350006`00350001     : 00000000`00000000 000038b0`310fb3da 00000000`0013fcc8 00000000`00000000 : 0x00000001`00000001
00000000`0013fc78 00000000`00000000     : 000038b0`310fb3da 00000000`0013fcc8 00000000`00000000 00000000`00000000 : 0x00350006`00350001

3.3.2 分页文件的配置

在系统上配置的分页文件列表存储在注册表值HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PagingFiles中,其中包含每个分页文件的名称、最小尺寸和最大尺寸。

如果注册表键中的最小和最大尺寸被设置为0,则使用下表中的默认值:

也可以将Windows配置为在关机时删除分页文件,这可能是出于隐私的考虑,因为它们存储了系统运行时被MPW置出的东西。将注册表值HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\ClearPageFileAtShutdown设为1可以激活这一功能。

3.4 在读取过程中的状态

3.4.1 错误线程的状态

发生错误的线程等待读取完成:因为它需要页面内容来恢复其执行,所以在这期间它能做的事情不多。请注意与由MPW执行的分页不同,后者发出一个异步写入并继续其工作。

VMM_MMPFN.u1.Event里存储了事件的地址,当读取完成时就激活这个事件。

我们可以通过在MiWaitForlnPageComplete+0xbd上设置断点,即KeWaitForSingleObject被调用的地方,在线程开始等待之前捕获它。通过这样做,我们能够观察到PTEs的状态和接下来的章节中描述的_MMPFNs。下面是一个断点命中时的调用堆栈:

1: kd> kn
 # Child-SP          RetAddr               Call Site
00 fffff880`03a147d0 fffff800`044c313f     nt!MiWaitForInPageComplete+0xbd
01 fffff880`03a148b0 fffff800`044a9a8a     nt!MiIssueHardFault+0x28b
02 fffff880`03a14980 fffff800`0449976e     nt!MmAccessFault+0x146a
03 fffff880`03a14ae0 00000001`3ff61744     nt!KiPageFault+0x16e
04 00000000`0013fc20 00000001`3ff698f0     0x00000001`3ff61744
05 00000000`0013fc28 000007fe`f9783400     0x00000001`3ff698f0
06 00000000`0013fc30 00000000`0013df28     0x000007fe`f9783400
07 00000000`0013fc38 00000000`00213c78     0x13df28
08 00000000`0013fc40 00000000`0013fc78     0x213c78
09 00000000`0013fc48 00000000`0013fc80     0x13fc78
0a 00000000`0013fc50 00000000`7fff0000     0x13fc80
0b 00000000`0013fc58 00000001`2c0f0000     0x7fff0000
0c 00000000`0013fc60 00000001`00000035     0x00000001`2c0f0000
0d 00000000`0013fc68 00000001`00000001     0x00000001`00000035
0e 00000000`0013fc70 00350006`00350001     0x00000001`00000001
0f 00000000`0013fc78 00000000`00000000     0x00350006`00350001

3.4.2 PTEs的状态

簇中的每个PTE都被设置为指向transition列表中的一个页面。通过将内容解释为_MMPTE_TRANSITION的实例,我们可以看到它们被设置为如下内容:

Valid: 0

Protection: 软件保护值。当PTE指向被换出的数据时这个值和发生错误之前是一样的

PageFrameNumber: 之前申请的页面的PFN

因此,就PTE而言,就像这些页面是在ModifiedStandby列表中一样。然而,_MMPFNs记录了每个页面都有一个正在读取的事实。

3.4.3 _MMPFN状态

对于每个页的_MMPFN,会设置成这样:

u1.Event: 事件的地址,当读取完成后会被激活。所有参与集群读取的mpfns都指向同一个事件,这个指针在MiWaitForInPageComplete+0xbd时调用KeWaitForSIngleObject,作为第一个参数传入rcx

u2.ShareCount:0。这说明该页还没有被映射到一个虚拟地址。

u3.ReferenceCont: 1。说明正在进行的读取:I/O管理器和文件系统驱动程序正在引用该页。

u3.e1.PageLocation_MMLISTS.StanbyPageList。注意_MMPFN是如何 "倾向于 "成为Standby列表的一部分的。这个原因将很快得到解释。

u3.e1.ReadInProgress :1. 这个用来追踪页面正在被读取的过程中。

OriginalPte:设置为错误发生时PTE的值。这是_MMPTE_SOFTWARE的实例,它存储了软件保护和分页文件信息(文件号和偏移量)。我们将在下一节描述_MMPFN的最终状态时再说这个成员上。

3.5 读取完成时的状态

3.5.1 Faulting虚拟页面的状态

我们用Faulting虚拟页这个词来表示包含发生硬错误地址的虚拟页。这个页面显然被映射到簇建立时分配的相应的物理页面上,这样内存访问就可以恢复了。PTE变为一个有效的,并且_MMPFN被设置为一个Active页的样子。

如果错误是在试图从内存中读出时发生的,那么该页是clean的,所以PTEDirty1位是清零的,而_MMPFN中的OriginalPte仍然将该页内容的位置存入分页文件(即文件号和偏移量)。像往常一样,只要一个页面处于这种状态,它就可以被移到Standby列表中,因为它的内容被安全地保存在分页文件中。

如果错误是在尝试写内存时发生的,作为错误处理的一部分,该页被设置为脏页。分页文件信息被从OriginalPte中删除,这意味着必须调用MiReleaseConfirmedPageFileSpace

值得注意的是,PTE_MMPFN的最终设置与软错误的设置非常相似。

自然_MMPFNu3.e1.ReadInProgress会被清零。

3.5.2 预取虚拟页的状态

这些页面是包含在簇中的PTE所映射的页面(不包括发生错误的虚拟页),它们被完全保留在读操作开始时的状态。这些PTEs被设置为指向某个Transition列表上的页面;在读操作完成后,其物理页面的_MMPFN被留在Standby状态,所以PTEs_MMFPN是一致的。这种方法有一些有趣的含义。

首先,对预取虚拟页面的最终访问会导致一个软错误,这个错误会很快被解决,而不需要进一步的I/O。因此,如果程序访问的是发生错误页面周围的页面,我们看到预取带来的好处。

同时,将物理页留在Standby列表中,使它们可以被重新使用。我们现在知道,当没有Zeroed或空闲页时,VMM会使用Standby页,因为在这种状态下的页,按照设计,在分页文件中有其内容的副本。当然,由于这些物理页面刚刚被读取,甚至没有被访问过,所以它们非常符合要求。

最后,由于PTEs被留在Transition,它们的P位是清零的,这保证了系统中没有处理器将它们缓存在TLB中。这样,如果这些页面被重新利用,例如为了满足来自其他地址空间的错误,就不需要刷新任何TLB。这与任何Standby页被重新利用时的情况没有什么不同:从工作集中裁剪出来的页面不再被缓存在TLB中,因为当页面改变状态时,缓存被无效。这使得Standby状态特别适合于簇读取的物理页面,这些页面已经被预先加载,但实际上并没有被访问过。

总之,将所有预取的物理页放入Standby中,预取可以获得比只取单个分页文件的好处,同时,留下可用的物理内存供重新使用,而且还不会不污染TLB--这是一个双赢的组合。

这种方法是基于这样的概念:空闲的物理内存实际上是被浪费的内存。与其让一堆未使用的内存被垃圾或零填满,不如预先加载,试图在硬错误发生之前就预测到它们。

预取物理页的_MMPFN仅被更新,以使其进入Standby状态:

  • u1.Flink被用来链接实例到链表里。以前,u1.Event为读取事件的指针。事件(与ul.Flink重合)持有读取事件的指针。
  • u2.Blink,当处于读取中时他设置为0,这个也是用来把实例链到链表里的。
  • u3.e1.ReadInProgress被清零

当然,在没有被修改的成员中,有OriginalPte,它指向内容的分页文件副本,就像任何其他Standby页一样。

3.6 被排除在外的虚拟页的状态

正如已经提到的,被排除的PTEs可以通过在相应的MDL元素中存在的dummyPFN来识别,并且在读取开始时不做任何改变。当然,它们在读取完成后也不会被修改,所以它们不受集群读取的影响,而文件数据则在虚页中丢失。

4 页面置入冲突

4.1 页面置入冲突概念

VMM确定发生了硬错误时,它仍然独占WS推锁。在进入等待置入读取完成之前,VMM释放推锁,这样同一地址空间的其他线程就可以自由地执行VMM代码(例如,调用VirtualAllocEx的线程或遇到页面错误的其他线程)。这使得其他线程有可能改变正在执行的页面置入所涉及的虚拟地址空间的部分,这种事件我们将命名为页面置入冲突。

以下几种页面置入冲突是可能的:

  • 保护改变带来的冲突:页面的保护在读取时被改变了,比如说可读可写变成了只读。

  • 内存被释放或者取消了提交:虚拟区域包含的页在读取时被释放了;虚拟页在读取时被取消提交了。

    注意:VirtualFreeEx API只允许释放一个之前前被保留(并最终提交)的整体范围:整个保留范围必须被释放。不可能,比如说,释放(即取消提交和 "取消保留")该范围的一个单页。当一个范围被释放时,它既不是保留也不是提交。另一方面,有可能取消提交子范围,甚至以前保留的范围的单页;子范围仍然保留,但不再提交。这可能意味着释放虚拟内存相当于扔掉了整个范围的VAD,而取消提交子范围意味着将它们的PTEs设置为取消提交的。

  • 页面错误冲突:另一个线程试图访问正在读取的虚拟页,导致同一虚拟页的另一个页面错误。

    此外,由于VMM实现了簇,对于上述每种碰撞类型,所涉及的页面可能是解决错误实际需要的页面,或者是簇中正在进行分页的另一个页面。在正在读取的簇的不同页面上发生多次碰撞也是可能的。

4.2 保护变化引起的冲突

4.2.1 冲突发生在发生错误的PTE上

在读取过程中,PTE处于Transition状态。对VirtualProtectEx的调用改变了页面保护,将_MMPTE.u.Trans.Protection改变为新的值,而不改变其他成员,因为PTE仍然处于Transition,仍然指向同一个物理页。

这种冲突是在MiWaitForlnPageComplete中检测到的,VMM在这里等待读取的完成。在等待之后,这个函数再次获得WS推锁,并调用MilsFaultPtelntact,以检查涉及错误的PTE(以及分页结构层次的其他部分)是否已经改变;这就检测到了保护值的变化。

在这种情况下,MiWaitForlnPageComplete返回STATUS_PTE_CHANGED,这有两个影响:VMMPTE保持不变,即处于Transition,并将_MMPFN添加到Standby列表中。之后,VMM恢复执行错误指令,这将引起另一个错误;VMM检查尝试的访问是否在PTE中存储的保护下被允许,如果是这样,则将错误解决为软错误,因为现在该页处于Standby状态。如果新的保护禁止尝试的访问,就会产生一个访问违规的异常。

上面的逻辑意味着,VMM通过用新的保护重新评估错误来解决这种冲突。

当第二个错误被处理时,VMM不启动读操作,所以它保持获得的WS推锁,直到它完成更新PTE使其有效。

4.2.2 冲突发生在预取的PTE上

这种情况发生在被簇读预取的PTE的保护被改变时。这只是导致_MMPTE.u.Trans.Protection被设置为新的值。在读取结束时,PTE将处于transition状态,页面处于standby状态,就像没有发生冲突时的情况一样。如果预取的页面后来被访问,软错误将根据PTE的新保护来处理。

4.3 释放或者取消提交引起的冲突

4.3.1 释放或者取消提交的是发生错误的页面

在这种情况下,被读取的页面在读取过程中被释放或取消提交。这里描述的行为是通过在读取过程中调用VirtualFreeEx观察到的。

当API调用返回时,如果调用参数指定释放内存范围,则PTE被设置为0,如果该页只是被取消提交,则被设置为0x200。这个值对应于MMPTE_SOFTWAREProtection=0x10,WinDbg将其解释为Decommitted,如下面的例子。

在调用之前,PTE处于Transition状态,并指向物理页;在调用之后,该页的_MMPFNPteAddress成员的第1位被设置。这个位在实际地址中总是0,因为PTE是8字节对齐的,所以它可以作为一个标志。当它被设置时,表示这个页面在读取完成后将被返回到Free列表中。MiWaitForlnPageComplete被用于等待读取数据的事件发出信号时检查这个位,并采取相应的行动,因为虚拟页已经被释放或取消提交,所以丢弃读取的数据。

对于这种冲突,MilssueHardFault返回STATUS_PTE_CHANGED之后,MmAccessFault返回STATUS_SUCCESS,这告诉KiPageFault恢复错误发生时的上下文并重试错误指令。这将导致另一个页面错误,因为PTE是0或0x200,而这一次VMM将检测到对无效地址的访问,MmAccessFault将返回STATUS_ACCESS_VIOLATIONKiPageFault将在用户模式下引发一个异常。
除非被__try/__except块捕获,否则会使进程崩溃。因为一个程序在释放内存时,如果不注意检查它的一个线程是否还在访问它,那就是自找麻烦。

4.3.2 释放或者取消提交的是预取的页面

这个事件与上一节的事件类似(与之前一样,分析是通过调用VirtualFreeEx进行的):一个簇读取正在进行中,被预取的一个页面被取消提交或者它们所属的整个范围被释放。

当一个页面被取消提交时,其PTE被设置为0x200(_MMPTE_SOFTWARE.Protection = 0x10 = Decommitted);当一个页面被释放时,其PTE被清零。

4.4 页面错误冲突

4.4.1 初始处理和支持块

这也许是最复杂的一种页面置入碰撞,因为它涉及到对被读页面的并发访问。碰撞的页面错误最初被VMM代码视为软错误,因为在读取过程中,PTE的设置与该页面在StandbyModified列表中的设置完全一样:它有_MMPTE.u.Trans.Transition被置1,PageFrameNumber存储物理页的PFN,其他成员根据页面保护和缓存策略设置。

VMM发现相关的_MMPFN实例的u3.e1.ReadlnProgress置1时,就会检测到碰撞。

为了理解如何处理冲突碰撞的页面错误,我们需要在VMM用于页面置入的数据结构上增加一些细节。我们已经知道,_MMPFN.u1.Event存储了一个事件的地址,这个事件在读取完成时被发出信号。这个事件是一个数据结构的一部分,储存了在页面置入过程中使用的其他变量。当VMM处理硬错误时,它通过调用MiGetlnPageSupportBlock分配这个数据结构的一个实例,所以我们将这个结构命名为支持块。不幸的是,在Windows的公共符号中似乎没有这个数据结构的声明。下面是作者逆向的一些字段:

@#@IN_PAGE_SUPPORT_BLOCK
...
        +8 Thread
    +10h ListEntry
    +20h Event1
    +38h Event2
...
    +90h Unk-90
...
    +b8h Unk-b8
...
    +c0h Mdl
    +f0h PnfArray

对于初学者来说,必须知道_MMPFN.u1.Event指向为解决硬错误而分配的@#@IN_PAGE_SUPPORT_BLOCK实例内的Event1,所以VMM可以从_MMPFN得到@#@IN_PAGE_SUPPORT_BLOCK

PfnArray成员是一个存储物理页PFN的数组,从分页文件中读取的数据将被载入其中。它们是簇置入的PTE所指向的物理页。

VMM可以用两种不同的方式来处理碰撞的页面错误:它可以让第二个发生错误的线程等待第一个线程发起的读取完成,或者它可以在第二个线程中对相同的虚拟内存内容启动第二个读取操作。我们可以将第一种方法称为同步处理,因为第二个线程与第一个线程同步,因此,我们将第二种策略称为异步处理。

在接下来的章节中,我们将更详细地描述这一逻辑,我们将看到为什么VMM可能选择异步处理冲突。

4.4.2 同步处理

#### 相同Faulting PTE上的冲突

由第二个错误线程执行的VMM代码从错误PTE_MMPFN(PTE处于Transition,所以它包括PFN),并从_MMPFN得到@#@IN_PAGE_SUPPORT_BLOCK。然后,它增加支持块中的一个计数器,并等待
@#@IN_PAGE_SUPPORT_BLOCK.Event2被激活。

同时,第一个故障线程正在等待@#@IN_PAGE_SUPPORT_BLOCK.Event1(由_MMPFN.u1.Event指出),当读操作完成时,I/O管理器会发出信号。Event2不是由I/O管理器直接发出信号的,所以第一个出错的线程总是第一个被恢复执行的。

可能有任意数量的冲突线程在等待Event2,每个线程都试图解决正在读取的页面上的错误(我们将在下一节描述簇中其他PTE的冲突)。

Event1被激活时,VMM独占WS推锁,并将_MMPFN设置为Active状态。然后,它检查支持块中由冲突线程增加的计数器,当它发现它大于0时,发出Event2信号。这将唤醒所有冲突的线程,然而由它们执行的VMM代码时又试图获取WS推锁,所以它们再次因为推锁阻塞。

在保持推锁的同时,第一个线程更新了页面PTE,将其变为一个有效的PTE,将VA映射到刚刚读取的物理页面。然后,它释放推锁并恢复错误指令,这样用户模式的代码就可以继续其业务。

碰撞的线程依次获得了WS推锁,并检测到PTE已经改变,所以它们不试图更新它,只是简单地恢复它们的错误指令,这将执行成功,因为PTE现在是有效的。

每个碰撞的线程都会递减支持块中的计数器;检测到最后一个使用支持块的线程会释放它。

#### 在簇上的其他PTEs的冲突

这种情况与上一节的情况没有太大区别:当读正在进行时,集群中的所有PTE都在transition中,它们所指向的_MMPFNs被标记为读正在进行。在这个阶段,错误地址的PTE_MMPFN与集群中的其他地址没有区别。
冲突线程仍在等待Event2被激活,然后等待WS的推锁。当第一个线程释放推锁时,集群中的其他PTE处于Transition状态,相应的_MMPFNS处于Standby状态。因此,冲突线程所要做的是解决一个实际的软错误,他们也是这样做的。当所有冲突的错误都被解决后,他们的PTE是有效的,而其他簇的PTE则处于transition状态。

这种情况大大受益于预取:冲突线程不需要执行磁盘访问,只需等待第一个线程为其完成工作,即使预取尚未完成,预取也是有效的。

4.4.3 异步处理

#### 异步处理何时发生

在本节中,我们将描述导致异步处理的两种情况。虽然我们不能确定这是VMM选择这种行动方式的唯一情况,但它在以下情况下这样做却是事实。

##### ActiveFaultCount大于0

导致异步处理的一种情况是,当_ETHREAD.ActiveFaultCount大于0时,线程产生了一个页面错误。这个计数器在启动读取之前被递增,在从MiWaitForlnPageComplete返回之后被递减。当VMM检测到一个页面错误冲突时,它检查这个计数器,如果发现它大于0,就异步处理这个冲突。

注意事项:这里由于内核版本和书上不一致,导致反汇编MiResolveTransitionFault函数时,找不到对应的汇编指令 cmp byte ptr [rdx+456h],bl,故不做复现操作。用原书截图。

下面是一个线程在发现ActiveFaultCount大于0时,MiResolveTransitionFault中执行的当前指令:

rdx指向的时_ETHREAD实例,+456的地方就是ActiveFaultCount,他当前是1。为了捕获这个事件,这里采用了条件断点的方法

这个断点的意思就是当rdx+456等于0时,恢复执行,反之断下。nt! ?? ::FNODOBFM::string'` 是因为函数布局的原因造成的。在x64 Windows中,发现函数的代码块位于主函数体所跨越的区域之外是很常见的。这个块(有时不止一个)通常位于比函数符号对应的地址更低的位置,块内的地址是这样显示的,这使得查看反汇编代码十分滑稽。你都不能像下面这样使用这个符号:

1: kd> u nt! ?? ::FNODOBFM::`string'+0x39103
Syntax error at 'nt! ?? ::FNODOBFM::`string'+0x39103'

想对这种地址进行反汇编还挺麻烦的,书上说的方法是首先是找到内核基址,然后再找出偏移。即:

下面是断下后的线程状态:

##### 第一个Faulting 线程比第二个线程的I/O优先级低

为了更好地理解这一节,我们先简短的介绍一下I/O优先级。

一个线程的I/O优先级存储在_ETHREAD.ThreadIoPriority中,下面是来自Windbg的输出:

1: kd> !thread @$thread
THREAD fffff8800470af40  Cid 0000.0000  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 1
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffff8000465b1c0       Image:         Idle
Attached Process          fffffa8030f03040       Image:         System
Wait Start TickCount      0              Ticks: 42471 (0:00:11:02.551)
Context Switch Count      86354          IdealProcessor: 1
UserTime                  00:00:00.000
KernelTime                00:10:43.316
Win32 Start Address nt!KiIdleLoop (0xfffff800044d2310)
Stack Init fffff88004728c70 Current fffff88004728c00
Base fffff88004729000 Limit fffff88004723000 Call 0000000000000000
Priority 16 BasePriority 0 PriorityDecrement 0 IoPriority 0 PagePriority 0
...
1: kd> dt _ETHREAD -y ThreadIoPriority
ntdll!_ETHREAD
   +0x448 ThreadIoPriority : Pos 10, 3 Bits

大部分的线程的I/O 优先级为2。这个成员的宽度是3bits。那么他的范围就是0-7。

这个属性被文件系统驱动栈使用,它将优先级较高的线程的I/O操作排在其他线程的前面。I/O优先级的一个应用是提高播放视频、音乐等应用程序的响应速度,使其输出不会出现差错。

线程的I/O优先级可以通过指定nPriority参数然后调用SetThreadPriority API来改变。比方说设置为THREAD_MODE_BACKGROUND_BEGIN就是将优先级设置为0。

VMM必须从分页文件中读取数据时,它检查当前线程(线程A)的I/O优先级,如果它小于2,它将设置@#@in_page_support_block. Unk-90的第2位
并继续执行I/O任务。如果后来,线程B与线程A发生碰撞,由线程B执行的VMM代码会检查I/O的优先级。当优先级为2或更高时,它检查Unk-90的第2位,如果被设置,则异步解决错误。

这种设计背后的逻辑是,线程A正在以低I/O优先级进行分页,而线程B需要相同的虚拟页,并且有正常(或更高)的I/O优先级,所以让B等待A是有害的。

这种优化似乎是在Superfetch中使用的,这是一个Windows组件,当有足够的未使用的内存时,它预取被换出的内容,将预取的页面放在Standby链表中。这与簇内分页描述的方法相同,但规模更大。Superfetch通过进程跟踪内存使用情况,试图预测哪些虚拟页面更有可能被需要。当物理内存变得可用时,它不是让它在Free链表中闲置,而是开始预取换出的内容。然而,Superfetch似乎是在低I/O优先级下进行的,正如下面的线程状态所显示的那样,其中的函数名称与Superfetch有关,I/O优先级为0:

这有很大的意义:虽然试图充分利用空闲内存是件好事,但让磁盘子系统忙于预取活动是不明智的;其他进程可能需要访问磁盘来响应用户。因此,Superfetch的行为和运行,其I/O优先级设置为0。然而,如果另一个线程碰撞到一个被 "superprefetched "的页面,让它等待低优先级的读取完成是不行的。现在这个页面需要尽快完成,而不仅仅是因为Superfetch的猜测游戏,所以另一个读被启动了,用正常的I/O优先级把Superfetch挤出去。

#### 异步读取是如何建立的

一旦VMM确定它必须开始第二个读操作,它就会分配一个新的物理页,从分页文件中加载内容,并分配一个新的支持块,该支持块通过其ListEntry成员与已经在进行的读操作的支持块相连接。这个支持块在其PfnArray中存储了新页的PFN。我们将把第一个I/O的支持块称为主块。至于同步的情况,VMMtransiiton PTE中的PFN得到_MMPFN,从事件指针_MMPFN.u1.Event 到主支持块,因为指向的事件是
@#@IN_PAGE_SUPPORT_BLOCK.Event1(即_MMPFN.ul.Event指向+0x20的支持块)。

新的支持块,我们称之为从支持块,用于读取发生碰撞的单个页面,也就是说,即将开始的读取不是针对一个簇,只是针对一个页面。

注意新的读取是如何使用与原来不同的物理页的:这两次读取是独立的操作,每次都是将同一块4kB的分页文件加载到不同的物理页。

这种情况可能涉及多个线程:可能有一个主支持块和多个从支持块,每个支持块都是由于不同的线程在相关页面上发生碰撞而产生的。在下文中,我们将使用主支持块或从支持块这两个术语,分别与分配主支持块或从支持块之一的线程有关。

当一个从操作被建立时,PTE不会被更新:它仍然存储着主线程设置簇读取时为该特定虚拟分配的物理页的PFN。从线程执行的读取将以物理页为目标,在这个阶段,这些物理页只在从支持块中 "已知"。

在从线程中执行的VMM代码持有WS推锁,同时它检测到发生了碰撞,并且一个异步I/O正在进行中。这段代码将在它自己的从@#@IN_PAGE_SUPPORT_BLOCK实例中的Event1上等待读取完成,但在这样做之前,它将释放WS推锁。

每个线程,包括主线程,都试图在其读操作完成后获得WS推锁,所以我们有不同的情况,取决于主线程或从线程谁 "获胜",即谁先获得推锁。

#### 场景A: 主线程先获得推锁

##### 主线程采取的动作

在获得推锁后,主线程检测到有从支持块与自己的支持块链在一起。这些从块可能是针对发生错误的PTE或簇中其他PTE上的碰撞。当主线程拥有推锁时,它已经完成了对簇中所有页面的读取,所以它抛弃了所有从线程的读取操作。

它通过设置一个标志,向每个从线程发出信号,表明其读取操作必须被放弃。这个标志是从线程读取所对应的物理页的_MMPFN实例中PteAddress的第1位。请注意,这个位,我们将称之为discard 标志位,与页面在被读时被取消提交或释放时所用的位相同。在所有这些情况下,它被用来向刚刚完成读取的线程发出信号,表明这一切都是徒劳的,因为在它等待的时候,事情发生了变化。

为了设置discard标志,主线程会扫描与自己的支持块链接的链表,并从每个块中获得页面的PFN。接下来只是一个算术问题,计算_MMPFN地址并设置标志。除了设置标志,主线程还从链表中取消对每个块的链接。

之后主线程像往常一样完成页面置入:将错误页映射为Active,并将预取的PTE放在Standby中;释放WS推锁;恢复错误指令的执行。

主线程也会检查自己的支持块共享计数,并最终发出信号@#@lN_PAGE_SUPPORT_BLOCK.Event2以唤醒其他碰撞并选择同步处理的线程。例如,一个ActiveFaultCount等于0的线程可能与主线程碰撞,并等待
@#0lN_PAGE_SUPPORT_BLOCK. Event2,而另一个ActiveFaultCount大于0的线程则开始异步读取。

##### 从线程采取的动作

在获得推锁后,每个从线程找到它自己的支持块,发现它不是任何链表的一部分,并检测到它刚刚加载数据的页面的_MMPFN中设置的discard标志。每个线程在不更新PTE的情况下将该页返回到空闲列表,释放推锁并继续执行错误指令,这将会执行成功,因为PTE已经被主线程变得有效。

#### 场景B: 其中一个从线程先获得推锁

##### 获胜从线程执行的动作

第一个获取推锁的线程会发现它的支持块仍然是一个链的一部分,所以它将通过以下方式访问主支持块@#@lN_PAGE_SUPPORT_BLOCK.PmasterBlock

这个成员位于长度可变的PFN数组之后,然而@#@lN_PAGE_SUPPORT_BLOCK.Mdl.Size给出了@#@lN_PAGE_SUPPORT_BLOCK.Mdl加上PFN数组所跨越的字节数, 所以可以计算出PmasterBlock的地址. 这个成员又指向主块。

该线程扫描主块中的PFN列表,寻找发生碰撞的那个。主块的PFN数组存储了多个元素,因为主块的读取按簇的,所以从线程必须找到发生碰撞的那个元素。为了做到这一点,从线程在自己的__MMPFN实例中查看PteAddress(它在读取中使用的物理页的实例);这个成员指向发生碰撞的VAPTE。对于主块数组中的每个PFN,它访问_MMPFN.PteAddress,寻找指向相同PTE的成员。然后,它设置PteAddress的第1位,从而向主线程发出信号,这个页面必须被丢弃(在这个阶段,主线程仍在等待其读取完成或获得推锁)。

在这种情况下,从线程 "知道 "自己赢了,因为它的支持块仍然在列表中被链起来,所以它更新了PTE,用自己的物理页的PFN替换了指向主线程分配的物理页的PFN。这就是抛弃主线程的读取,使次线程实际解决错误的原因。

胜利的线程仍然要处理其他在同一VPN上碰撞并输掉比赛的从线程,所以它扫描所有其他与主线程连锁的支持块,并检查PFN(一个从块有一个PFN)是否为碰撞的PteAddress。如果是这样的话,它就解除该块的链接,并将相关的_MMPFN.PteAddress的第1位,向另一个从线程发出信号,表示它必须丢弃它的页面。

请注意,获胜的线程并没有解除所有从块的链,因为在簇中其他PTE上的碰撞可能会有区块在链上。当获胜的线程完成了清理工作,主块可能仍然与其他的块链在一起,与不同的碰撞有关。

无论如何,获胜的线程通过使PTE有效、释放推锁和恢复出错指令继续执行。

##### 主线程采取的动作

主线程将_MMPFN.PtreAddress的第1位置1的页面返回到空闲列表中,并且不更新它们的PTEs

其他预取的PTE的页仍然被放入Standby状态,PTE被留在transition状态,就像没有发生碰撞时一样。

之后主线程释放推锁并恢复错误指令。

##### 失败的从线程采取的动作

这些线程发现自己处于类似于主线程获胜时的情况:他们的支持块不是链的一部分,而且他们的物理页有_MMPFN.PteAddress的第1位被设置。它们中的每一个都将其物理页返回到free list中,释放推锁并恢复错误指令。

#### Prefetched PTEs上的冲突

当主线程获胜时,所有的从读取都会被丢弃,无论它们是针对发生错误的PTE还是预取的PTE之一。主线程将所有预取的 PTE 留在transition中,其物理页在standby中。

当其中一个从线程获胜时,它使其特定PTE的读取结果无效,正如前面所解释的那样,所有其他碰撞的线程都是如此。然后,它使PTE有效,因为从线程不只是预取它:它对它产生了一个错误。其他线程发现自己的读取结果无效,就不会去碰这个PTE

总之,在访问正在预取的PTE的过程中,赢得比赛的从线程可以独立进行,映射VA并恢复出错指令。我们可以看到这是一件好事,因为Superfetch很可能以低优先级对PTE簇进行置入,所以需要这些PTE中任何一个的线程应该被允许以自己的优先级进行置入。

4.5 页面置入冲突实验

4.5.1 概述

本节解释了如何进行一些分页碰撞的实验,对于愿意验证或进一步研究本章所解释的概念的读者来说,可能会感兴趣。

需要注意的是:这些实验涉及到使用测试驱动,并通过更新处理器寄存器来调整内核代码的执行,所以有很高的系统崩溃的风险。最好是使用虚拟机进行这些测试。

要解决的主要问题是如何使所需的事件同时发生,例如 如何取消一个页面的提交,或者在分页过程中引起一个碰撞错误。

为了解决这个问题,我们可以从观察MiWaitForlnPageComplete+Oxbd开始,这是调用KeWaitForSingleObject,VMM等待_mmpfn ...ul。事件成为信号。在调用的那一刻,rex被设置为该事件的地址。我们开发了一个测试用的内核模式驱动,名为WrkEvent,它创建了一个包含5个事件的数组,可用于这些测试。因此,我们可以把断点放在MiWaitForlnPageComplete+0xbd处,并将rcx设置为WrkEvent控制的一个事件的地址。只要这个事件没有发出信号,开始页面置入的线程就会一直暂停,就像读操作还在进行一样。这使得我们有可能造成各种碰撞,并分析VMM的行为。

WrkEvent(WrkEvCIient)的客户端程序允许交互式地设置或清除其事件,一个名为MemColls的应用程序在不同的线程中执行内存访问操作,以引起所需的碰撞。

重要提示:以下章节描述的测试是在关闭UAC的系统上进行的。

4.5.2 Test 驱动和他的客户端

注意:这里描述的测试驱动只适用于Windows 7 x64 RTM(即SPl前,没有安装更新)。它在任何其他版本的Windows上都会崩溃,因为它使用固定的偏移量调用ntkrnlmp.exe的未导出函数,只在特定版本上有效。

驱动程序(WrkEvent.sys)和客户端(WrkEvClient.exe)必须被复制到同一目录下。客户端不需要任何命令行参数,启动后,它加载驱动程序,然后显示一个选项菜单。

数字签名的事情就不提了。挂上调试器,禁用数字签名就可以加载驱动。

WrkEvCIient程序显示一个简单的菜单,其中有打印事件地址到调试器控制台、发出事件信号和清除事件的选项。菜单选项是不言自明的,在执行完要求的操作后,程序会返回到菜单本身。当选择设置或清除一个事件的选项时,程序要求提供要采取行动的事件的基于0开始的索

当选择了打印对象地址选项时,驱动程序将地址打印到调试器控制台,如上图所示。输出的内容包括:

  • 5个事件地址
  • 事件指针数组的地址
  • gate地址,KGATE的实例。以后的章节会用到。

在接下来的章节中描述的测试中,将只使用事件地址。

注意事项

#define KE_INITIALIZE_GATE_OFFSET               0x70980
#define KE_SIGNAL_GATE_BOOST_PRIORITY_OFFSET    0x2eba0

PCHAR pKeSetEvent = (PCHAR)&KeSetEvent;
    g_pKeSignalGateBoostPriority = (PKeSignalGateBoostPriority)(pKeSetEvent - KE_SIGNAL_GATE_BOOST_PRIORITY_OFFSET);
    g_pKeInitializeGate = (PKeInitializeGate)(pKeSetEvent - KE_INITIALIZE_GATE_OFFSET);

驱动代码里使用了KeSetEvent来定位两个未导出函数KeInitializeGateKeSignalGateBoostPriority

因此要避免的蓝屏首先就是这个指针的修复,这可以将ntoskrnl.exe拖到在IDA里进行操作。

这里可以看到KeInitializeGate的地址是1400085F0。

KeSetEvent的地址是140084600

KeSignalGateBoostPriority的地址是14004039C

那么相应的Offset就能重新计算出来了

KE_INITIALIZE_GATE_OFFSET = 140084600 - 1400085F0 = 0x7c010

KE_SIGNAL_GATE_BOOST_PRIORITY_OFFSET = 140084600 - 14004039C = 0x44264

#define KE_INITIALIZE_GATE_OFFSET               0x7c010
#define KE_SIGNAL_GATE_BOOST_PRIORITY_OFFSET    0x44264

在完成修订后,内核调试下,在DriverEntry入口下断,检查是否正确。

bu WrkEvent!DriverEntry
1: kd> ?? g_pKeInitializeGate
<function> * 0xfffff800`044175f0
1: kd> u 0xfffff800`044175f0
nt!KeInitializeGate:
fffff800`044175f0 83610400        and     dword ptr [rcx+4],0
fffff800`044175f4 488d4108        lea     rax,[rcx+8]
fffff800`044175f8 c60107          mov     byte ptr [rcx],7
fffff800`044175fb c6410101        mov     byte ptr [rcx+1],1
fffff800`044175ff c6410206        mov     byte ptr [rcx+2],6
fffff800`04417603 48894008        mov     qword ptr [rax+8],rax
fffff800`04417607 488900          mov     qword ptr [rax],rax
fffff800`0441760a c3              ret
1: kd> ?? g_pKeSignalGateBoostPriority
<function> * 0xfffff800`0444f39c
1: kd> u 0xfffff800`0444f39c
nt!KeSignalGateBoostPriority:
fffff800`0444f39c 48895c2410      mov     qword ptr [rsp+10h],rbx
fffff800`0444f3a1 48896c2418      mov     qword ptr [rsp+18h],rbp
fffff800`0444f3a6 56              push    rsi
fffff800`0444f3a7 57              push    rdi
fffff800`0444f3a8 4154            push    r12
fffff800`0444f3aa 4155            push    r13
fffff800`0444f3ac 4156            push    r14
fffff800`0444f3ae 4883ec20        sub     rsp,20h

使用DbgView.exe开启内核调试Verbose输出,Windbg就能看到了

记得去掉SXS.DLL的输出日志:

1: kd> ed nt!Kd_FUSION_Mask 0
1: kd> ed nt!Kd_SXS_Mask 0

下面是WrkEvent.sys的输出日志

[Log]: WrkEvent - Work event driver, compiled Apr 28 2022 16:42:13
[Log]: WrkEvent - Device created
[Log]: WrkEvent - Work event driver successfully loaded.
[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA8032652ED8
[Log]: WrkEvent[1] address: 0XFFFFFA8032652EF0
[Log]: WrkEvent[2] address: 0XFFFFFA8032652F08
[Log]: WrkEvent[3] address: 0XFFFFFA8032652F20
[Log]: WrkEvent[4] address: 0XFFFFFA8032652F38
[Log]: Ev ptr array address: 0XFFFFFA8032652F50
[Log]: Gate address 0XFFFFFA8032652F78
[Log]: Device closed

4.5.3 MemColls

MemColls是一个简单的测试程序,它可以进行一系列的内存操作,如访问内存的范围,改变保护,取消提交等。它显示一个简单的菜单,每个操作都有不言自明的选项。要求的操作在一个新创建的线程中进行。这使得它有可能,例如,启动一个导致页面置入的内存访问操作,通过一个驱动控制的事件暂停页面置入,并在MemColls的同一实例中启动其他碰撞操作。主菜单由一个专门的线程管理,因此在置入线程暂停时,程序继续响应命令。

启动后,MemColls保留并提交一个虚拟内存范围。它的命令行需要两个参数,以字节为单位的范围大小和它的保护,后者必须是VirtualAllocExflProtect参数的有效数值,例如:

    PAGE_READWRITE 4
    PAGE_READONLY  2

这两个参数都可以在命令行中指定为十六进制或十进制数量。纯数字被解释为十进制值,而0x...形式被解释为十六进制值。

可通过菜单的操作作用于分配的区域。

两个操作(c和t)控制MemColls在执行其操作前是否导致中断进入调试器。当中断操作被激活时,MemColls在执行所请求的操作之前调用DebugBreak API,它在将要执行的线程的上下文中这样做。这时,调用的线程是当前线程(MemColls是当前进程),这使得检查MemColls的PTE更容易(例如,不需要用Iprocess /P或/i切换到它),并设置只对该线程有效的断点,用 bp /t @thread ...

@$thread 是 Windbg的伪寄存器,表示当前线程。这种方式的坏处就是如果断点是激活的,并且调试器没有连接到系统上,MemColls在调用DebugBreak时就会崩溃。

当前的中断状态与菜单和存储区域地址一起显示。

4.5.4 例子:挂起一个置入操作

前置条件:

  • 系统处于内核调试
  • WrkEvent.sys, WrkEvClient.exe, MemColls.exe 在同一目录了。

接下来按照下面的步骤,我们就能挂起一个页面置入操作:

  • 运行WrkEvClient;最好是在一个单独的cmd进程里敲入下面的命令,管理员权限启动的cmd.exe:
start WrkEvClient.exe
  • 使用p命令打印事件地址:
[Log]: WrkEvent - Work event driver, compiled Apr 28 2022 17:23:11
[Log]: WrkEvent - Device created
[Log]: WrkEvent - Work event driver successfully loaded.
[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA80326EACF8
[Log]: WrkEvent[1] address: 0XFFFFFA80326EAD10
[Log]: WrkEvent[2] address: 0XFFFFFA80326EAD28
[Log]: WrkEvent[3] address: 0XFFFFFA80326EAD40
[Log]: WrkEvent[4] address: 0XFFFFFA80326EAD58
[Log]: Ev ptr array address: 0XFFFFFA80326EAD70
[Log]: Gate address 0XFFFFFA80326EAD98
[Log]: Device closed
  • 运行MemColls指定内存区域大约为物理内存大小的%90。4GB的话,大约就是3,865,470,566字节。
  • 选择向整个区域进行写入。这会让VMM为区域强制映射物理页。当MemColls访问整个范围时,虚拟页可能就会被置出用来释放物理内存。
  • 选择激活断点模式选项
  • 选择访问子范围选项来访问一个内存子范围,并输入区域的起始地址,大小为1页(0x1000)或更小,一个内存访问类型(读或写,任何都可以)。

由于breaking现在是激活的,调试器将接管控制权,即将执行内存访问的线程是当前线程。我们现在可以检查范围第一页的PTE,它很可能会被换掉。!pte扩展的输出应该是这样的:

0: kd> !pte 13F8F0000
                                           VA 000000013f8f0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00020    PDE at FFFFF6FB40004FE0    PTE at FFFFF680009FC780
contains 02E00000601D2867  contains 02F0000060AD3867  contains 0380000060DD4867  contains 0000486500000080
pfn 601d2     ---DA--UWEV  pfn 60ad3     ---DA--UWEV  pfn 60dd4     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 4865
                                                                                  Protect: 4 - ReadWrite

提示:如果页面处于transition,试着用一个更大的区域重复前面的步骤。关键是要有一个足够大的区域,以迫使VMM repurpose它的一部分。

在这个阶段,通过输入以下信息来记录当前进程和当前线程的地址是非常有用的

0: kd> ? @$proc
Evaluate expression: -6046469066656 = fffffa80`325bf060
0: kd> ? @$thread
Evaluate expression: -6046459684000 = fffffa80`32eb1b60

这两个是_EPROCESS_ETHREAD的实例,后面可以用于检测进程的状态。

  • 检测一些更高地址的PTE,比如说,13FA31000,13FA32000,记住,一个PTE映射0x1000字节。它们很可能在相邻的偏移处被换出。
0: kd> !pte 13F8F1000
                                           VA 000000013f8f1000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00020    PDE at FFFFF6FB40004FE0    PTE at FFFFF680009FC788
contains 02E00000601D2867  contains 02F0000060AD3867  contains 0380000060DD4867  contains 0000486600000080
pfn 601d2     ---DA--UWEV  pfn 60ad3     ---DA--UWEV  pfn 60dd4     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 4866
                                                                                  Protect: 4 - ReadWrite

0: kd> !pte 13F8F2000
                                           VA 000000013f8f2000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00020    PDE at FFFFF6FB40004FE0    PTE at FFFFF680009FC790
contains 02E00000601D2867  contains 02F0000060AD3867  contains 0380000060DD4867  contains 0000486700000080
pfn 601d2     ---DA--UWEV  pfn 60ad3     ---DA--UWEV  pfn 60dd4     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 4867
                                                                                  Protect: 4 - ReadWrite
  • VMM等待_MMPFN.u.Event1的调用处,用以下命令为当前线程设置一个断点:

bp /t @$thread nt!MiWaitForInPageComplete+0xbd

  • 恢复执行。当断点被击中时,VMM已经建立了簇读取,并且即将等待读取操作的完成。发生错误的PTE现在处于transition状态,它之后的PTE很可能也处于transition,因为它们已经被包含在簇中。
0: kd> r
rax=0000000000000000 rbx=0000000000000000 rcx=fffffa80315fd5b0
rdx=0000000000000009 rsi=0000000000000001 rdi=fffffa80315fd590
rip=fffff80004500a2d rsp=fffff880061597d0 rbp=fffffa80315fd650
 r8=0000000000000000  r9=0000000000000000 r10=fffff80004651c80
r11=fffff88006159790 r12=0000000000000000 r13=fffffa80315fd680
r14=fffff88006159938 r15=fffffa80316385b1
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0000  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
nt!MiWaitForInPageComplete+0xbd:
fffff800`04500a2d e8de85fdff      call    nt!KeWaitForSingleObject (fffff800`044d9010)

这里的rcx是从_MMPFN.u1.Event中获取的。记住,这也是支持块内部的+0x20处的成员,所以,从rcx开始,可以检测支持块的其他部分。

这个阶段的线程调用栈如下:

1: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff880`092987d0 fffff800`044a813f     nt!MiWaitForInPageComplete+0xbd
01 fffff880`092988b0 fffff800`0448ea8a     nt!MiIssueHardFault+0x28b
02 fffff880`09298980 fffff800`0447e76e     nt!MmAccessFault+0x146a
03 fffff880`09298ae0 00000001`3fcf1466     nt!KiPageFault+0x16e
04 00000000`005dfbf0 00000001`3fcf4320     MemColls!AccessMemory+0x66 [F:\cpp\WmipCodeSamples\MemColls\MemColls.cpp @ 46] 
05 00000000`005dfbf8 00000000`0000099c     MemColls!`string'
06 00000000`005dfc00 00000001`3fd00000     0x99c
07 00000000`005dfc08 00000001`3fd01000     0x00000001`3fd00000
08 00000000`005dfc10 00000000`00000000     0x00000001`3fd01000

Hint: 如果调试器没有使用MemColls的符号,那么输入.reload /user

还要注意的是,MemColls是在一个独立的线程中重印菜单的,而这个线程是执行内存访问的线程,也是打到断点的线程。当这种情况发生时,菜单处理线程可能正处于列出菜单选项的过程中,由于整个系统在调试器控制时被冻结,我们可能最终得到一个部分打印的菜单。这并不意味着出了问题:当我们恢复执行时,MemColls将完成菜单的显示。

  • 设置rcx为测试驱动所控制的一个事件的地址,也就是替换掉当前的将要等待的事件。

    0: kd> r rcx=0XFFFFFA80326EACF8
    0: kd> r
    rax=0000000000000000 rbx=0000000000000000 rcx=fffffa80326eacf8
    rdx=0000000000000009 rsi=0000000000000001 rdi=fffffa80315fd590
    rip=fffff80004500a2d rsp=fffff880061597d0 rbp=fffffa80315fd650
     r8=0000000000000000  r9=0000000000000000 r10=fffff80004651c80
    r11=fffff88006159790 r12=0000000000000000 r13=fffffa80315fd680
    r14=fffff88006159938 r15=fffffa80316385b1
    iopl=0         nv up ei pl zr na po nc
    cs=0010  ss=0000  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    nt!MiWaitForInPageComplete+0xbd:
    fffff800`04500a2d e8de85fdff      call    nt!KeWaitForSingleObject (fffff800`044d9010)
  • 删除断点(例如bc* 删除所有断点)。这一点很重要,因为尽管它对当前的线程进行了过滤,但它使系统的速度相当慢。这表明,断点实际上导致了调试器在每个线程中达到指令时都要进行控制。因此,是调试器在断下后检查了过滤条件,并在后者不被满足时自动恢复了执行。

  • 恢复执行。正在访问该页面的线程现在将被冻结,直到我们向驱动客户端发出事件信号。同时,我们可以用MemColls启动碰撞操作(页面错误、取消提交等)。

接下来,我们手动让调试器中断下来,查看我们冻结的线程:

  • 第一步是切换内存上下文到MemColls

    0: kd> .process /P fffffa80`325bf060
    Implicit process is now fffffa80`325bf060
  • 接下来设置我们的当前线程

    0: kd> .thread fffffa80`32eb1b60
    Implicit thread is now fffffa80`32eb1b60
  • 最后打印调用栈

0: kd> k
  *** Stack trace for last set context - .thread/.cxr resets it
 # Child-SP          RetAddr               Call Site
00 fffff880`06159560 fffff800`044d6992     nt!KiSwapContext+0x7a
01 fffff880`061596a0 fffff800`044d91af     nt!KiCommitThreadWait+0x1d2
02 fffff880`06159730 fffff800`04500a32     nt!KeWaitForSingleObject+0x19f
03 fffff880`061597d0 fffff800`044f913f     nt!MiWaitForInPageComplete+0xc2
04 fffff880`061598b0 fffff800`044dfa8a     nt!MiIssueHardFault+0x28b
05 fffff880`06159980 fffff800`044cf76e     nt!MmAccessFault+0x146a
06 fffff880`06159ae0 00000001`3f8e1466     nt!KiPageFault+0x16e
07 00000000`004bfb70 00000001`3f8e4320     MemColls!AccessMemory+0x66 [F:\cpp\WmipCodeSamples\MemColls\MemColls.cpp @ 46]
08 00000000`004bfb78 00000000`000001b4     MemColls!`string'
09 00000000`004bfb80 00000001`3f8f0000     0x1b4
0a 00000000`004bfb88 00000001`3f8f1000     0x00000001`3f8f0000
0b 00000000`004bfb90 00000000`00000000     0x00000001`3f8f1000

可以发现线程因为等待事件而在KeWaitForSingleObject中挂起。

为了解冻线程,我们必须让我们的驱动客户端激活这个事件。

有趣的现象是,若此时我们选择退出选项来终止MemColls,这个进程并不会退出,直到我们激活事件。在完成读操作之前,Windows不会终止该线程,因此也不会终止该进程。

5 分页结构的分页

5.1 基本概念

分页结构(即PDPTPDPT)本身是可分页的,这意味着,对于一个给定的VA来说,它们中的一个或多个可以缺失。例如,PT可以缺失,这意味着由VA选择的PDE将有P位清零;PTPD也可以缺失;最后,PDPT也可以缺失,因此该地址只存在一个无效的PML4项。

当这种情况发生时,一个分页结构(PS)可能因为几种原因而丢失;最明显的是当我们第一次访问由它映射的VA时,所以PS本身从未被初始化。然而,这并不是唯一的情况:存储PS的物理页面可以像其他页面一样被重复使用。

在下文中,我们将使用术语叶子页(leaf page)来指代存储虚拟页内容的物理页,因为这样的页是分页结构层次树中的一片叶子,其根是PML4。一个叶子页存储了在一个给定的VA可见的内容(如程序数据、程序代码等)。相反,一个分页结构页存储的是用于将VA映射到其叶子页的分页结构。

我们已经知道,叶子页面会经历一个生命周期,例如,它们会变成Active,然后是Standby/Modified,等等。PS页也遵循同样的周期:它们可以被移到transition list中,重新使用并在以后的时间里恢复。因此,一个已经被初始化和使用的分页结构可能会丢失,因为它的物理页正处于transition状态,或者它的内容已经被换出去了。

牢记当一条指令引用一个所有PS都存在的VA,但叶子页不存在,即PTEP位清零时,处理器抛出一个页面错误异常,CR2设置为发生错误的VA

现在假设一个VA被引用,PDPTPD是存在的,但是PDEP位是清零的,也就是说,PTPTE是丢失的。处理器产生了一个页面错误异常,CR2仍然设置为指令所引用的VA,而不是缺失的PTEVA。当我们想到使PTE在某个VA上可见的是PML4的自动项时,这就变得很明显了,这并不是处理器架构所定义的东西。可以想象,我们可以改变自动项,从虚拟空间中完全取消映射所有的PS,但是它们仍然会被用来映射指令所引用的VA。简而言之,处理器对PS可见的VA一无所知,只是简单地将CR2设置为指令试图访问的VA,对其进行虚拟到物理的转换是不可能的。

这意味着页面错误处理例程的工作是确定错误的原因:是否只是一个缺失的叶子页或PS层次结构中的其他层次。MmAccessFault在处理错误的早期就这样做了:它从PML4E开始检查层次结构的每一层,并检查P位是否被设置。如果不是这样,它将采取适当的措施使该项有效(例如,将其指向一个全新的页面或检索一个先前被删除的页面),然后转到下一级,直到PML4EPDPTEPDE都有效并指向一个物理页面。之后,叶子页的处理就像前面解释的那样。

5.2 分页结构的首次初始化

当这种情况发生时,父结构的项被设置为0。例如,PML4E设置为0意味着PDPT必须被初始化。这也意味着被置零的项下面的所有层次结构也必须被初始化,因为被置零的项所映射的所有虚拟范围以前从未被访问过。在前面的例子中,PDPT必须被初始化,所以它的所有子PDs和它们的子PTs也从未被初始化过。当然,这并不意味着VMM将为初始化512个PDs和为每个PD初始化512个PTs,因为这意味着仅分页结构就要消耗262,144个物理页或1GB的物理内存。它将只初始化映射错误地址所需的PDPT

PS项设置为0也可能是由于指令试图访问一个无效的地址(一个没有被正确保留和提交的区域),所以VMM调用MiCheckVirtualAddress来检查VADs,如果是这种情况,会引发一个访问违规异常。本节的其余部分描述了地址有效时的情况。

对于层次结构中发现的每个PS项为0,MmAccessFault调用MiDispatchFault,后者又调用MiResolveDemandZeroFault。后者分配一个新的物理页的方式与分配一个叶子页的方式基本相同,考虑到缓存的颜色,考虑到页面列表搜索的优先级等等。一个新的分页结构页必须填入0,就像叶子页一样,因为这代表了它的项是未初始化的,也就是说,一个子分页结构从未为它们创建过。这样,当MmAccessFault移动到层次结构的下一级时,就会发现另一个被清零的条目,并执行同样的步骤。这样一直持续到PT被分配并且PDE指向它为止。之后,错误处理将以前讲过的描述进行。

使用相同的逻辑来映射虚拟地址和分页结构是通过分页结构在虚拟地址空间中的映射方式来实现的。一个VA和它的PTEVA之间的关系与PTEVA和映射PTPDEVA之间的关系相同;这也适用于PDPTE/PDPML4E/PDPT

有趣的是MiCheckVirtualAddress实际上可以被多次调用:为层次结构中每一个缺失的层次调用一次(在最坏的情况下,PDPTPDPT可能全部缺失),当发现PTE设置为0时再调用一次。这有点低效,但它只发生在第一次访问一个页面的时候。它不会发生,例如,当分页结构被换出时:不需要调用MiCheckVirtualAddress,因为它已经发生了首次错误。

对于一个PS来说,_MMPFN.u3.e1.PageLocation被设置为_MMLISTS.ActiveAndValid,因为它发生在一个叶子页面上。

一个新的PS被添加到进程工作集,其方式与叶子页相同:一个项被添加到分页结构的VPN的工作集列表中。

我们可以看到这种项是每个进程的WSL的一部分。作为一个例子,考虑notepad.exe实例的以下状态:

0: kd> !process fffffa8032b6e5d0 7
PROCESS fffffa8032b6e5d0
    SessionId: 1  Cid: 0910    Peb: 7fffffdb000  ParentCid: 0914
    DirBase: 96ab6000  ObjectTable: fffff8a002b4cc60  HandleCount:  58.
    Image: notepad.exe
    VadRoot fffffa8032dd5770 Vads 59 Clone 0 Private 355. Modified 2. Locked 0.
    DeviceMap fffff8a001c83a60
    Token                             fffff8a001dc37f0
    ElapsedTime                       00:00:16.598
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         167552
    QuotaPoolUsage[NonPagedPool]      6960
    Working Set Sizes (now,min,max)  (1503, 50, 345) (6012KB, 200KB, 1380KB)
    PeakWorkingSetSize                1503
    VirtualSize                       82 Mb
    PeakVirtualSize                   82 Mb
    PageFaultCount                    1544
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      424

我们可以使用!vad看看这个虚拟地址空间,下面是裁剪的输出结果:

: kd> !vad fffffa8032dd5770
VAD             Level         Start             End              Commit
fffffa8032723da0  5              10              1f               0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8032d58450  4              20              26               0 Mapped       READONLY           Pagefile section, shared commit 0x7
fffffa8032639150  5              30              33               0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa8031bbe890  3              40              41               0 Mapped       READONLY           Pagefile section, shared commit 0x2
fffffa80329d2670  5              50              50               1 Private      READWRITE
fffffa80326fe6f0  4              60              c6               0 Mapped       READONLY           \Windows\System32\locale.nls
...

最后一项告诉我们虚拟范围0x50000-0x50FFF是有效的,下面是这个范围分页结构的状态:

0: kd> .process /P fffffa8032b6e5d0
Implicit process is now fffffa80`32b6e5d0
.cache forcedecodeptes done
0: kd> !pte 50000
                                           VA 0000000000050000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000280
contains 4030000095E28867  contains 176000007DB2F867  contains 4000000078430867  contains 8180000072736867
pfn 95e28     ---DA--UWEV  pfn 7db2f     ---DA--UWEV  pfn 78430     ---DA--UWEV  pfn 72736     ---DA--UW-V

这告诉我们,VA已经被映射了,而且转换它所需的所有PS都已到位。如果我们转储 WS list,我们看到确实有PS的项,输出省略了大部分内容:

0: kd> !wsle 1

Working Set @ fffff70001080000
    FirstFree      5df  FirstDynamic        5
    LastEntry      679  NextSlot            5  LastInitialized      76e
    NonDirect        0  HashTable           0  HashTableSize          0

Reading the WSLE data ..................
...
fffff6fb40000009          0        0        1  <-- entry for the PD
...
fffff68000000009          0        0        1  <-- entry for the PT
...
fffff6fb7da00009          0        0        1  <-- entry for the PDPT

此外,我们已经知道,PML4本身的项总是存在于列表中。

当一个PS被初始化时,它的共享计数(也就是这个PS页面的__MMPFNu2.ShareCount)被设置为1。每当一个PS的项变得有效时,共享计数就会增加,所以它通常等于有效项的数量加1。

此外,PS_MMPFNUsedPageTableEntries在每次一个项变得有效并链接到一个子页时被递增。这个成员和共享计数之间有一个重要的区别:当子页首先被移到一个transition list,然后被重新利用时,一个有效的项后来可能变得无效。当重新利用发生时,共享计数会被递减(我们将在下一节再讨论这个问题),但UsedPageTableEntries不会被递减,因为该项仍在使用中。当内存被释放,项真正变成未使用时,后者才会被递减。另外,UsedPageTableEntries似乎并不用于叶子页,通常对它们设置为0。

UsedPageTableEntries被发现在_MMPFN实例中为0,用于映射系统地址的PS

5.3 分页结构页面从Active到Modified或者从Active到Standby的转换

如果我们首先考虑一个ActivePT,我们知道它有一个共享计数,从1开始,然后在每次它的一个项变得有效时被递增。

最终,作为VMM修剪活动的一部分,一个项所指向的叶子页被移到ModifiedStandby中。当这种情况发生时,共享计数不会被递减。为了理解这个原因,我们必须考虑到共享计数是用来决定PT本身何时可以被移到ModifiedStandby状态:共享计数设置为1意味着PT可以改变状态。在一个PS进入其中一个transition状态后,VMM可以将其作为正常操作的一部分重新使用,而不需要进一步更新其叶子页的_MMPFN。这带来了两个问题。

其一,对于StandbyModiifed叶子页面_MMPFN.u4.PteFrame被设置为PTPFN。如果VMM要重新使用PT,存储它的物理页将被重新用于其他方面,而叶子页的_MMPFNs仍然指向它。

其二,假设叶子页最终进入Standby状态,后来被重新利用;指向它的PTE必须被更新,记录到它的分页文件偏移。如果在这个阶段,VMM已经重新使用了PT,它将不得不从分页文件中重新加载它来更新PTE

只有当所有使用的PTEs都指向本身已被重新利用的叶子页时,即它们指向分页文件时,重新利用PT才能避免这些问题。在这种情况下,transition list中就不会有_MMPFNs指向PT,因为它们都被重新利用了。原则上是当一个叶子页被重新利用时,VMM会减少PT的共享计数,而不是当它被移到一个transition list时。

因此叶子页遵循它们的生命周期,从ActiveStandby/Modiifed,并最终被重新利用,同时PT共享计数减少。当它下降到1时,VMM知道PT有资格被自己移动到Modified状态。由于PTWS列表中有一个项,它最终将作为WS修剪或页面替换的一部分被移动。

在这个过程中,_MMPFN.UsedPageTableEntries并没有被递减,它一直在追踪有多少项在使用,不管这些项是指向被换掉的叶子页。也有可能VMM使用这些信息来确定从工作集移除页面的优先级:UsedPageTableEntries等于0的叶子页在PT之前被移除;移除一个页面意味着取消4kB的虚拟范围的映射,而PT覆盖2MB的范围,所以后者更可能很快被需要换回来。这个假设还没有被作者验证过。

到目前为止所概述的过程也适用于PS层次结构的上层:当一个PT被重新利用时,原来指向它的PDE被更新以存储分页文件的偏移量,PD共享计数被递减;当它下降到1时,PD可以被重新利用,从而递减PDPT共享计数,依此类推。

在 "Modified "链表中的PS页面会像其他页面一样被移到 "Standby "链表中:MPW会将其内容写入分页文件,并将该页面标记为clean。在那里,如果出现对物理内存的需求,该页可以被重新利用。

5.4 从Transition状态或者分页文件中返回一个分页结构

迟早会发生一个页面错误,其中的一个PS要么在transition list上,要么被换出去了。从上一节描述的逻辑来看,我们可以理解为第一个缺失的PS下面的所有PS都被换出去了,例如,如果PDPT存在而PD缺失,PT被换出去了(但PD仍然可以处于transition状态)。同样地,如果PDPT缺失,PDPT也被换出去了。

VMMPML4E开始检查整个层次结构,如果该项的P位清零,则调用MiDispatchFault。它以我们在前几章中描述的方式处理ModifiedStandby和换出的叶子页。当它为一个叶子页被调用时,后者被一个PTE指向;当它为一个PS页被调用时,需要的页被一个PDE、一个PDPTE或一个PML4E指向。这些项具有与PTE相同的布局,MiDispatchFault执行的步骤与它对叶子页所做的基本相同,以使引用的PxE有效。如果条目处于transition,它调用MiResolveTransitionFault;如果内容被换出,它调用MiResolvePageFileFault,并返回一个NTSTATUS,指示其调用者调用MilssueHardFault。后面的函数又从分页文件中读取页面。这与对叶子页执行的过程相同,结果是PxE被指向存储PS的物理页;这是对层次结构的每一级直到PDE所做的。一旦PT回到原位,错误处理的后续流程就像前面几章中描述的那样。

5.5 分页结构的页面状态图

图37显示了PS页的状态图。它与叶子页的状态图相似,但没有由错误触发的状态转换,因为分页结构从未被 "faulted back",而是由VMM根据需要进行恢复。

Notes

  1. 只有当所有的项都未使用或者是分页文件中换出的内容时才会移除PS

免费评分

参与人数 12吾爱币 +13 热心值 +9 收起 理由
kiseyzed + 2 + 1 脑瓜子嗡嗡的
marlborogolo + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
shadmmd + 1 谢谢@Thanks!
huihua + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
haonanyxp + 1 已经处理,感谢您对吾爱破解论坛的支持!
gaosld + 1 + 1 谢谢@Thanks!
zamliage + 1 不明觉厉!
水到渠成的执着 + 1 + 1 用心讨论,共获提升!
hxw0204 + 1 + 1 热心回复!
huayugongju + 1 + 1 谢谢@Thanks!
411183343 + 1 谢谢@Thanks!

查看全部评分

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

推荐
xnhlover 发表于 2023-2-19 17:36
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00018    PDE at FFFFF6FB40003080    PTE at FFFFF68000610570
contains 00C000000E8FC867  contains 6270000009A82867  contains 66D00000236D3867  contains 00022E8500000080
pfn e8fc      ---DA--UWEV  pfn 9a82      ---DA--UWEV  pfn 236d3     ---DA--UWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: 20e85
                                                                                  Protect: 4 - ReadWrite
3: kd> .thread
Implicit thread is now fffffa80`01d5f060
你好,大佬,请教一个奇怪的问题, 为什么我在测试windows7这个分页文件缺失的时候,发现pte指向的偏移不正确呢,pte本身数据表明的偏移明明是22e85,但wdbg计算出来确是20e85,经过都取文件pagefile.sys,20e85才是正确的
那为什么不是22e85,还有个问题就是有的pte显示的与windbg显示一样, 对于这2个问题,我确信是在我测试的进程上下文中
沙发
lovehfs 发表于 2022-5-8 10:32
3#
waimenlu 发表于 2022-5-12 09:34
4#
ycxlsxb 发表于 2022-5-12 16:13
谢谢楼主分享!虽然一知半解。
5#
liuzijiaqq 发表于 2022-5-13 12:16
好高端,完全看不懂
6#
pheigo 发表于 2022-5-14 11:02
好像很厉害的样子,但却看不明白
7#
cookieandww 发表于 2022-5-15 12:35
好复杂,看不懂!
8#
xu86167789 发表于 2022-5-15 14:55
感谢楼主分享!!!!!!
9#
fuliyejishu 发表于 2022-5-15 18:47
根本看不懂,但应该很高级
10#
ningrui 发表于 2022-5-19 09:36
好高端,完全看不懂
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-21 17:08

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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