吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4580|回复: 15
收起左侧

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

[复制链接]
BeneficialWeb 发表于 2022-6-19 11:11

用户范围内存管理 Final

1 用户范围共享内存

1.1 基本概念

VMM提供的功能允许不同的进程共享一个区域的内存。这是通过设置共享进程的PTEs指向相同的物理页来实现的。VMM使用一些数据结构来追踪共享页,我们很快就会对其进行详细分析。

这些数据结构也构成了另一个VMM功能的基础:文件映射,即把虚拟地址空间的一部分映射到文件内容的能力。从广义上讲,这是通过把文件当作分页文件来做的,所以内存内容是通过置入和换出操作从文件中读取和写入的。我们将在后面描述文件映射。

1.2 共享内存API,Section对象和原型PTEs

本节是对用于实现共享内存的API和数据结构的简短概述,目的是给出一个大致的轮廓。我们将在本章后面更详细地研究这些概念。

为了分配一个共享内存区域,我们必须首先调用CreateFileMapping,它创建了一个文件映射对象,也称为section对象(我们将在下面使用后一个术语)。这个调用创建了追踪共享页面状态所需的内部数据结构,并返回一个指向section对象的句柄。仅仅创建section对象并不能使共享内存被访问:这是通过调用MapViewOfFileEx完成的,它将section对象映射到虚拟地址空间,并返回区域地址。

到目前为止,分配的内存还没有与其他人共享,然而,另一个进程可以通过在调用MapViewOfFileEx时传递一个句柄,将相同的section对象映射到其地址空间。当这样做的时候,这两个进程就会看到相同的物理内存。一个进程可以通过几种方式获得另一个进程所创建的section对象的句柄,例如,创建进程可以在调用CreateFileMapping时为section指定一个名称,第二个进程也会这样做,因此,实际上并没有创建一个section对象,而是打开现有的section对象。

这些API的名称指的是VMM的文件映射功能,因为一个section对象可以用来映射一个文件,但首先认识 "普通 "共享内存会更容易理解它的工作原理。鉴于内存共享和文件映射之间的相似性,文件映射也可以共享并不奇怪,这样多个进程就可以 "看到 "同一个文件的内容,我们将看到这是加载和共享DLLs的基础。

CreateFileMapping的第一个参数是要映射的文件的句柄,如果我们只想分配共享内存,我们只需要把它设置为INVALID_HANDLE_VALUE。这给了我们一个不与特定文件内容相联系的section对象。

一个实际映射了一个文件的section对象被说成是由文件支持的,因为它的内存页的内容是从文件中读取的,并在物理内存被重新使用时写入文件中。为了与这个术语保持一致,一个仅仅为了共享内存而创建的section对象被说成是由分页文件支持的,因为它的内容是由后者存储和重新加载的。

在用section对象创建的数据结构中,有一组特殊的PTEs,称为原型PTEs。它们是特殊的,因为它们实际上不被处理器用来转换虚拟地址,因为它们驻留在不被分页结构层次所指向的内存页中。我们仍然称它们为PTEs,因为它们是_MMPTE的实例,但是它们是工作副本,VMM使用它们来填补处理器使用的 "真正的 "PTEs。为了理解原型PTEs是如何被使用的,我们可以描述两个进程共享一个新分配的内存页的状态,此时两个进程都还没有触及该页。

Untitled.png

转换进程1的虚拟页n的PTE间接地指向一个原型PTE,这意味着它是一个无效的PTEP=0),其布局由VMM定义为指向原型PTE。我们将这种PTE格式称为原型指针PTE,它在内核类型中被定义为_MMPTE_PROTOTYPE。原型PTE又被设置为一个值,这个值意味着对于第一个接触该页的进程来说,必须对一个zeroed页进行映射。这个值是_MMPTE_SOFTWARE的一个实例,所有的成员都被设置为0,除了Protection,对于一个读/写页,保护被设置为4。

如果我们假设进程1先接触到页面,情况会发生如下变化:

Untitled 1.png

VMM将进程1的PTE和原型PTE都设置为有效,使其拥有物理页kPFN。进程1现在 "看到 "页k,因为处理器将1.n页的虚拟地址转换为k页的物理地址。

当页面被重新使用时,原型PTE会存储分页文件的偏移量;反过来,共享进程会通过他们的原始指针来引用原型PTE。

1.3 建立共享内存区域

本节更深入地描述了在建立共享区域时创建的数据结构。

1.3.1 CreateFileMapping的作用

CreateFileMapping返回的是_SECTION_OBJECT的实例引用句柄,他的Segment成员存储了_SEGMENT实例的地址。在Windows符号中,Segment成员被定义为指向_SEGMENT_OBJECT的指针,但是实际上他的内存布局是_SEGMENT

_SEGMENT.PrototypePte 存储了原型PTE数组的地址。对于共享内存来说,这个数组起始地址开始的成员被命名为_SEGMENT.ThePtes ,也就是+0x48的地方。下面是一个1GB 大小section的例子:

kd> !handle 0 3 Section

Searching for handles of type Section

PROCESS fffffa8001cbb630
    SessionId: 1  Cid: 07a0    Peb: 7fffffdc000  ParentCid: 0460
    DirBase: 2a7a0000  ObjectTable: fffff8a0025f9320  HandleCount:  13.
    Image: MemTests.exe

Handle table at fffff8a0025f9320 with 13 entries in use

0034: Object: fffff8a00184b950  GrantedAccess: 000f0007 Entry: fffff8a00279c0d0
Object: fffff8a00184b950  Type: (fffffa80019127b0) Section
    ObjectHeader: fffff8a00184b920 (new version)
        HandleCount: 1  PointerCount: 2
        Directory Object: fffff8a00607e080  Name: map
kd> dt nt!_SECTION_OBJECT fffff8a00184b950
   +0x000 StartingVa       : (null) 
   +0x008 EndingVa         : 0xfffff8a0`00000304 Void
   +0x010 Parent           : 0xb589597e`fa5875be Void
   +0x018 LeftChild        : (null) 
   +0x020 RightChild       : 0xfffff800`ffffffff Void
   +0x028 Segment          : 0xfffff8a0`02800000 _SEGMENT_OBJECT
kd> dt nt!_SEGMENT 0xfffff8a0`02800000
   +0x000 ControlArea      : 0xfffffa80`01a31a10 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : 0x40000
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : 0x40000
   +0x018 SizeOfSegment    : 0x40000000
   +0x020 ExtendInfo       : (null) 
   +0x020 BasedAddress     : (null) 
   +0x028 SegmentLock      : _EX_PUSH_LOCK
   +0x030 u1               : <unnamed-tag>
   +0x038 u2               : <unnamed-tag>
   +0x040 PrototypePte     : 0xfffff8a0`02800048 _MMPTE
   +0x048 ThePtes          : [1] _MMPTE

SEGMENT 的地址是0xfffff8a002800000 *,ProtetypePte位于+0x48的地方。这个地址上存放了原型**PTEs**的数组,也就是*_MMPTE的实例。在这个例子中,页面保护请求的是可读可写的,所以PTEs的_MMPTE_SOFTWARE`实例的Protection设置为4。

我们可以使用!pte扩展来解析第一次时的情况:

kd> dq 0xfffff8a0`02800048
fffff8a0`02800048  00000000`00000080 00000000`00000080
fffff8a0`02800058  00000000`00000080 00000000`00000080
fffff8a0`02800068  00000000`00000080 00000000`00000080
fffff8a0`02800078  00000000`00000080 00000000`00000080
fffff8a0`02800088  00000000`00000080 00000000`00000080
fffff8a0`02800098  00000000`00000080 00000000`00000080
fffff8a0`028000a8  00000000`00000080 00000000`00000080
fffff8a0`028000b8  00000000`00000080 00000000`00000080
kd> !pte 0xfffff8a0`02800048 1
                                           VA fffff8a002800048
PXE at FFFFF8A002800048    PPE at FFFFF8A002800048    PDE at FFFFF8A002800048    PTE at FFFFF8A002800048
contains 0000000000000080
not valid
 DemandZero
 Protect: 4 - ReadWrite

就像调试器显示的一样,这个值代表了一个demand-zero PTE,因为他是无效的,而且不处于transition状态,分页文件偏移的高32位(_MMPTE_SOFTWARE.PageFileHigh)存的也是0。当一个实际意义的PTE(也就是被处理器使用的PTE)被设为这样的值时,对这块映射地址的访问将会导致填满0的页被映射进来。总之,对于一个新分配的section,原型PTEs被初始化为demand-zero的。这类似于当一个区域被部分提交时,实际的PTEs与私有内存的情况。

SEGMENT.ControlArea指向_CONTROL_AREA的实例,他的Segment成员指向segment自身。一个_CONTROL_AREA可以用!ca调试器扩展命令来解析。

kd> !ca 0xfffffa80`01a31a10

ControlArea  @ fffffa8001a31a10
  Segment      fffff8a002800000  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                   0  Mapped Views                0
  User Ref                    1  WaitForDel                0  Flush Count                 0
  File Object  0000000000000000  ModWriteCount             0  System Views                0
  WritableRefs                0  PartitionId                0  
  Flags (2000) Commit 

      Pagefile-backed section

Segment @ fffff8a002800000
  ControlArea       fffffa8001a31a10  ExtendInfo    0000000000000000
  Total Ptes                   40000
  Segment Size              40000000  Committed                40000
  CreatingProcess   fffffa8001cbb630  FirstMappedVa                0
  ProtoPtes         fffff8a002800048
  Flags (80000) ProtectionMask 

Subsection 1 @ fffffa8001a31a90
  ControlArea  fffffa8001a31a10  Starting Sector        0  Number Of Sectors    0
  Base Pte     fffff8a002800048  Ptes In Subsect    40000  Unused Ptes          0
  Flags                       8  Sector Offset          0  Protection           4

控制区指向一个subsection,由_SUBSECTION的实例描述。对于共享内存(与映射文件相反),通常只有一个subsection的实例,所以它对segment已经提供的信息没有增加多少,然而它的重要性在映射文件中会变得更明显。注意基础PTE地址是如何指向原型PTE数组的。值得注意的是,最终出现在原型PTE中的保护被存储在subsection中。采用的保护值编码和以前提到的一样。

控制区没有一个明确的指向subsection的指针,subsection似乎位于前者之后,即在控制区地址的+0x80的位置(控制区的大小)。

我们可以看到,创建section对象本身并没有预留,更没有提交虚拟地址空间。相反,它导致了一组数据结构的创建,以后可以用来将共享区域映射到一个或多个进程的地址空间。区域在地址空间中被映射的VA并不存储在这些数据结构中,只有当MapViewOfFileEx被调用时才会建立。这些结构描述了一个给定大小的虚拟地址范围,而没有把它与一个特定的起始地址联系起来。

下图总结了这些数据结构之间的关系。

Untitled 2.png

1.3.2 MapViewOfFileEx的作用

#### 1. 虚拟地址映射到不同的进程中

MapViewOfFileEx将共享区域映射到调用进程的虚拟地址空间,并返回映射的起始地址。调用者可以指定该区域被映射的虚拟地址,也可以让系统选择一个合适的地址。这给我们带来了一个到目前为止我们设法避免的概念,即共享内存在不同进程中不一定被映射在同一个虚拟地址上。

MapViewOfFileEx的参数dwNumberOfBytesToMap指定了视图的大小,即被映射到section的虚拟区域的长度(有可能映射section的一个子范围)。只有在所选择的起始地址上有一个所需大小的空闲区域时,映射才会成功。由于不同的进程在其虚拟地址空间中可能有不同的空闲范围分布,VMM可能为映射选择一个不同的起始地址。即使调用者指定了一个起始地址,如果它与已经在使用的虚拟地址区域重叠,映射也会失败。原则上在不同的进程中,一个section可以而且经常会被映射在不同的虚拟地址上。

因此,当我们说两个进程访问一个section的同一页面时,我们实际上并不是说它们访问同一个虚拟地址。相反,每个进程在它自己的地址空间中访问这些虚拟地址,他们只是映射到该section到同一个物理页。在一个进程映射了一个section之后,每个映射的虚拟页都对应于从该section开始的某个偏移量,例如,第一个映射的页对应于该section的第一个0x1000字节,第二个对应于0x1000 - 0x1FFF范围,以此类推。从section开始的偏移量决定了哪个原型PTE被用来映射虚拟范围(例如,第一个PTE映射的范围偏移量在0和0xFFF之间,以此类推),最终,在虚拟地址看到的是哪个物理页。把它们放在一起,两个进程在虚拟地址上看到的是同一个物理页,而这个虚拟地址与section开始的偏移量是相同的。

为了完整起见,值得指出的是,MapViewOfFileEx也允许为映射指定一个从section起始地址开始的偏移量。因此,从映射起始地址开始的偏移量为0的地址可能在从section起始地址开始的偏移量为x。然而,当涉及到识别在一个给定的VA上看到的物理页时,重要的是来自section开始的偏移量,这必须计算出最终传递给MapViewOfFileEx的偏移量。

#### 2. 数据结构被映射创建

由于MapViewOfFileExsection页面映射到一个虚拟地址区域,它在VAD树中为该区域创建一个项。下面我们可以看到来自!vad的输出的摘录,我们的测试进程有一个1GB的映射视图:

kd> !vad fffffa8003ab9f70
VAD           Level     Start       End Commit
fffffa8001a578f0  4        10        1f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8001ab4660  3        20        2f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8001c5cc00  4        30        33      0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa8001c05610  2        40        40      0 Mapped       READONLY           Pagefile section, shared commit 0x1
fffffa8003746090  4        50        50      1 Private      READWRITE          
fffffa8001ab6d00  3        60        c6      0 Mapped       READONLY           \Windows\System32\locale.nls
fffffa8003881860  4        e0        ef      6 Private      READWRITE          
fffffa8003db30a0  1       160       25f      5 Private      READWRITE          
fffffa800388f6d0  4       310       40f     35 Private      READWRITE          
fffffa80037689e0  3       410       50f     14 Private      READWRITE          
fffffa8003b82930  5       510     4050f      0 Mapped       READWRITE          Pagefile section, shared commit 0x40000
fffffa8003ce64a0  4     76ba0     76cbe      4 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\kernel32.dll
fffffa8003b202e0  2     76dc0     76f68     12 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\ntdll.dll

这里我们有该区域的_MMVAD实例的部分内容:

kd> dt -b nt!_MMVAD fffffa8003b82930
...
   +0x010 RightChild       : (null) 
   +0x018 StartingVpn      : 0x510
   +0x020 EndingVpn        : 0x4050f
   +0x028 u                : <unnamed-tag>
         +0x000 Protection       : 0y00100 (0x4)
...
   +0x048 Subsection       : 0xfffffa80`01a31a90 
   +0x048 MappedSubsection : 0xfffffa80`01a31a90 
   +0x050 FirstPrototypePte : 0xfffff8a0`02800048 
   +0x058 LastContiguousPte : 0xfffff8a0`02a00040 
   +0x060 ViewLinks        : _LIST_ENTRY [ 0xfffffa80`01a31a80 - 0xfffffa80`01a31a80 ]
      +0x000 Flink            : 0xfffffa80`01a31a80 
      +0x008 Blink            : 0xfffffa80`01a31a80 
...

这里有几件事值得一提:

  • StartingVpnEndingVpn被设置为区域边界。
  • u.Protection被设置为该section的保护值。实际上,MapViewOfFileEx允许通过其dwDesiredAccess参数来指定对该section的期望访问权限。在这里,我们指定了读/写访问,并且由于该部分是以读/写保护方式创建的,所以读写的内部编码(即4)最终出现在VAD中。如果我们重复测试进程进行一个只读访问的请求,我们最终在VAD中得到了1的保护,这相当于只读页面保护。
  • Subsection 指向与该subsection一起创建的section
  • FirstProtetypePte指向PTE数组的起始位置

这个VAD是在调用MapViewOfFileEx的时候,把从该section开始的偏移量设置为0而产生的。非零偏移量会导致FirstPrototypePte指向与偏移量相对应的原型PTE。注意,偏移量必须是64k的倍数,因此也是页面大小的倍数。视图的第一个字节将总是一个页面的第一个字节,FirstPrototypePte指向用于映射该页面的原型PTE。

检查映射的虚拟地址的PTEs如下:

kd> !pte 510000
                                           VA 0000000000510000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000010    PTE at FFFFF68000002880
contains 007000002C282867  contains 008000002C283867  contains 0000000000000000
pfn 2c282     ---DA--UWEV  pfn 2c283     ---DA--UWEV  not valid

在这种特殊情况下,PDE是无效的,也就是说,这个VAPT还没有被分配。VMM将查看VAD,以解决第一次访问该页所引起的错误。

#### 3. 对NUMA的支持

MapViewOfFileExNuma扩展了MapViewOfFileEx的功能,允许为要使用的物理页面指定一个首选的NUMA节点。它的工作方式与VirtualAllocExNuma类似。_MMVAD.u5.VadFlags3.PreferredNode被设置为nndPreferred(传递给API的节点号)+1。页面错误处理程序使用这个值,尽可能从指定的节点分配物理页面。

1.4 首次访问共享页面

从这一节开始,我们将用类似于私有内存的方法来描述一个共享页的生命周期。在调用MapViewOfFileEx之后,共享页中的虚拟地址还没有被映射到物理地址上,就处理器而言,因为它们的PTEs仍然是无效的,甚至是缺失的。因此,我们将从对页面的第一次访问开始分析,这将导致一个页面错误。

在下文中,我们将使用硬件PTE这一术语来指实际用于将虚拟地址转换为物理地址的PTE,以区别于原型PTE,后者只是VMM使用的工作副本。在这个术语中,硬件PTE是一个8字节的槽,被处理器访问以执行虚拟到物理的转换。我们不会用硬件PTE这个术语来指代PTE内容的特定格式,例如,一个有效的PTE值,其中P位被设置,并且有一个有效的PFN。因此,一个硬件PTE可以存储不同格式的值,包括那些P位清零和其他位设置为VMM定义的值的值。

由于我们对映射到一个section的虚拟地址感兴趣,所以总是包含有两个PTE:硬件PTE和原型PTE

当第一次访问发生时,VMM分配一个零填充的页面,并更新原型和硬件PTE,使它们的P位被设置,PFN域指向物理页面。

对于一个读写映射的视图,最终的原型PTE结果如下:

_MMPTE_HARDWARE.Dirty1 = 1, 使得页面可写,这是处理器定义的R/W位。

_MMPTE_HARDWARE.Owner =0, 是页面只能在内核模式访问

_MMPTE_HARDWARE.Global =1,告诉处理器当cr3被加载时,不要无效这个页面的TLB项。这通常用于系统范围地址,在所有的地址空间中,虚拟到物理的映射是相同的。

注意:

  1. Dirty1Write 的详细介绍以前已经讲过了
  2. 我们对这些位的解释就像这是一个硬件PTE一样,然而原型PTE本身只是一个工作副本。如果一个硬件PTE被填充了这些内容,这些位会产生这里详述的效果。

硬件PTE的值如下:

_MMPTE_HARDWARE.Dirty1 = 1 如果访问是一个写操作,否则的话是0。

_MMPTE_HARDWARE.Dirty =1 如果访问时写,否则被设置为0。

_MMPTE_HARDWARE.Write = 1

_MMPTE_HARDWARE.Owner =1 ,使得页面可以被内核模式和用户模式访问。

_MMPTE_HARDWARE.Global = 0, 告诉处理器在cr3加载时无效这个页面的TLB项。

注意:

  1. 我们知道一个已经被映射以解决demand-zero的错误并且尚未写入分页文件的页面应该被认为是脏的,不管访问的类型是什么,因为页面内容的副本(即使它只是充满了零)还不存在于分页文件中。记住,"干净 "并不意味着 "从未被写入",而是其内容 "等于它的分页文件副本"。因此,将硬件PTE设置为干净的读访问似乎是错误的。然而,通过对测试程序的实验,我们可以看到,当修剪发生时,VMM将这样的页面移到Modified list中。在这个阶段,VMM至少有两条信息将该页标记为脏页:原型PTE_MMPFN,它的u3.e1.Modified被设置为1。
  2. 请注意,当涉及到硬件PTE时,OwnerGlobal的设置与用户模式范围内的地址一致。

检查分配给虚拟地址的页面的_MMPFN的内容也很重要。

_MMPFN.u2.ShareCount = 1。每当另一个进程映射该部分并访问该页面时,该计数将增加。

_MMPFN.u3.e1.Modified = 1。

_MMPFN.u3.ReferenceCount = 1。当其他进程访问同一个页面时,这个计数不会增加。无论该页是否包含在一个或多个工作集中,它的引用计数都是1。当其他组件在内存中锁定该页时,引用计数才会被增加。

_MMPFN.OriginalPte 原型PTE的初始值被拷贝到这里,比方说0x80的一个读写section。

_MMPFN.PteAddress 设置为原型PTE的地址

_MMPFN.u4.PteFrame 设置为存储原型PTE所在的物理页的PFN。

_MMPFN.u4.ProtoTypePte = 1

关于_MMPFN,有几件重要的事情要指出来。

OriginalPtePteAddressPteFrame用于管理页面的生命周期,因为它从ActiveModified/Standby,最终被重新利用。所有这些字段都指的是原型PTE,而不是硬件PTE,因为是前者指的是页面或交换文件的偏移。硬件PTEs只有在有效的时候才直接指向物理页,否则它们在生命周期的所有其他阶段都与原型PTEs有关(间接地,我们很快就会看到)。

ProtoTypePte标志是告诉管理页面的VMM组件这个页面是一个section的一部分。我们很快就会看到这个信息是如何被工作集管理器使用的。!pfn扩展(见:附录A第565页关于这个扩展的详细信息)为设置了这个位的页面打印 "Shared "标签,例如:

kd> !pfn 3607f
    PFN 0003607F at address FFFFFA8000A217D0
    flink       00000248  blink / share count 00000001  pteaddress FFFFF8A002800048
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 02A692  Active     MP      
    Modified Shared
kd> dt -b nt!_mmpfn FFFFFA8000A217D0
...
+0x028 u4               : <unnamed-tag>
      +0x000 PteFrame         : 0y0000000000000000000000000000000000101010011010010010 (0x2a692)
      +0x000 Unused           : 0y000
      +0x000 PfnImageVerified : 0y0
      +0x000 AweAllocation    : 0y0
      +0x000 PrototypePte     : 0y1
      +0x000 PageColor        : 0y000000 (0)

如果其他进程访问同一页面,相应的硬件PTE将以同样的方式被设置,并且页面共享计数被增加。请注意,要做到这一点,其他进程必须实际访问该页。简单地映射相同的部分只会导致创建VAD,但是,只要页面没有被 "触及",硬件PTE就不会被设置为页面的PFN,并且共享计数也不会被增加。

1.5 从工作集中移除一个共享页面

1.5.1 对页面处于活动状态的所有进程所执行的动作

当一个属于某section的页面从进程的工作集中删除时,VMM必须执行与适应于私有页面的不同的操作。

首先,硬件PTE必须被设置成引用prototype,而对于一个私有页来说,它必须在transition时被设置。我们可以看到,由工作集管理器执行的FreeWsleList函数会检查_MMPFN.u4.PrototypePte并采取相应的行动。

这个函数将硬件PTE设置为_MMPTE_PROTOTYPE的一个实例,我们将其称为proto-pointer,以避免与原型PTEs混淆。

proto-pointer 被设置成下面这样:

_MMPTE_PROTOTYPE.Valid = 0

_MMPTE_PROTOTYPE.ProtoType = 1

这个位的组合确定了PTE内容是一个proto-pointer

工作集管理器(WSM)还设置_MMPTE_PROTOTYPE.Protection , 这个值来自于该页的工作集列表项。当WSM在删除页面的过程中,后者仍然有一个WSLE,其中_MMWSLENTRY.Protection设置为页面保护。与VAD和各种PTE类型的情况一样,保护由与处理器定义的保护位无关的数值表示。WSM_MMWSLENTRY.Protection复制到_MMPTE_PROTOTYPE.Protection

这很有趣,因为它使改变一个section页的保护成为可能。在映射了一个section之后,在一个特定的进程上下文中我们可以使用VirtualProtectEx来改变它的一些页面保护。从广义上讲,这不是对原始保护值和新保护值的每一个组合都有效,而是当保护的改变试图限制对页面的访问时。无论如何,有些组合是允许改变的,例如从读/写到只读。

这就提出了一个问题,即当页面在工作集中,PTE有效时,在哪里存储新的保护。

对于一个私有页,当它无效时,VMM定义的保护被存储在硬件PTE中;在下文中,我们将把这个阶段的PTE内容称为软件PTE(它是_MMPTE_SOFTWARE的一个实例)。当硬件PTE最终变得有效时,软件PTE被复制到_MMPFN.OriginalPte,在那里它将最终被更新为分页文件的偏移量,仍然保留保护值。关键的一点是,只要物理页被硬件PTE引用,_MMPFN就可以用来存储软件PTE。即使在页面处于过渡期也是如此,因为硬件PTE仍然引用它。当页面被重新使用时,软件PTE值必须从_MMPFN中驱逐,因为_MMPFN实例将被更新以反映该页面被重新分配的目的。然而,在这个阶段,硬件PTE是完全可用的,因为它不需要再存储PFN,所以包括保护的OriginalPte被复制回硬件PTE本身。

如果我们对私有页使用VirtualProtectEx,软件PTE会被更新。软件PTE在硬件PTE_MMPNF.OriginalPte之间切换。

这对于一个section页来说是不可能的,因为_MMPFN是在所有进程中共享的。我们需要一个每个进程都有的(per-process)结构,在硬件PTE有效时 "停放 "软件PTE。我们不能使用VAD,因为尽管它是一个per-process结构,但它是用来存储整个虚拟区域映射到section的保护,而VirtualProtectEx允许改变区域内单个页面的保护。所以,最后,VMM使用工作集列表来解决这个问题。

可以看到,VMM似乎总是将页面保护复制到_MMWSLENTRY.Protection,并从那里复制到_MMPTE_PROTOTYPE.Protection,而不仅仅是对使用了VirtualProtectEx的页面。

_MMPTE_PROTOTYPE的另一个重要成员是ProtoAddress。正如它的名字所暗示的,它应该用来存储原型PTE的地址,但情况并非总是如此。在进一步深入了解它的使用方法之前,我们应该指出这个成员的宽度是48位,所以它不能存储一个完整的64位指针。不过这并不是一个问题,因为地址必须是规范地址,所以上面的16位只能等于第47位,可以被丢弃。当ProtoAddress实际用于存储原型PTE的地址时,完整的地址可以通过将这个值与0xFFFF000000000000进行或运算,因为原型PTE是在系统区域,其中第63-47位都为1。

对于映射在用户区的sectionProtoAddress通常不指向原型PTE,而是设置为0xFFFFFFFFFFFF,这告诉VMM,它必须查看VAD以最终解决涉及该PTE的错误。最后,VMM仍将读取原型PTE来解决故障,但它将通过查看VAD来找到它,而VAD又存储了subsection和原型PTE数组的地址。错误地址与VAD所描述的区域起始的偏移量将被用来计算原型PTE与数组起始的偏移量。值得注意的是,调试器将这样的原形指针解释为指向一个VAD

kd> !pte 520000
                                           VA 0000000000520000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000010    PTE at FFFFF68000002900
contains 02C000003266A867  contains 014000003332E867  contains 1A4000007B5C1867  contains FFFFFFFF00000480
pfn 3266a     ---DA--UWEV  pfn 3332e     ---DA--UWEV  pfn 7b5c1     ---DA--UWEV  not valid
                                                                                  Proto: VAD
                                                                                  Protect: 4 - ReadWrite

ProtoAddress设置为实际地址的Proto-pointers可以在缓存管理器映射的section中找到,但我们在此不做分析,也可以在可执行文件的sections中找到,这一点将在后面介绍。

当一个部分页面从WS中删除时,VMM也会执行以下操作:

  • 像私有页面的情况一样减少该PTE所属的PT的共享计数。
  • 减少被从WS中移除的物理页的共享计数。

只要有其他进程的页面是活动的,即有有效的硬件PTE指向该页面,原型PTE就不会被更新。

1.5.2 对页面处于活动状态的最后一个进程所执行的额外动作

当共享计数下降到0时,VMM检测到它正在从最后一个使用它的进程中删除一个活动的section页面。

这种情况发生在有一个或多个进程映射该section的视图,并且该section的某个物理页即将从使用它的最后一个地址空间中删除(例如通过WS修剪),也就是说,指向该页的最后一个有效硬件PTE即将被设置为一个原指针值。我们可以用下面的图来表示这种情况:

Untitled 3.png

当这种情况发生时,引用计数被递减。如果它仍然大于零,那么某些组件已经在内存中锁定了该页。

另一方面,如果引用计数也变成了0,这个页面就会被移到ModifiedStandby列表中,原型PTE被设置为过渡状态,这与私有页面的硬件PTE的做法相同,即它成为一个_MMPTE_TRANSITION的实例:

_MMPTE_TRANSITION.Valid = 0

_MMPTE_TRANSITION.Protection 设置为_MMPFN.OriginalPte的第5-9位。

_MMPTE_TRANSITION.Transition = 1 这个位被set后,以及Valid =0 的话那么这个PTE就被标记为处于过渡状态。

_MMPTE_TRANSITION.PageFrameNumber 设置为物理页的PFN

_MMPFN被相应地设置为在一个过渡列表中的页面,其中有:

_MMPTE.u1.Flink , _MMPTE.u2.Blink 链接到Modified或者Standby List实例。

_MMPTE.u3.ReferenceCount = 0

_MMPTE.u3.e1.PageLocation 设置为相应的列表(_MMLISTS.ModifiedPageList或者_MMLISTS.StandbyList

_MMPFN.PteAddress_MMPFN.OriginalPte_MMPFN.u4.PteFrame: 无变化,始终引用原型PTE和原来的软件PTE的值。

1.6 Section页面写到分页文件

当一个用于section的页面被写入分页文件时,会从Modified列表中转到Standby列表中。至于一个私有页_MMPFN.OriginalPte.u.Soft.PageFileHigh被设置为分页文件的偏移量。通过在_MMPFN的成员上设置访问断点,我们可以观察到页面被Modifed页面写入器异步写入(当写入正在进行时,_MMPFN.u3.e1.WriteinProgress被set),就像发生在私有页面上一样。

1.7 软错误

作为section一部分的页面的软错误可能由于几种原因发生。

一个进程可以触及一个虚拟地址,这个虚拟地址的页面已经从它的工作集中删除,但是它的原型PTE仍然有效并且指向物理页面。在这种情况下,在错误发生时,硬件PTE被设置为原指针,但原型PTE仍然有效。

类似的情况也会发生,但原型PTE和物理页处于过渡期:原型PTE实际上是无效的,但物理页在Modified 或者 Standby List中仍然可用。

一个进程映射的section第一次接触到一个页面,他的原型PTE是有效的或处于过渡期。

在所有这些情况下,原型PTE描述的是具有所需内容的物理页,这就是使该事件成为软错误的原因。如果它处于过渡期,VMM将该页变为活动状态,并更新所涉及的硬件PTE,以便它也是有效的,并指向同一个物理页。它还增加物理页的共享计数,并更新跟踪该页是脏还是干净的位。下面是一些可能的组合:

  • 如果该页在Standby List中,并且进行的是读取访问,那么_MMPFN.OriginalPte中的分页文件偏移量被保留,_MMPTE.u3.e1.Modified被清除,所以该页保持干净。硬件PTE有:

    _MMPTE.u.Hard.Dirty1 = 0,这使得该页对处理器来说是只读的,所以第一次写入尝试将导致一个错误,从而将该页的状态改为脏页。

    _MMPTE.u.Hard.Dirty = 0

    _MMPTE.u.Hard.Write = 1, 这个告诉VMM这个页面是一个可读可写的页面。

  • 如果该页在Standby List中,但进行的访问是写访问,它就会变成脏页:

    _MMPFN.OriginalPte.u.Soft.PageFileHigh = 0

    _MMPTE.u3.e1.Modified = 1

    硬件PTE是可读可写的(_MMPTE.u.Hard.Dirty1 = 1),因为不需要拦截首次写访问。

  • 如果该页在Modified List中,它过去和现在都是脏的。如果访问是读访问,硬件PTE仍然被设置为只读。

即使不列举所有可能的页面状态和访问类型的组合,一般的规则是,VMM_MMPFN中跟踪页面是干净的还是脏的,并设置硬件PTE来检测干净页面何时变脏。

1.8 Section页面的重新使用

这一部分讲的生命周期的对于私有页和共享页是相似的。指向该页的PTE被替换成_MMPFN.OriginalPte的内容,其中存储了分页文件的偏移量和VMM定义的页面保护。这就把PTE和物理页的关系解开了,物理页可以被重新用于其他方面。

对于section页面,_MMPFN指的是原型PTE,即:

  • _MMPFN.PteAddress 被设置为原型PTE的地址
  • _MMPFN.u4.PteFrame 存储包含原型PTE的页面的PFN

因此重新使用的结果是更新原型PTE,这是正确的做法,因为现在映射该section的硬件PTE被设置为VAD的原型指针,不得更改。

下图描述了再利用后的总体情况:

Untitled 4.png

1.9 向Clean Section页面进行写入

这种情况的处理方式类似于写到一个私有页面的情况。

  • 硬件PTE的Dirty1DirtyAccessed被置1。
  • 分配的分页文件被释放,_MMPFN.OriginalPte.u.Soft.PagingFileHigh 设置为0
  • 对于私有页面来说,_MMPFN.u3.e1.Modified 没有被set,。然而可以观察到,当该页从工作集中被移除时,这个位被设置;因此我们可以得出结论,VMM检查PTE位并相应地更新_MMPFN

1.10 硬错误

对于私有页来说,一个物理页被分配,并且被分页文件中检索到的内容填充。之后,原型PTE和硬件PTE都被设置为有效,并通过PFN字段指向新的物理页,该物理页也被添加到工作集中。

如果访问是读,页面和PTE被配置为干净的,否则就是脏的。

1.11 原型PTEs的分页

到目前为止,我们对与section对象实现有关的一个问题视而不见。VMM必须为section的每个页面分配一个原型PTE。例如,一个1GBsection包含了262,144个页面,因此必须在内核内存中分配相同数量的8字节PTE,占用2MB的内存。如果所有sectionPTE数组都驻留在物理内存中,将会有很大的开销。我们还没有涉及到内存映射文件,但是它们是基于section对象的,每个DLL和可执行文件都是通过内存映射来访问的。缓存管理器也会为数据文件创建section对象,所以原则上在一个运行中的系统中会有很多section对象。

VMM足够聪明,他将原型PTE分页出去,以节省物理内存。在探讨这个问题之前,我们必须先说一下什么是分页池。

1.11.1 分页池概述

分页池是一个映射到可分页内存的系统虚拟地址区域。VMM提供API(实际上,在内核模式下,设备驱动接口的缩写是DDI,所以我们从现在开始使用这个术语)来分配和取消分配池中的一个区域。分配区域的内核组件或驱动程序可以自由地访问该虚拟区域,但是必须注意其内容可以在任何时候被取消映射。对于内核模式的代码来说,有时候这点很重要,因为在某些条件下,不能产生页面错误。当代码可以产生页面错误时,它可以自由访问分页池区域,而VMM负责处理任何错误。

内核模式代码从分页池中分配的时候可以指定一个四字节的标签,以识别其分配,并由各种工具显示,以跟踪池的使用情况。一个这样的工具是!pool调试器扩展,我们将在下一节中使用它。

1.11.2 在分页池中转储Section数据结构

我们可以使用!pool扩展来确认一些用于实现内存sectons的数据结构确实是在分页池中。下面是!vad对一个进程的1GB视图section的输出结果:

kd> !vad fffffa8003bd8180
VAD           Level     Start       End Commit
...    
fffffa80038f2700  5       500     404ff      0 Mapped       READWRITE          Pagefile section, shared commit 0x40000
...

通过查看VAD,我们可以找到subsection和原型PTE数组的地址:

kd> dt nt!_mmvad fffffa80038f2700
...
   +0x048 Subsection       : 0xfffffa80`01abeab0 _SUBSECTION
   +0x048 MappedSubsection : 0xfffffa80`01abeab0 _MSUBSECTION
   +0x050 FirstPrototypePte : 0xfffff8a0`02800048 _MMPTE
   +0x058 LastContiguousPte : 0xfffff8a0`02a00040 _MMPTE
...

然后我们使用!pool扩展来查看原型PTE数组的地址:

kd> !pool 0xfffff8a0`02800048
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page fffff8a002800048 region is Paged pool
*fffff8a002800000 : large page allocation, tag is MmSt, size is 0x201000 bytes
        Pooltag MmSt : Mm section object prototype ptes, Binary : nt!mm

这告诉我们,这个地址确实是一个分页池区域的一部分,分配的标签是MmSt。 我们还可以看到,池区域的大小是0x201000,即2MB加1个页,这与segment在同一区域的事实是一致的。为了更好地理解这一点,我们可以从subsection中检索段的地址:

kd> dt nt!_subsection 0xfffffa80`01abeab0
   +0x000 ControlArea      : 0xfffffa80`01abea30 _CONTROL_AREA
...
kd> !ca 0xfffffa80`01abea30

ControlArea  @ fffffa8001abea30
  Segment      fffff8a002800000  Flink      0000000000000000  Blink        0000000000000000
...

segment位于原型PTEs数组之前,位于分页池区域的开始。正如我们之前所说,一个1GB的区域需要262144个PTEs,占用2MB。由于PTEs从区域的第一页内的+0x48开始,由于segment实例,区域必须跨越一个额外的页面来容纳它们(假设VMM在从池中分配时将区域大小四舍五入为页面大小的整数倍)。

分页池有一个相关的工作集,与用于进程地址空间的工作集不一样,它记录了哪些虚拟地址当前被映射到了物理页。因此,我们可以说,当一个物理页被用来映射池中的一个虚拟区域时,它就被添加到分页池的工作集中。

其他与section相关的数据结构不保存在分页池中。例如,如果我们使用!pool 来查看控制区的地址,我们会得到:

kd> !pool 0xfffffa80`01abea30
Pool page fffffa8001abea30 region is Nonpaged pool
 fffffa8001abe000 size:   60 previous size:    0  (Allocated)  FOCX
 fffffa8001abe060 size:   20 previous size:   60  (Free)       ViMm
 fffffa8001abe080 size:   80 previous size:   20  (Allocated)  SeTl
 fffffa8001abe100 size:   a0 previous size:   80  (Allocated)  ViMm
 fffffa8001abe1a0 size:  100 previous size:   a0  (Allocated)  MmCa
 fffffa8001abe2a0 size:  130 previous size:  100  (Allocated)  File (Protected)
 fffffa8001abe3d0 size:   90 previous size:  130  (Allocated)  Vad 
 fffffa8001abe460 size:   40 previous size:   90  (Allocated)  ReTa
 fffffa8001abe4a0 size:   90 previous size:   40  (Free)       Vad 
 fffffa8001abe530 size:  100 previous size:   90  (Allocated)  Ntfx
 fffffa8001abe630 size:   10 previous size:  100  (Free)       CcBc
 fffffa8001abe640 size:   c0 previous size:   10  (Allocated)  FMsl
 fffffa8001abe700 size:   a0 previous size:   c0  (Allocated)  Nbtl
 fffffa8001abe7a0 size:  120 previous size:   a0  (Allocated)  NbtD
 fffffa8001abe8c0 size:  160 previous size:  120  (Allocated)  Ntfx
*fffffa8001abea20 size:   d0 previous size:  160  (Allocated) *MmCa
        Pooltag MmCa : Mm control areas for mapped files, Binary : nt!mm
 fffffa8001abeaf0 size:  100 previous size:   d0  (Allocated)  MmCa
 fffffa8001abebf0 size:   30 previous size:  100  (Free)       CcWk
 fffffa8001abec20 size:   80 previous size:   30  (Allocated)  Clfs
 fffffa8001abeca0 size:   20 previous size:   80  (Free)       ViMm
 fffffa8001abecc0 size:  160 previous size:   20  (Allocated)  Ntfx
 fffffa8001abee20 size:  1e0 previous size:  160  (Allocated)  MmCi

这告诉我们控制区是在非分页池内存中,顾名思义,它是不可分页的。分页池内存的地址通常在0xFFFF8A0'000000 - 0xFFFFF8BF'FFFFFFF范围内。

1.11.3 原型PTEs的分页逻辑

一般来说,分页池(PP)物理页可以在任何时候被VMMPP工作集中删除,就像用户范围页一样,以重新使用物理内存用于其他目的。当访问PP内存时,VMM的工作是根据需要对物理内存进行fault back

然而,原型PTEs带来了一个特殊的问题。例如,考虑一个活动的section页面:原型PTE用它的PFN字段指代它,而页面的_MMPFN用它的u4.PteFrame指代包含PTE的页面。如下图所示:

Untitled 5.png

现在假设原型页(即存储原型PTEs的页面)被重复使用,将其内容保存在分页文件中,用于其他目的,例如解决demand-zero的错误。_MMPFN.u4.PteFrame仍然会指向物理页,尽管后者不再存储原型PTEs,这显然会导致灾难。仅仅是VMM可以在需要的时候恢复换出的PP内容这一事实对于原型PTE来说是不够的:VMM本身需要确保在它需要访问原型PTE的时候有相同的物理页存在。因此,存储原型PTE的页面被以一种特殊的方式处理。

当一个原型PTE被设置为有效并指向一个物理页面时,存储原型PTE的页面的共享计数被增加。这与发生在私有页上的情况没有什么不同。例如,当一个demand-zero fault被解决时,无论是对于私有页还是section页,MilnitializePfn被调用,正是这个函数增加了PTE页的共享计数。对于私有页,PTE页是实际的PT,而对于section页,PTE页是包含原型PTE的页面。这与一般的概念是一致的,即section页面的_MMPFN与原型PTE有关,所有的成员通常指的是一个私有页面的硬件PTEPteAddress, OriginalPte, u4.PteFrame)。

当它的一个PTE与它所指向的物理页脱离关系时(例如,PTE被设置为指向分页文件,或者被完全释放),原型页共享计数被递减。一般来说,当一个PTE有效或处于过渡期时,它就与一个物理页相关。同样,这与用于私有页的逻辑相同,只是应用于原型页而不是页表。

值得指出的是,VMM之外的其他内核模式组件,例如内核模式驱动,不能操纵分页池页面的共享计数:这是VMM本身的内部逻辑。

对于一个PP页,共享计数的使用方式与用户范围页的使用方式基本相同:当该页被用于映射PP虚拟地址(即添加到PP工作集)时,它被递增,当VA被取消映射时,它被递减。当共享计数下降到零时,引用计数被递减;当引用计数下降到零时,该页被移到一个过渡列表。

现在假设VMM决定从PP工作集中删除物理页集,以尝试重新使用内存,而其中有一个原型页。这个页面的共享计数将被递减,但是,只要有一个或多个PTE引用物理页面,它就不会下降到零,所以引用计数不会被递减,这个页面仍然处于活动状态。这是一个奇特的情况,因为我们有一个共享计数为非零的页面,它实际上不是任何工作集的一部分;通常,一个物理页面的共享计数等于包括它的工作集的数量。

到目前为止还不错,VMM有办法确保在需要的时候不获取该页(特别是被自己的工作集管理器获取)。然而,它必须处理与该页相对应的PP虚拟区域可能未被映射的事实。当WSM将该页从PP工作集中移除时,它将映射该页的PTE设置为过渡状态,所以试图以其常规虚拟地址访问原型PTE可能会导致页面错误。

VMM代码通过映射页面的工作VA地址来访问它,从而说明了这种可能性。例如,这发生在MiPfnShareCountlsZero函数里面,当VMM需要设置一个过渡状态的原型PTE时:原型页的PFN取自PTE映射的页面的_MMPFN.u4.PteFrame,并用于使原型页在工作虚拟范围内可见,原型PTE被更新。这个额外的工作只针对原型PTE,而硬件PTE直接在分页结构区域的虚拟地址处被更新(并且发生在MiPfnShareCountlsZero函数之外)。

VMM需要更新PTE的另一个阶段是当一个页面被重新使用时,PTE必须从过渡PTE改变为指向分页文件的软件PTE。执行这个操作的代码总是通过调用MiMapPagelnHyperSpaceWorker来映射存储在工作地址的PTE。这对硬件和原型PTE都有效:页面上的_MMPFN.u4.PteFrame成员被重新利用,用来访问相关的页表(无论是 "真实 "页表还是原型页),PTE被更新。

总之所有VMM需要接触原型PTE的阶段都是由于物理页仍然可用(因为共享计数非零),而且PP虚拟地址当前是否映射到它并不重要。

当一个页面中的所有原型PTE与其他物理页面没有关系,并且当该页面从PP工作集中移除时,共享计数下降到零,并且该页面可以安全地被移到过渡列表中。

这个逻辑补充了在PP页面上进行的常规分页活动,以管理那些可以被_MMPFNs引用的特殊页面。这是以一种优雅的方式完成的,通过两个标准的耦合:

  • 储存原型PTEs的页面共享计数增加了物理页的项数,这与硬件页表的共享计数方式相同。
  • 分页池的物理页和系统中的其他物理页一样,都要遵守共享/引用计数规则。

1.11.4 解决原型PTEs被换出时的错误

在上一节中,我们看到了VMM如何避免在无法做到的情况下将原型页带出活动状态。然而,在某些时候,原型页可以被重新利用,所以迟早会发生错误,为此原型页被置换出来。

这与私有页的分页结构被换出时发生的情况没有太大区别,处理方法是调用MiDispatchFaultrdx设置为必须被页面置入的原型PTE的地址。下面是一个调试会话的节选,其中原型PTE被换出。

该进程试图访问地址0x4F0000,该地址是一个section的一部分,其PTE是无效的:

kd> !pte 4f0000
                                           VA 00000000004f0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000010    PTE at FFFFF68000002780
contains 02C000002A1AD867  contains 014000002A234867  contains 0A2000002A12C867  contains FFFFFFFF00000480
pfn 2a1ad     ---DA--UWEV  pfn 2a234     ---DA--UWEV  pfn 2a12c     ---DA--UWEV  not valid
                                                                                  Proto: VAD
                                                                                  Protect: 4 - ReadWrite

下面是这个section区域的VAD地址:

kd> !vad fffffa80024be5d0
VAD           Level     Start       End Commit
fffffa80028b6e20  4        10        1f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8001800a70  3        20        2f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8002112460  4        30        33      0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa80028bc280  2        40        40      0 Mapped       READONLY           Pagefile section, shared commit 0x1
fffffa800182ec50  3        50        50      1 Private      READWRITE          
fffffa800183eaf0  4        60        c6      0 Mapped       READONLY           \Windows\System32\locale.nls
fffffa8000c83920  1       120       21f      6 Private      READWRITE          
fffffa80025a7dd0  5       220       31f     14 Private      READWRITE          
fffffa800234b580  4       360       36f      6 Private      READWRITE          
fffffa800183fa60  3       3f0       4ef     35 Private      READWRITE          
fffffa800251e870  5       4f0     404ef      0 Mapped       READWRITE          Pagefile section, shared commit 0x40000
fffffa8000d14320  4     779e0     77afe      4 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\kernel32.dll
fffffa8000d5aa70  2     77c00     77da8     12 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\ntdll.dll
fffffa80028bfd70  4     7efe0     7f0df      0 Mapped       READONLY           Pagefile section, shared commit 0x5
fffffa80020c3bc0  3     7f0e0     7ffdf      0 Private      READONLY           
fffffa80024be5d0  0     7ffe0     7ffef     -1 Private      READONLY           
fffffa8002100cd0  4    13fb60    13fb73      2 Mapped  Exe  EXECUTE_WRITECOPY  \Users\31231\Desktop\MemTests.exe
fffffa80028cb840  3  7fef99b0  7fef99b2      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-conio-l1-1-0.dll
fffffa80028cee30  5  7fefa140  7fefa142      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-locale-l1-1-0.dll
fffffa80028cb3c0  4  7fefa150  7fefa154      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-math-l1-1-0.dll
fffffa80028ce1d0  5  7fefa160  7fefa163      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-convert-l1-1-0.dll
fffffa80028ccac0  2  7fefa170  7fefa173      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-stdio-l1-1-0.dll
fffffa80028ccb50  5  7fefa190  7fefa193      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-string-l1-1-0.dll
fffffa800175ee30  4  7fefa340  7fefa342      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-heap-l1-1-0.dll
fffffa80028ce3b0  5  7fefa350  7fefa352      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-file-l1-2-0.dll
fffffa80028cca00  3  7fefa360  7fefa362      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-processthreads-l1-1-1.dll
fffffa80028bcaa0  5  7fefa3b0  7fefa3b2      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-synch-l1-2-0.dll
fffffa8001802ba0  4  7fefa3c0  7fefa3c2      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-localization-l1-2-0.dll
fffffa80027ce670  5  7fefa3d0  7fefa3d2      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-file-l2-1-0.dll
fffffa80028ccdb0  1  7fefa410  7fefa412      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-core-timezone-l1-1-0.dll
fffffa80028c9620  5  7fefa420  7fefa511      4 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\ucrtbase.dll
fffffa80028c8c20  4  7fefa520  7fefa523      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\api-ms-win-crt-runtime-l1-1-0.dll
fffffa80028cba10  5  7fefa530  7fefa546      2 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\vcruntime140.dll
fffffa8000d14290  3  7fefde50  7fefdeba      3 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\KernelBase.dll
fffffa80028c9010  4  7fefdf20  7fefdf3e      4 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\sechost.dll
fffffa80028c9500  5  7fefe070  7fefe19c      3 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\rpcrt4.dll
fffffa80025d5c60  2  7fefe620  7fefe6fa      7 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\advapi32.dll
fffffa80018004b0  4  7feffe70  7fefff0e      7 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\msvcrt.dll
fffffa8001840f80  5  7fefff20  7fefff20      0 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\apisetschema.dll
fffffa8000d3ed30  3  7fffffa0  7fffffd2      0 Mapped       READONLY           Pagefile section, shared commit 0x33
fffffa80023b3630  4  7fffffd6  7fffffd6      1 Private      READWRITE          
fffffa800183ec20  5  7fffffde  7fffffdf      2 Private      READWRITE          

Total VADs: 42, average level: 4, maximum depth: 5
Total private commit: 0x71 pages (452 KB)
Total shared commit:  0x4005d pages (1048948 KB)

通过VAD,我们可以得到原型PTE的地址:

kd> dt nt!_mmvad fffffa800251e870
   +0x000 u1               : <unnamed-tag>
   +0x008 LeftChild        : (null) 
   +0x010 RightChild       : (null) 
   +0x018 StartingVpn      : 0x4f0
   +0x020 EndingVpn        : 0x404ef
   +0x028 u                : <unnamed-tag>
   +0x030 PushLock         : _EX_PUSH_LOCK
   +0x038 u5               : <unnamed-tag>
   +0x040 u2               : <unnamed-tag>
   +0x048 Subsection       : 0xfffffa80`02352980 _SUBSECTION
   +0x048 MappedSubsection : 0xfffffa80`02352980 _MSUBSECTION
   +0x050 FirstPrototypePte : 0xfffff8a0`01a00048 _MMPTE
   +0x058 LastContiguousPte : 0xfffff8a0`01c00040 _MMPTE
   +0x060 ViewLinks        : _LIST_ENTRY [ 0xfffffa80`02352970 - 0xfffffa80`02352970 ]
   +0x070 VadsProcess      : 0xfffffa80`01841b31 _EPROCESS

PTE的地址是无效的:

kd> dq 0xfffff8a0`01a00048
fffff8a0`01a00048  ????????`???????? ????????`????????
fffff8a0`01a00058  ????????`???????? ????????`????????
fffff8a0`01a00068  ????????`???????? ????????`????????
fffff8a0`01a00078  ????????`???????? ????????`????????
fffff8a0`01a00088  ????????`???????? ????????`????????
fffff8a0`01a00098  ????????`???????? ????????`????????
fffff8a0`01a000a8  ????????`???????? ????????`????????
fffff8a0`01a000b8  ????????`???????? ????????`????????

通过使用!pte查看原型PTE地址,我们看到存储它的页面是在分页文件中。

kd> !pte 0xfffff8a0`01a00048
                                           VA fffff8a001a00048
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1400    PDE at FFFFF6FB7E280068    PTE at FFFFF6FC5000D000
contains 000000003D104863  contains 0000000004A69863  contains 0000000025F5C863  contains 0000A88B00000080
pfn 3d104     ---DA--KWEV  pfn 4a69      ---DA--KWEV  pfn 25f5c     ---DA--KWEV  not valid
                                                                                  PageFile:  0
                                                                                  Offset: a88b
                                                                                  Protect: 4 - ReadWrite

通过对MiDispatchFault下断,我们得到了Windbg的输出:

bp /t fffffa80028b9700 nt!MiDispatchFault "j @rdx = 0x4F0000 '';'r rdx;gc'"
kd> r
rax=fffff88003905ae0 rbx=0000000000000000 rcx=0000000000000000
rdx=00000000004f0000 rsi=0000000000000000 rdi=ffffffffffffffff
rip=fffff80003efb900 rsp=fffff88003905978 rbp=fffff88003905a00
 r8=fffff8a001a00048  r9=0000000000000000 r10=ffffffff00000480
r11=fffff8a001a00048 r12=fffffa80028b9700 r13=00000000004f0000
r14=fffffa8001841ec8 r15=0000000000000004
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!MiDispatchFault:
fffff800`03efb900 fff3            push    rbx

事实上,通过Google后,函数原型可能是这样的:

NTSTATUS
MiDispatchFault(
  IN  ULONG_PTR FaultStatus,
  IN  PVOID VirtualAddress,
  IN  PMMPTE PointerPte,
  IN  PMMPTE PointerProtoPte,
  IN  LOGICAL RecheckAccess,
  IN  PEPROCESS Process,
  IN  PVOID TrapInformation,
  IN  PMMVAD Vad
  );

也就是说rdx应该是虚拟地址,r8是原型PTE的地址。

此时的调用栈如下:

kd> !thread @$thread
THREAD fffffa80028b9700  Cid 09c8.09cc  Teb: 000007fffffde000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000f53dd0
Owning Process            fffffa8001841b30       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      9567           Ticks: 0
Context Switch Count      1492           IdealProcessor: 0             
UserTime                  00:00:00.265
KernelTime                00:00:01.232
Win32 Start Address 0x000000013fb68eec
Stack Init fffff88003905c70 Current fffff88003905530
Base fffff88003906000 Limit fffff88003900000 Call 0000000000000000
Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`03905978 fffff800`03eeb979 : 00000000`00000000 ffffffff`ffffffff 00000000`00000000 00000000`00000000 : nt!MiDispatchFault
fffff880`03905980 fffff800`03edc76e : 00000000`00000000 00000000`004f0000 00000000`00000001 00000000`00000003 : nt!MmAccessFault+0x359
fffff880`03905ae0 00000001`3fb617bd : 00000001`3fb6a900 000007fe`fa503400 00000000`0021dd18 00000000`0040fad8 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`03905ae0)
00000000`0021fa10 00000001`3fb6a900 : 000007fe`fa503400 00000000`0021dd18 00000000`0040fad8 00000000`0021fa68 : 0x00000001`3fb617bd
00000000`0021fa18 000007fe`fa503400 : 00000000`0021dd18 00000000`0040fad8 00000000`0021fa68 00000001`0021fa70 : 0x00000001`3fb6a900
00000000`0021fa20 00000000`0021dd18 : 00000000`0040fad8 00000000`0021fa68 00000001`0021fa70 00000000`004f0000 : 0x000007fe`fa503400
00000000`0021fa28 00000000`0040fad8 : 00000000`0021fa68 00000001`0021fa70 00000000`004f0000 00000000`404f0000 : 0x21dd18
00000000`0021fa30 00000000`0021fa68 : 00000001`0021fa70 00000000`004f0000 00000000`404f0000 00000000`0000006d : 0x40fad8
00000000`0021fa38 00000001`0021fa70 : 00000000`004f0000 00000000`404f0000 00000000`0000006d 00000001`00000001 : 0x21fa68
00000000`0021fa40 00000000`004f0000 : 00000000`404f0000 00000000`0000006d 00000001`00000001 000d001c`000d0001 : 0x00000001`0021fa70
00000000`0021fa48 00000000`404f0000 : 00000000`0000006d 00000001`00000001 000d001c`000d0001 00000000`00000000 : 0x4f0000
00000000`0021fa50 00000000`0000006d : 00000001`00000001 000d001c`000d0001 00000000`00000000 000026c9`06aacda8 : 0x404f0000
00000000`0021fa58 00000001`00000001 : 000d001c`000d0001 00000000`00000000 000026c9`06aacda8 00000000`0021fab8 : 0x6d
00000000`0021fa60 000d001c`000d0001 : 00000000`00000000 000026c9`06aacda8 00000000`0021fab8 00000000`00000000 : 0x00000001`00000001
00000000`0021fa68 00000000`00000000 : 000026c9`06aacda8 00000000`0021fab8 00000000`00000000 00000000`00000000 : 0x000d001c`000d0001

此时我们重新定义断点就能得到书上的例子了:

kd> bp /t fffffa80028b9700 nt!MiDispatchFault "j @rdx = 0xfffff8a001a00048 '';'r rdx;gc'"
breakpoint 0 redefined
kd> r
rax=fffff88003905a80 rbx=0000000000000000 rcx=0000000000000000
rdx=fffff8a001a00048 rsi=0000000000000000 rdi=ffffffffffffffff
rip=fffff80003efb900 rsp=fffff88003905978 rbp=fffff88003905a00
 r8=0000000000000000  r9=0000000000000000 r10=0000000000000000
r11=fffffa8002352980 r12=fffffa80028b9700 r13=fffff8a001a00048
r14=fffff80004062bc0 r15=fffff88003905ae0
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!MiDispatchFault:
fffff800`03efb900 fff3            push    rbx

可以看到此时rdx等于原型PTE的地址。

此时的调用栈如下:

kd> !thread @$thread
THREAD fffffa80028b9700  Cid 09c8.09cc  Teb: 000007fffffde000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000f53dd0
Owning Process            fffffa8001841b30       Image:         MemTests.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      9572           Ticks: 4 (0:00:00:00.062)
Context Switch Count      1493           IdealProcessor: 0             
UserTime                  00:00:00.265
KernelTime                00:00:01.279
Win32 Start Address 0x000000013fb68eec
Stack Init fffff88003905c70 Current fffff88003904b00
Base fffff88003906000 Limit fffff88003900000 Call 0000000000000000
Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`03905978 fffff800`03eed467 : 00000000`00000000 00000000`004f0000 00000000`00000000 00000000`00000000 : nt!MiDispatchFault
fffff880`03905980 fffff800`03edc76e : 00000000`00000000 00000000`004f0000 00000000`00000001 00000000`00000003 : nt!MmAccessFault+0x1e47
fffff880`03905ae0 00000001`3fb617bd : 00000001`3fb6a900 000007fe`fa503400 00000000`0021dd18 00000000`0040fad8 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`03905ae0)
00000000`0021fa10 00000001`3fb6a900 : 000007fe`fa503400 00000000`0021dd18 00000000`0040fad8 00000000`0021fa68 : 0x00000001`3fb617bd
00000000`0021fa18 000007fe`fa503400 : 00000000`0021dd18 00000000`0040fad8 00000000`0021fa68 00000001`0021fa70 : 0x00000001`3fb6a900
00000000`0021fa20 00000000`0021dd18 : 00000000`0040fad8 00000000`0021fa68 00000001`0021fa70 00000000`004f0000 : 0x000007fe`fa503400
00000000`0021fa28 00000000`0040fad8 : 00000000`0021fa68 00000001`0021fa70 00000000`004f0000 00000000`404f0000 : 0x21dd18
00000000`0021fa30 00000000`0021fa68 : 00000001`0021fa70 00000000`004f0000 00000000`404f0000 00000000`0000006d : 0x40fad8
00000000`0021fa38 00000001`0021fa70 : 00000000`004f0000 00000000`404f0000 00000000`0000006d 00000001`00000001 : 0x21fa68
00000000`0021fa40 00000000`004f0000 : 00000000`404f0000 00000000`0000006d 00000001`00000001 000d001c`000d0001 : 0x00000001`0021fa70
00000000`0021fa48 00000000`404f0000 : 00000000`0000006d 00000001`00000001 000d001c`000d0001 00000000`00000000 : 0x4f0000
00000000`0021fa50 00000000`0000006d : 00000001`00000001 000d001c`000d0001 00000000`00000000 000026c9`06aacda8 : 0x404f0000
00000000`0021fa58 00000001`00000001 : 000d001c`000d0001 00000000`00000000 000026c9`06aacda8 00000000`0021fab8 : 0x6d
00000000`0021fa60 000d001c`000d0001 : 00000000`00000000 000026c9`06aacda8 00000000`0021fab8 00000000`00000000 : 0x00000001`00000001
00000000`0021fa68 00000000`00000000 : 000026c9`06aacda8 00000000`0021fab8 00000000`00000000 00000000`00000000 : 0x000d001c`000d0001

MiDispatchFault是解决大多数类型错误时会被调用的主力函数,它能够检索分页文件然后返回得到原型PTEs

1.12 释放共享内存

释放共享内存是一个两步的过程。首先,UnmapViewOfFile被调用,以便将虚拟范围与section对象分离。在其他方面,这将减少所有属于调用该函数进程的工作集的页面共享计数。

值得注意的是,当一个sectionn个进程映射时,所有页面的页面共享数不一定等于n。一个共享进程只有在实际访问该页时才会对共享计数加1:在这个阶段,VAD被查询,硬件PTE从原型中被填充。所以,一般来说,section活动的物理页可以有1到n之间的任何共享计数。当UnmapViewOfFile被调用时,属于调用进程工作集的页面共享计数被递减。对于某些页面,这可能会导致共享计数下降到0,从而导致它们被移到ModifiedStandby列表中。

当没有任何进程留下映射一个section的视图时,所有的section页面都在一个过渡列表中。

释放共享内存的第二步是关闭section对象的句柄。只要有进程对section对象的句柄是打开的,section数据结构(control area、subsections、segment)就会被保留,section页就会留在过渡列表中。Modified页面继续被MPW置换出去,Standby页面可以被重新利用,但这不一定发生,如果有其他可用的内存。

值得指出的是,这与私有内存被释放时有什么不同。对于私有内存,物理页会立即被添加到空闲列表中,因为它们的内容不再需要了:一旦我们释放了内存,我们就没有办法拿回它的内容。由于同样的原因,在分页文件块中干净的,老化的物理页和被换出的内容也将被释放。另一方面,对于共享内存来说,只要该section存在,其内容就不能被丢弃,所以它要么在过渡列表的页面中,要么被换出。

当最后一个句柄被关闭时,该section就被销毁。同样,当这种情况发生时,内存内容不能再被检索。在这个阶段,该section所消耗的物理页被移到空闲列表中,分页文件空间被释放。

这种行为可以通过MemTests程序和像SysinternalsProcess Explorer这样的工具来观察,它可以显示空闲、待机和修改页面数量。

首先,我们启动一个MemTests的实例,并分配一个足够大的部分对象,以便在内存计数器中显示出来,例如,在1GB的系统中,申请512MB。我们可以用Memory section test选项来做这件事。

然后我们使用访问区域选项来写入整个区域,这样该section就会消耗物理页。进程资源管理器将显示已用物理内存的增加。下图显示了一个1GB的虚拟机上512MB section的物理内存使用情况。

Untitled 6.png

然后我们启动MemTests的第二个实例,使用打开现有文件映射选项来打开相同的section并读取整个区域,这样第二个进程实际上是在引用物理页。

Untitled 7.png

之后,我们使用Release file mapping选项,在一个MemTests实例中取消映射视图并关闭section句柄。我们可以看到StandbyModified计数器变化不是很大(除了正常的波动),因为在第二个MemTests进程中,该section的物理页仍然被映射。下图是这个阶段的一个快照:

Untitled 8.png

当我们把视图从第二个实例中取消映射,并且在关闭句柄之前,我们看到Modified列表出现了急剧的增长。

Untitled 9.png

这些是不属于工作集的section页面,已经被移到了Modified列表中。它们是脏的,因此它们没有进入Standby列表,因为我们之前写到了内存区域。物理内存图显示了一个向下的斜坡的开始,这是由于MPW正在执行页面置出并将页面移到Standby列表中。

当我们关闭该section的第二个也是最后一个句柄时,所有的section内容都被丢弃了。仍然在Modified列表中的页面和被移到Standby列表中的页面都被添加到Free列表中,然后被归零。最后,Zeroed计数器大大增加,而ModifiedStandby计数器则下降。

Untitled 10.png

我们还可以看到,System Commit的值下降了,因为刚刚释放的section需要用内存或分页文件来支持。

1.13 共享内存状态图

图49显示了页面状态图,共享内存的转换用粗体字标出。

Untitled 11.png

1.14 共享内存和工作集列表项

1.14.1 VMM 怎么从VA获取他的WSLE

以前我们说过VMM如何在_MMPFN中存储一个工作集中活动页面的WSLE索引(在u1.Wslndex成员中)。这样做是为了能够从一个有效的VA到其WSLEWSLEWSL中是随机排列的,没有简单的方法可以通过查看列表来找到某个特定VA的条目。相反,VMM可以很容易地从VAPTE,从PTE_MMPFN页,并在那里找到WSLE索引。

1.14.2 共享内存存在的问题和一个不是问题的问题。

上一节中概述的方法对于最多映射在单个WS中的私有页很有效,但是共享内存带来了一个新的问题。同一物理页可以先被添加到一个工作集,然后再添加到另一个工作集(或其他几个工作集)。VMM不能确定在所有的WS中都有相同的WSLE,因为WSLE的分配取决于每个进程访问内存的顺序,并会被一些事件释放,包括VMM的修剪活动。因此,一个共享页可以与不同WS中的不同WSL索引相关联。

VMM按照其通常的路径寻找WSLE时,它可以检测到这种情况:它可以从_MMPFN中读取索引并访问由它选择的WSLEWSLE包含它所指的VA,所以VMM可以比较WSLEVA和它开始的VA。如果这两个不一样,它就知道WSLE不是正确的那个。当这种情况发生时,VMM需要一种方法来获得正确的条目。

在看VMM如何解决这个问题之前,值得花几句话来谈谈不属于这个问题的东西。到目前为止,我们集中讨论了WSLE索引,它在不同的WS中可能是不同的。这同样适用于页面被映射的VA:一个共享页面可以被映射在不同地址空间的不同VA上。然而,当涉及到从VAWSLE时,这并不是一个问题。

考虑两个地址空间AB,其中同一个共享页被映射在不同的VAaVAb,但假设VMM已经能够在两个WS中使用索引为i的同一个WSLE。当地址空间A是当前的时候,VMM可以从PTE得到_MMPFN并得到索引i,这是有效的。当地址空间B是当前的,VAbPTE指向同一个_MMPFN,允许再次提取索引i。总之,不同的VA不是问题,因为最终它们会映射到同一个页面;而不同的WSLE则是一个需要解决的问题。

不同的VA和相同的索引的情况,实际上可以通过对内存section的实验来观察。

Untitled 12.png

以下是两个进程的信息:

kd> !process 0 1 MemTests.exe
PROCESS fffffa80017152b0
    SessionId: 1  Cid: 030c    Peb: 7fffffdf000  ParentCid: 05d0
    DirBase: 142cc000  ObjectTable: fffff8a00180a930  HandleCount:  13.
    Image: MemTests.exe
    VadRoot fffffa80026bd510 Vads 42 Clone 0 Private 124. Modified 131072. Locked 0.
    DeviceMap fffff8a000f53dd0
    Token                             fffff8a00184d060
    ElapsedTime                       00:45:46.416
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         22688
    QuotaPoolUsage[NonPagedPool]      4920
    Working Set Sizes (now,min,max)  (561, 50, 345) (2244KB, 200KB, 1380KB)
    PeakWorkingSetSize                131891
    VirtualSize                       11 Mb
    PeakVirtualSize                   523 Mb
    PageFaultCount                    131889
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      145

PROCESS fffffa8001776880
    SessionId: 1  Cid: 02ac    Peb: 7fffffdd000  ParentCid: 05d0
    DirBase: 30fa2000  ObjectTable: fffff8a0015348d0  HandleCount:  13.
    Image: MemTests.exe
    VadRoot fffffa80018014f0 Vads 42 Clone 0 Private 125. Modified 0. Locked 0.
    DeviceMap fffff8a000f53dd0
    Token                             fffff8a001895060
    ElapsedTime                       00:43:06.312
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         22688
    QuotaPoolUsage[NonPagedPool]      4920
    Working Set Sizes (now,min,max)  (554, 50, 345) (2216KB, 200KB, 1380KB)
    PeakWorkingSetSize                131883
    VirtualSize                       11 Mb
    PeakVirtualSize                   523 Mb
    PageFaultCount                    131881
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      145
kd> !vad fffffa80026bd510
VAD           Level     Start       End Commit
fffffa8001808990  4        10        1f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8001776d60  3        20        2f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa80012f2940  4        30       12f      6 Private      READWRITE          
fffffa8001285e00  2       130       133      0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa8002461200  3       140       140      0 Mapped       READONLY           Pagefile section, shared commit 0x1
fffffa8002225bf0  1       150       150      1 Private      READWRITE          
fffffa80028e3540  5       160       1c6      0 Mapped       READONLY           \Windows\System32\locale.nls
fffffa800217c510  4       1d0       2cf     14 Private      READWRITE          
fffffa80023d6790  5       2d0       2d0      0 Mapped       READWRITE          Pagefile section, shared commit 0x1
...
kd> !vad fffffa80018014f0
VAD           Level     Start       End Commit
fffffa800258d010  4        10        1f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8002513b60  3        20        2f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa800126bb10  4        30        33      0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa800228d100  2        40        40      0 Mapped       READONLY           Pagefile section, shared commit 0x1
fffffa80028a90d0  4        50        50      1 Private      READWRITE          
fffffa8001802570  3        60        c6      0 Mapped       READONLY           \Windows\System32\locale.nls
fffffa80025c3ec0  4        d0        d0      0 Mapped       READWRITE          Pagefile section, shared commit 0x1
...

然后查看D0000和2D0000的pte信息。

kd> .process /i fffffa80017152b0
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ed6490 cc              int     3
kd> !pte 2d0000
                                           VA 00000000002d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001680
contains 00C0000011CB4867  contains 0140000011F37867  contains 0360000010A4A867  contains B3200000371AE825
pfn 11cb4     ---DA--UWEV  pfn 11f37     ---DA--UWEV  pfn 10a4a     ---DA--UWEV  pfn 371ae     ----A--UR-V

kd> .process /i fffffa8001776880
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ed6490 cc              int     3
kd> !pte D0000
                                           VA 00000000000d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000680
contains 00C0000026699867  contains 0140000012F9C867  contains 03C000002F01D867  contains B3200000371AE825
pfn 26699     ---DA--UWEV  pfn 12f9c     ---DA--UWEV  pfn 2f01d     ---DA--UWEV  pfn 371ae     ----A--UR-V

可以看到两个VA虽然不同,但是PFN的索引都是371ae。

1.14.3 解决方案:Hash表。

VMM通过使用一个哈希表来解决这个问题。实际上,有两个哈希表,我们很快就会对它们进行更详细的分析,但是,现在只需要考虑一个单一的、逻辑的、将VAWSL索引联系起来的哈希表就足够了。每当VMM发现_MMPFN.u1.WsIndex选择的WSLE是为另一个VA准备的,它就会使用哈希表来从VA获得正确的索引。每个WS都有自己的哈希表(在Hyperspace区域,它是进程私有的),所以每个表都有它所属的WS的正确索引。我们现在要描述VMM何时决定创建哈希表项。

从现在开始描述的逻辑是在以下VMM函数中实现的。MiUpdateWsle, MiUpdateWsleHash, MiSwapWslEntries.

首先要注意的是,一个私有页面的普通WSLEDirect位设置,就像下面这个。

kd> !process 0 1 MemTests.exe
PROCESS fffffa8002147b30
    SessionId: 1  Cid: 0760    Peb: 7fffffde000  ParentCid: 05d0
    DirBase: 3ae11000  ObjectTable: fffff8a000bb85e0  HandleCount:  11.
    Image: MemTests.exe
    VadRoot fffffa80026bd510 Vads 42 Clone 0 Private 124. Modified 0. Locked 0.
    DeviceMap fffff8a000f53dd0
    Token                             fffff8a001e89a90
    ElapsedTime                       00:02:13.364
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         22680
    QuotaPoolUsage[NonPagedPool]      4920
    Working Set Sizes (now,min,max)  (563, 50, 345) (2252KB, 200KB, 1380KB)
    PeakWorkingSetSize                563
    VirtualSize                       11 Mb
    PeakVirtualSize                   11 Mb
    PageFaultCount                    560
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      145

kd> .process /i fffffa8002147b30
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ed6490 cc              int     3
kd> !pte 60000
                                           VA 0000000000060000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000300
contains 00C000001235C867  contains 014000000335F867  contains 01300000033A0867  contains A32000003369B867
pfn 1235c     ---DA--UWEV  pfn 335f      ---DA--UWEV  pfn 33a0      ---DA--UWEV  pfn 3369b     ---DA--UW-V

kd> !pfn 3369b
    PFN 0003369B at address FFFFFA80009A3D10
    flink       00000232  blink / share count 00000001  pteaddress FFFFF68000000300
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 0033A0  Active     M       
    Modified                
kd> ?? ((nt!_mmwsl *)0xfffff70001080000)->Wsle[0x232].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y0
   +0x000 Direct           : 0y1
   +0x000 Protection       : 0y00000 (0)
   +0x000 Age              : 0y000
   +0x000 VirtualPageNumber : 0y0000000000000000000000000000000000000000000001100000 (0x60)

当一个共享页被添加到一个工作集,并且还没有成为另一个WS的一部分时,该WSLE就会像之前的一样被设置Dirtect位。VMM_MMPFN.u1.Wslndex检测到这种情况,对于这样的一个页面,它是0。在该页被添加到WS中后,_MMPFN.u1.Wslndex将被设置为WSLE索引。

kd> .process /i fffffa8002147b30
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ed6490 cc              int     3
kd> !pte 70000
                                           VA 0000000000070000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000380
contains 00C000001235C867  contains 014000000335F867  contains 01300000033A0867  contains A390000032A9C825
pfn 1235c     ---DA--UWEV  pfn 335f      ---DA--UWEV  pfn 33a0      ---DA--UWEV  pfn 32a9c     ----A--UR-V

kd> !pfn 32a9c
    PFN 00032A9C at address FFFFFA800097FD40
    flink       00000239  blink / share count 00000001  pteaddress FFFFF8A001DF1A88
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 024FF5  Active     MP      
    Modified Shared              
kd> ?? ((nt!_mmwsl *)0xfffff70001080000)->Wsle[0x239].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y0
   +0x000 Direct           : 0y1
   +0x000 Protection       : 0y00100 (0x4)
   +0x000 Age              : 0y000
   +0x000 VirtualPageNumber : 0y0000000000000000000000000000000000000000000001110000 (0x70)

当一个共享页被添加到一个WS中,并且它在_MMPFN.u1.Wslndex中已经有一个值,VMM试图在新的WS中使用相同的索引。

如果WSLEWS中是空闲的,它就被使用,然而Direct位不会被设置。因此,看起来Direct只在首次给页面分配WS索引的WS中被设置。

kd> !pte 60000
                                           VA 0000000000060000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000300
contains 00C0000026877867  contains 014000003D53A867  contains 0130000026E3B867  contains A390000032A9C825
pfn 26877     ---DA--UWEV  pfn 3d53a     ---DA--UWEV  pfn 26e3b     ---DA--UWEV  pfn 32a9c     ----A--UR-V

kd> !pfn 32a9c
    PFN 00032A9C at address FFFFFA800097FD40
    flink       00000239  blink / share count 00000002  pteaddress FFFFF8A001DF1A88
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 024FF5  Active     MP      
    Modified Shared              
kd> ?? ((nt!_mmwsl *)0xfffff70001080000)->Wsle[0x239].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y0
   +0x000 Direct           : 0y0
   +0x000 Protection       : 0y00100 (0x4)
   +0x000 Age              : 0y000
   +0x000 VirtualPageNumber : 0y0000000000000000000000000000000000000000000001100000 (0x60)

如果WSLE在使用中,VMM会将其内容转移到另一个空闲项中,并重新使用被添加到WS的页面项。因此,即使在这种情况下,该页的WSLE是在_MMPFN中找到索引的那个。然而,当这种情况发生时,VMM也为VA创建一个哈希表项。这个项将把页面映射的VAWSLE索引联系起来,尽管这个索引与_MMPFN中的索引相同。此外,该项将清除Direct位并设置哈希位,就像下面这个:

kd> !pte d0000
                                           VA 00000000000d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000680
contains 00C000003B42A867  contains 01400000093ED867  contains 013000003B5AE867  contains A390000032A9C825
pfn 3b42a     ---DA--UWEV  pfn 93ed      ---DA--UWEV  pfn 3b5ae     ---DA--UWEV  pfn 32a9c     ----A--UR-V

kd> ?? ((nt!_mmwsl *)0xfffff70001080000)->Wsle[0x239].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y1
   +0x000 Direct           : 0y0
   +0x000 Protection       : 0y00100 (0x4)
   +0x000 Age              : 0y000
   +0x000 VirtualPageNumber : 0y0000000000000000000000000000000000000000000011010000 (0xd0)

一般来说,哈希位似乎表明存在一个哈希表项。在这种情况下,创建哈希表项就是白费功夫。如果将来WSLE最终被重新定位,该项最终会被使用,但最终的索引不再是_MMPFN中的那个。

VMM像我们刚才看到的那样移除一个WSLE时,有两种可能的情况:被移除的WSLE可以与一个私有或共享物理页相关联。

当前者适用时,_MMPFN.u1.WsIndex被更新为分配给该页的新WSLE的索引。该WSLE仍然有Direct位置1,并且它没有哈希表项。这就像它从一开始就被加载到最终的WSLE中一样。

如果被移除的WSLE是一个共享页面,_MMPFN.u1.WsIndex不能被改变。可能有其他WSs在该索引中包括相同的页面,它们需要_MMPFN保持不变才能被访问。因此,被移除的 WSLE 变成了真正需要哈希表项的 WSLE:它即将结束在一个索引不同于_MMPFN.u1.WsIndexVMMVA创建一个哈希表项(如果不存在的话),并将WSLE的新索引记录在其中。

在这种逻辑下,最后一个 "指向 "WSLEVA获胜,而之前由该项追踪的VA被重新定位。可能,这背后的原因是,在需要通过哈希表访问之前,老的WSLE有更大的机会被工作集管理器从WSL中驱逐。WSM扫描的是WS而不是VA,所以它不需要通过哈希表。

1.14.4 Hash表细节

本节的概念可以通过分析MiUpdateWsleHash函数来观察。

实际上有两个哈希表,由通常位于地址0xFFFFF700'01080000的_MMWSL结构指向。

#### 非直接Hash表

第一个是由_MMWSL.NonDirectHash,大小为1页。它通常在地址0xFFFFF704'40000000处,即在超空间区域的+17GB处。它的项类型是_MMWSLE_NONDIRECT_HASH

kd> dt -v nt!_MMWSLE_NONDIRECT_HASH
struct _MMWSLE_NONDIRECT_HASH, 2 elements, 0x10 bytes
   +0x000 Key              : Ptr64 to Void
   +0x008 Index            : Uint4B

并且长度为0x10字节,因此该表可以存储多达0x100个项。VMM查看该表以找到给定VAWSL索引。它使用虚拟页号的第0-7位(即VA的第12-19位)作为该表的索引。当然,碰撞是可能的,即两个VA的第12-19位具有相同的值,选择同一个条目。条目中的Key成员存储了该条目实际所指的完整VA,所以VMM会检查它,看它是否选择了正确的哈希表条目。如果不是这样,它将依次扫描表,寻找Key成员中的VA。(Key实际上被设置为VA与1的OR运算,所以它有位0是1;在搜索正确的条目时要考虑到这一点;记住,WSLE与页面对齐的地址有关)。

由于这个哈希表被限制为0x100个条目,它可能会变得很满。当这种情况发生时,MiConvertWsleHash被调用(例如,由MiUpdateWsleHash调用),可能是为了将哈希表直接转换为直接哈希表,在下一节中讨论。

#### 直接Hash表

直接哈希表的地址存储在_MMWSL.HashTableStart,它通常被设置为0xFFFFF704'40001000,也就是说,就在非直接哈希表地址之后。该表本身是一个_MMWSLE_HASH的数组,索引如下:

  • 为了找到某个VA的对应项,_MMWSL.LowestPagableAddress是从地址自身得出来的。
  • 结果被右移了12位。
  • 最后的结果是数组的索引。一个_MMWSLE_HASH的大小是4个字节,所以项的偏移量是索引乘以4。

_MMWSL.LowestPagabieAddress通常是0,但是这个名字表明,如果一个WS在它的低范围内有一个不可分页的区域,这个成员指向可分页范围的开始。这可以节省数组空间,因为将数组的0号元素与内存的第一个可移除页相关联。不可分页的页面不需要在WSL中被考虑。

结果被右移了12次,因为每个WSLE都与一个虚拟页号相关联,所以项0代表第0页,项1代表第0x1000页,等等(假设最低可分页页地址为0)。

从上面的逻辑我们可以看出,这个数组实际上不是一个哈希表,而是一个直接访问数组。由于VPN和数组索引之间存在一对一的关系,所以不可能发生碰撞。出于这个原因,数组元素类型_MMWSLE_HASH不像非直接哈希情况那样包括一个键成员,它只是存储WSLE索引。

在8TB的用户模式范围内,直接哈希对每一个可能的VPN都有一个条目,所以它的大小是由以下公式给出的:

(800'00000000 >> 0xc) * 4 = 0x2'00000000 = 8GB

仅仅这个事实就告诉我们,VMM不可能提前映射整个表的物理内存--而每个进程都有一个哈希表! 发生的情况是,VMM在需要创建一个哈希表条目时,会在哈希表范围内映射一个页面,所以哈希表的虚拟区域有 "空位"。

作为一个例子,下面是映射explorer.exe实例的第一个哈希表页的PTE地址:

kd> !pte @@(((nt!_mmwsl*) 0xfffff70001080000)->HashTableStart)
                                           VA fffff70440001000
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE088    PDE at FFFFF6FB7DC11000    PTE at FFFFF6FB82200008
contains 000000000879C863  contains 000000000F365863  contains 000000000F3A4863  contains 80000000169F4863
pfn 879c      ---DA--KWEV  pfn f365      ---DA--KWEV  pfn f3a4      ---DA--KWEV  pfn 169f4     ---DA--KW-V

如果我们转储这个PTE和紧随其后的一个PTE,我们可以看到PTE设置为0,对应无效的虚拟页,与有效的虚拟页交替出现。

kd> dq FFFFF6FB82200008
fffff6fb`82200008  80000000`169f4863 80000000`14184863
fffff6fb`82200018  80000000`1d9f6863 00000000`00000000
fffff6fb`82200028  00000000`00000000 00000000`00000000
fffff6fb`82200038  80000000`0e578863 80000000`2a912863
fffff6fb`82200048  80000000`2a753863 80000000`397cd863
fffff6fb`82200058  80000000`35216863 80000000`30aa5863
fffff6fb`82200068  80000000`3c4eb863 80000000`20fc4863
fffff6fb`82200078  80000000`1ef39863 00000000`00000000

我们看到一个由3个归零的PTEs组成的块,从0xFFFFF6FB'82200020开始,然后后面是非零的项。我们可以通过检查内存内容来确认zeroed后的第一个非零项直接映射了哈希项。首先,我们从!pte扩展中得到映射的地址。

kd> !pte fffff6fb`82200038
                                           VA fffff70440007000
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE088    PDE at FFFFF6FB7DC11000    PTE at FFFFF6FB82200038
contains 000000000879C863  contains 000000000F365863  contains 000000000F3A4863  contains 800000000E578863
pfn 879c      ---DA--KWEV  pfn f365      ---DA--KWEV  pfn f3a4      ---DA--KWEV  pfn e578      ---DA--KW-V

接下来,我们看看fffff70440007000开始的内存内容,通过查找发现了下面的非0哈希表项:

kd> dd fffff70440007fb0
fffff704`40007fb0  00000879 000004a4 00000000 00000a52
fffff704`40007fc0  00000000 00000000 00000000 00000000
fffff704`40007fd0  00000000 00000000 00000000 00000000
fffff704`40007fe0  00000000 00000000 00000000 00000000
fffff704`40007ff0  00000000 00000000 00000000 00000000
fffff704`40008000  00000000 00000000 00000000 00000000
fffff704`40008010  00000000 00000000 00000000 00000000
fffff704`40008020  00000000 00000000 00000000 00000000

现在我们来计算一下0xfffff70440007fb0处的项所指向的VA

kd> ? ((fffff704`40007fb0-fffff70440001000)/4) << c
Evaluate expression: 29278208 = 00000000`01bec000

上面的表达式计算了项索引并将其左移12位,与计算索引的公式相反(_MMWSL.LowestPagableAddress为0)。这告诉我们,该项对应的VA是0x1bec000。让我们看看这是否与WSL一致。

kd> ?? ((nt!_mmwsl*) 0xfffff70001080000)->Wsle[0x879].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y1
   +0x000 Direct           : 0y0
   +0x000 Protection       : 0y00001 (0x1)
   +0x000 Age              : 0y110
   +0x000 VirtualPageNumber : 0y0000000000000000000000000000000000000001101111101100 (0x1bec)

我们用0x0x879,即在哈希表条目中发现的值作为进入WSL的索引,我们发现VPN 为0x1Bec的WSLE。这证实了3个无效PTE的 "空位 "之后的内存内容与直接哈希表一致。

用来存储哈希表的物理页被视为进程metapages

#### 使用哪个哈希表

当非直接哈希表满时,MiUpdateWsleHash调用MiConvertWsleHash。这表明一个进程开始使用非直接哈希表,如果需要的话,以后再切换到直接哈希表。作为一个例子,MemTests通常使用非直接哈希表,而explorer.execmd.exenotepad.exe使用直接哈希表。

对于一个使用非直接哈希表的进程,通常会观察到以下情况:

  • _MMWSL.NonDirectHash 非0,且通常设置为0xFFFFF704`40000000。
  • 上述成员所指向的页面是有效的。
  • _MMWSL.HashTableStart通常设置为直接哈希表的地址0xFFFFF704`40001000。
  • 直接哈希表开始的页是无效的

当一个进程使用直接哈希表是:

  • _MMWSL.NonDirectHash是0
  • 0xFFFFF704`40000000页面是无效的
  • _MMWSL.HashTableStart设置为直接哈希表的地址,通常是0xFFFFF704`40001000
  • 在直接哈希表范围内有有效的页面。

奇怪的是,!wsle扩展总是报告哈希表的大小和地址为0,不管使用的是哪一个。下面是使用非直接表的MemTests的输出。

kd> !wsle

Working Set @ fffff70001080000
    FirstFree      21a  FirstDynamic        5
    LastEntry      2ee  NextSlot            5  LastInitialized      36e
    NonDirect       c0  HashTable           0  HashTableSize          0

下面是explorer.exe的输出:

kd> !wsle

Working Set @ fffff70001080000
    FirstFree     1dd9  FirstDynamic        5
    LastEntry     1ddb  NextSlot            5  LastInitialized     1f6e
    NonDirect        0  HashTable           0  HashTableSize          0

一个有趣的区别是 "NonDirect "值,它对应于_MMWSL.NonDirectCount,对于MemTests.exe来说是非零值。

2 内存映射文件

2.1 基本概念

在内存中映射一个文件意味着使其内容从一个给定的起始虚拟地址可见。一旦一个文件被映射,它就可以通过访问虚拟内存来进行读写。VMM负责使文件内容在虚拟范围内可见,并通过分页更新内存内容来更新文件。映射文件的处理方式与分页文件类似:当它的一部分被首次访问时,会发生一个页面错误,VMM会将一个充满文件内容的物理页面映射到地址空间。当进程工作集被修剪或文件映射被关闭时,被修改的页面会进入已修改列表,并在以后被写入映射文件。

VMM使用section对象来映射文件,但初始化它们的方式是将虚拟地址与映射文件的偏移量联系起来。这种类型的section对象被称为由映射的文件支持,而没有映射文件的section对象被称为由分页文件支持。这个术语反映了当分页发生时,内存中的数据是从哪里读出来和写进去的。

映射的文件有两种:数据和可执行文件,也叫镜像文件,因为它们代表了可执行代码在内存中加载的快照。它们的映射方式不同:数据文件的映射方式与文件内容从磁盘上可见的方式完全相同,即如果映射从内存地址x开始,那么从文件开始的偏移量o的字节最终会出现在内存的地址x+o。镜像文件的映射方式不同,因此,文件偏移量和映射偏移量之间的关系要复杂一些。

section对象的主要API是CreateFileMappingMapViewOfFileEx,它们在前一章已经描述过了。

许多与分页文件支持的section对象有关的概念对映射文件支持的对象也是有效的,所以我们将只描述这两种对象的区别。

2.2 映射数据文件

2.2.1 映射数据文件的Section对象

#### Section对象数据结构

要映射一个文件,程序必须调用CreateFileMapping,将hFile参数设置为文件的句柄。

flProtect参数指定的保护必须与打开文件时请求的访问一致,例如,如果文件是以GENERIC_READ访问打开的,后来flProtect被设置为PAGE_READWRITECreateFileMapping就会失败,GetLastError返回ERROR_ACCESS_DENIED

dwMaximumSizeHighdwMaximumSizeLow指定了映射的大小:将它们设置为小于文件长度,只能映射文件的一部分,而指定一个大于文件长度的大小则会扩展文件。最后,将它们都设置为0会自动创建一个大小等于文件长度的映射。

CreateFileMapping的调用创建了类似于用于共享内存的数据结构集合。CreateFileMapping所返回的句柄指的是_SECTlON_OBJECT的一个实例,如下面的例子:

kd> !handle 0x38

PROCESS fffffa80024d7b30
    SessionId: 1  Cid: 0440    Peb: 7fffffd7000  ParentCid: 05d0
    DirBase: 1da15000  ObjectTable: fffff8a0019a7e10  HandleCount:  14.
    Image: MemTests.exe

Handle table at fffff8a0019a7e10 with 14 entries in use

0038: Object: fffff8a001896960  GrantedAccess: 000f0007 Entry: fffff8a001b200e0
Object: fffff8a001896960  Type: (fffffa8000d0fb80) Section
    ObjectHeader: fffff8a001896930 (new version)
        HandleCount: 1  PointerCount: 2
        Directory Object: fffff8a000ae6080  Name: map
kd> dt nt!_SECTION_OBJECT fffff8a001896960
   +0x000 StartingVa       : 0xfffffa80`024d7b30 Void
   +0x008 EndingVa         : 0xfffff8a0`00ae6080 Void
   +0x010 Parent           : 0xfffff880`030a2901 Void
   +0x018 LeftChild        : (null) 
   +0x020 RightChild       : 0xfffff8a0`00ae6010 Void
   +0x028 Segment          : 0xfffff8a0`00f2bd40 _SEGMENT_OBJECT

Segment似乎是_MAPPED_FILE_SEGMENT的实例的地址,而对于分页文件支持的section,它指向_SEGMENT的一个实例。前者实际上是后者的一个子集。

kd> dt nt!_MAPPED_FILE_SEGMENT
   +0x000 ControlArea      : Ptr64 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : Uint4B
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : Uint8B
   +0x018 SizeOfSegment    : Uint8B
   +0x020 ExtendInfo       : Ptr64 _MMEXTEND_INFO
   +0x020 BasedAddress     : Ptr64 Void
   +0x028 SegmentLock      : _EX_PUSH_LOCK
kd> dt nt!_SEGMENT
   +0x000 ControlArea      : Ptr64 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : Uint4B
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : Uint8B
   +0x018 SizeOfSegment    : Uint8B
   +0x020 ExtendInfo       : Ptr64 _MMEXTEND_INFO
   +0x020 BasedAddress     : Ptr64 Void
   +0x028 SegmentLock      : _EX_PUSH_LOCK
   +0x030 u1               : <unnamed-tag>
   +0x038 u2               : <unnamed-tag>
   +0x040 PrototypePte     : Ptr64 _MMPTE
   +0x048 ThePtes          : [1] _MMPTE

一个重要的区别是,原型PTEs并不像分页文件支持的section那样是segment的一部分。它们仍然同在一个分页池分配的区域内,但却是一个单独的区域。另外,_SEGMENT.PrototypePte对于共享内存来说,存储了第一个原型PTE的地址,但它不包括在_MAPPED_FILE_SEGMENT中(如果我们试图将内存内容作为一个_SEGMENT来转储,我们会得到PrototypePte的无意义值),所以我们需要一些其他的方法来找到PTE,我们将在后面看到这就是subsection的用处_MAPPED_FILE_SEGMENT.ControlArea存储的是控制区实例的地址。

kd> dt nt!_MAPPED_FILE_SEGMENT 0xfffff8a0`00f2bd40
   +0x000 ControlArea      : 0xfffffa80`027e2380 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : 1
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : 0
   +0x018 SizeOfSegment    : 0xa
   +0x020 ExtendInfo       : (null) 
   +0x020 BasedAddress     : (null) 
   +0x028 SegmentLock      : _EX_PUSH_LOCK
kd> dt nt!_CONTROL_AREA 0xfffffa80`027e2380
   +0x000 Segment          : 0xfffff8a0`00f2bd40 _SEGMENT
   +0x008 DereferenceList  : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x018 NumberOfSectionReferences : 1
   +0x020 NumberOfPfnReferences : 0
   +0x028 NumberOfMappedViews : 0
   +0x030 NumberOfUserReferences : 1
   +0x038 u                : <unnamed-tag>
   +0x03c FlushInProgressCount : 0
   +0x040 FilePointer      : _EX_FAST_REF
   +0x048 ControlAreaLock  : 0n0
   +0x04c ModifiedWriteCount : 0
   +0x04c StartingFrame    : 0
   +0x050 WaitingForDeletion : (null) 
   +0x058 u2               : <unnamed-tag>
   +0x068 LockedPages      : 0n1
   +0x070 ViewList         : _LIST_ENTRY [ 0xfffffa80`027e23f0 - 0xfffffa80`027e23f0 ]

控制区域的地址也可以使用!ca扩展命令查看

kd> !ca 0xfffffa80`027e2380

ControlArea  @ fffffa80027e2380
  Segment      fffff8a000f2bd40  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                   0  Mapped Views                0
  User Ref                    1  WaitForDel                0  Flush Count                 0
  File Object  fffffa800281d290  ModWriteCount             0  System Views                0
  WritableRefs                1  PartitionId                0  
  Flags (80) File 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a000f2bd40
  ControlArea       fffffa80027e2380  ExtendInfo    0000000000000000
  Total Ptes                       1
  Segment Size                     a  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa80027e2400
  ControlArea  fffffa80027e2380  Starting Sector        0  Number Of Sectors    0
  Base Pte     0000000000000000  Ptes In Subsect        1  Unused Ptes          0
  Flags                  a0000c  Sector Offset          a  Protection           6
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          0

!ca显示_CONTROL_AREA指向一个名为memtests.tmp的文件,这个信息被保存在_CONTROL_AREA.FilePointer中,它存储了一个_FILE_OBJECT的地址。

kd> dt nt!_CONTROL_AREA FilePointer. 0xfffffa80`027e2380
   +0x040 FilePointer  : 
      +0x000 Object       : 0xfffffa80`0281d291 Void
      +0x000 RefCnt       : 0y0001
      +0x000 Value        : 0xfffffa80`0281d291

Object被设置为一个_FILE_OBJECT的地址,RefCnt成员表明第0-3位被用作引用计数,所以我们必须屏蔽它们以获得实际地址。

kd> dt nt!_FILE_OBJECT 0xfffffa80`0281d290
   +0x000 Type             : 0n5
   +0x002 Size             : 0n216
   +0x008 DeviceObject     : 0xfffffa80`011e6ac0 _DEVICE_OBJECT
   +0x010 Vpb              : 0xfffffa80`00f98ce0 _VPB
   +0x018 FsContext        : 0xfffff8a0`01e16890 Void
   +0x020 FsContext2       : 0xfffff8a0`00eb2b00 Void
   +0x028 SectionObjectPointer : 0xfffffa80`01560768 _SECTION_OBJECT_POINTERS
   +0x030 PrivateCacheMap  : (null) 
   +0x038 FinalStatus      : 0n0
   +0x040 RelatedFileObject : 0xfffffa80`024f55d0 _FILE_OBJECT
   +0x048 LockOperation    : 0 ''
   +0x049 DeletePending    : 0 ''
   +0x04a ReadAccess       : 0x1 ''
   +0x04b WriteAccess      : 0x1 ''
   +0x04c DeleteAccess     : 0 ''
   +0x04d SharedRead       : 0 ''
   +0x04e SharedWrite      : 0 ''
   +0x04f SharedDelete     : 0 ''
   +0x050 Flags            : 0x40042
   +0x058 FileName         : _UNICODE_STRING "\Users\31231\Desktop\memtests.tmp"
   +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x070 Waiters          : 0
   +0x074 Busy             : 0
   +0x078 LastLock         : (null) 
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT
   +0x0b0 CompletionContext : (null) 
   +0x0b8 IrpListLock      : 0
   +0x0c0 IrpList          : _LIST_ENTRY [ 0xfffffa80`0281d350 - 0xfffffa80`0281d350 ]
   +0x0d0 FileObjectExtension : (null)

反过来,_FILE_OBJECT用其SectionObjectPointer成员指向_CONTROL_AREA。

kd> ?? ((nt!_FILE_OBJECT*) 0xfffffa80`0281d290)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`01560768
   +0x000 DataSectionObject : 0xfffffa80`027e2380 Void
   +0x008 SharedCacheMap   : (null) 
   +0x010 ImageSectionObject : (null)

SectionObjectPointer存储一个_SECTION_OBJECT_POINTERS实例的地址,其DataSectionObject成员指向_CONTROL_AREA

另外,!ca显示了_SUBSECTION,它存储在_CONTROL_AREA之后:后者的地址是0xfffffa80027e2380,前者是0xfffffa80027e2400,即+0x80字节,这是_C0NTR0L_AREA的大小。Subsection是获得访问原型PTE的关键,其地址被保存在_SUBSECTION.SubsectionBase中,并在上面的!ca输出中被列为 "Base Pte";Windbg显示目前还未分配。请注意,原型PTEs不是Segment的一部分,Segment位于0xfffff8a000f2bd40。

通过多次重复文件映射测试可以观察到,原型PTEs有时会在创建section对象时被分配,而在其他时候,它们只在通过MapViewOfFiteEx映射section的视图时被分配。就像本例中发生的那样,_SUBSECTI0N. SubsectionBase被设置为0,他直到实际分配发生时才有值。

#### Section对象拥有多个Subsection

一个section对象实际上可以包括一个以上的小节。下面是这样一个section的例子。

kd> !ca 0xfffffa80`027e2380

ControlArea  @ fffffa80027e2380
  Segment      fffff8a000f2bd40  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 2  Pfn Ref                   1  Mapped Views                2
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  fffffa800281d290  ModWriteCount             0  System Views                1
  WritableRefs                2  PartitionId                0  
  Flags (c080) File WasPurged Accessed 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a000f2bd40
  ControlArea       fffffa80027e2380  ExtendInfo    0000000000000000
  Total Ptes                     100
  Segment Size                100000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa80027e2400
  ControlArea  fffffa80027e2380  Starting Sector        0  Number Of Sectors    1
  Base Pte     fffff8a000cd5aa0  Ptes In Subsect        1  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          2

Subsection 2 @ fffffa8002572d10
  ControlArea  fffffa80027e2380  Starting Sector        1  Number Of Sectors   ff
  Base Pte     fffff8a001ae7000  Ptes In Subsect       ff  Unused Ptes        101
  Flags                   1000d  Sector Offset          0  Protection           6
  Accessed Static 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

每个subsection映射一个文件块,其长度等于subsection中的PTE数量(_SUBSECTION.PtesInSubsection!ca列为Ptes In Subsect)乘以页面大小。在上面的例子中,第1小节有0x1个PTEs,所以映射到块0-0xFFF。由于这是一个数据文件,它在内存中的映射与磁盘上的布局相同,所以第2小节从下一个字节开始,即在文件的偏移量0x1000。一般来说,一个subsection从文件开始的偏移量是由页面大小乘以在前面section中发现的PTE的总计数得到的。这个计数被存储在_SUBSECTION.StartingSectorStarting Sector)中,所以值得注意的是,尽管有这个名字,这个成员并不存储磁盘扇区号。对镜像文件,这个成员才是一个实际的扇区偏移。

我们已经知道第一个subsection位于控制区之后,但现在我们需要一种方法来找到其他subsection_SUBSECTI0N.NextSubsection存储了下一个subsection的地址,这个成员在最后一个subsection中被设置为0。下面是上面的!ca输出中列出的第一个、第二个subsection的这个成员的内容。

kd> dt nt!_subsection fffffa80027e2400
   +0x000 ControlArea      : 0xfffffa80`027e2380 _CONTROL_AREA
   +0x008 SubsectionBase   : 0xfffff8a0`00cd5aa0 _MMPTE
   +0x010 NextSubsection   : 0xfffffa80`02572d10 _SUBSECTION
   +0x018 PtesInSubsection : 1
   +0x020 UnusedPtes       : 0
   +0x020 GlobalPerSessionHead : (null) 
   +0x028 u                : <unnamed-tag>
   +0x02c StartingSector   : 0
   +0x030 NumberOfFullSectors : 1
kd> dt nt!_subsection 0xfffffa80`02572d10
   +0x000 ControlArea      : 0xfffffa80`027e2380 _CONTROL_AREA
   +0x008 SubsectionBase   : 0xfffff8a0`01ae7000 _MMPTE
   +0x010 NextSubsection   : (null) 
   +0x018 PtesInSubsection : 0xff
   +0x020 UnusedPtes       : 0x101
   +0x020 GlobalPerSessionHead : 0x00000000`00000101 _MM_AVL_TABLE
   +0x028 u                : <unnamed-tag>
   +0x02c StartingSector   : 1
   +0x030 NumberOfFullSectors : 0xff

MapViewOfSectionEx创建的VAD指向subsection,但现在我们有多个subsection,所以值得指出的是,VAD仍然只有一个subsection指针,它指向链中的第一个subsection

#### Section对象图

下图是对图41的修正

Untitled 13.png

#### 由映射文件支持的section的原型PTEs

由分页文件支持的section的原型PTEs最初被设置为demand-zeroPTEs,后来它们被用来最终追踪进入分页文件的内存内容。对于这样的section,要使用的文件是隐含的分页文件,所以唯一需要的信息是哪个分页文件和该文件的偏移量,这些信息被存储到
_MMPTE.u.Soft.PageFileLow_MMPTE.u.Soft.PageFileHigh中(前者只是一个索引)。

对于一个映射文件支持的section,情况就不太一样。首先,将原型PTE初始化为demand-zero的PTE是没有意义的,因为当一个section页面第一次被访问时,它必须用从文件中读取的数据来填充。此外,原型PTEs必须以某种方式识别整个文件系统中的文件,因为它不再是分页文件了。

为了解决这些问题,原型PTE被初始化为_MMPTE_SUBSECTION的实例,SubsectionAddress指向_SUBSECTI0N描述原型PTE映射的虚拟范围。从_SUBSECTION,VMM可以得到_CONTROL_AREA_FILE_OBJECT。原型PTE不会存储文件中的偏移量,因为这个信息可以从以下地方找到:

  • section中第一个PTE的偏移量,其地址由_SUBSECTlON.SubsectionBase给出。在base + n 8映射了文件在subsection_start + 0x1000 n处的东西。注意subsection中的偏移量是如何隐含在PTE数组中的位置的。
  • subsection块偏移表示是从文件哪里开始,即subsection_start,它由_SUBSECTION.StartingSector乘以页面大小给出。

因此,属于一个section的原型PTEs都被初始化为相同的值,指向该subsection本身。下面是一个例子:

ControlArea  @ fffffa80027e2380
  Segment      fffff8a000f2bd40  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 2  Pfn Ref                   1  Mapped Views                2
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  fffffa800281d290  ModWriteCount             0  System Views                1
  WritableRefs                2  PartitionId                0  
  Flags (c080) File WasPurged Accessed 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a000f2bd40
  ControlArea       fffffa80027e2380  ExtendInfo    0000000000000000
  Total Ptes                     100
  Segment Size                100000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa80027e2400
  ControlArea  fffffa80027e2380  Starting Sector        0  Number Of Sectors    1
  Base Pte     fffff8a000cd5aa0  Ptes In Subsect        1  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          2

Subsection 2 @ fffffa8002572d10
  ControlArea  fffffa80027e2380  Starting Sector        1  Number Of Sectors   ff
  Base Pte     fffff8a001ae7000  Ptes In Subsect       ff  Unused Ptes        101
  Flags                   1000d  Sector Offset          0  Protection           6
  Accessed Static 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

下面是在数组中发现的原型PTE的值:

kd> dq fffff8a001ae7000
fffff8a0`01ae7000  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7010  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7020  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7030  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7040  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7050  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7060  fa800257`2d1004c0 fa800257`2d1004c0
fffff8a0`01ae7070  fa800257`2d1004c0 fa800257`2d1004c0

我们可以看到PTE都被设置为相同的值。WinDbg能够解析为_MMPTE_SUBSECTION的一个实例:

kd> !pte fffff8a001ae7000 1
                                           VA fffff8a001ae7000
PXE at FFFFF8A001AE7000    PPE at FFFFF8A001AE7000    PDE at FFFFF8A001AE7000    PTE at FFFFF8A001AE7000
contains FA8002572D1004C0
not valid
 Subsection: FFFFFA8002572D10
 Protect: 6 - ReadWriteExecute

所显示的subsection地址与!ca所显示的一致。

值得注意的是,分页文件支持的section的原型PTE需要存储分页文件的偏移量,因为页面可以写到分页文件的任何地方,而对于映射文件支持的section,文件偏移量是由PTE在数组中的位置得出的。

我们可以从上面的 !pte 的输出中注意到,PTE 保护被设置为 6 - ReadWriteExecute。这个section对象是为一个有读/写保护的数据文件准备的,所以 "Execute "的属性是令人惊讶的。我们将在后面看到,其实硬件PTE的实际保护并不允许执行访问。

#### _MMPTE_SUBSECTION vs _MMPTE_PROTOTYPE

我们在上一节中看到,_MMPTE_SUBSECTION定义了指向一个的原型PTE项的布局,_MMPTE_PROTOTYPE是一个指向原型PTE的原型指针的格式。如果我们看一下这两种数据类型,我们会发现没有一个位可以唯一地识别一个PTE值是属于哪种类型。两者都是通过第0位清零和第10位设置来识别。值得注意的是,WinDbg解释这样的PTE值取决于它所属的地址区域:如果它在分页结构区域,它被解释为一个原型指针,否则就是一个subsection指针。我们可以通过在两个不同地址的同一个值上使用!pte扩展来确认这一点。比方说这个值:

0xCBA98765'43210400

他的0位是清零的,第10位被置1,proto/subsection的地址部分设置为0xCBA9'87654321。记住_MMPTE_PROTOTYPE_MMPTE_SUBSECTION 都只存储48位的地址,因为剩下的16位必须等于第47位(规范形式)。下面是!pte在分页结构区域内对这个值的解释:

kd> dq 1d0000 l1
00000000`001d0000  00000000`001d0000
kd> dq 1d0000
00000000`001d0000  00000000`001d0000 00000000`00000000
00000000`001d0010  00000000`00000000 00000000`00000000
00000000`001d0020  00000000`00000000 00000000`00000000
00000000`001d0030  00000000`00000000 00000000`00000000
00000000`001d0040  00000000`00000000 00000000`00000000
00000000`001d0050  00000000`00000000 00000000`00000000
00000000`001d0060  00000000`00000000 00000000`00000000
00000000`001d0070  00000000`00000000 00000000`00000000
kd> eq 1d0000 0xCBA9876543210400
kd> dq 1d0000 l1
00000000`001d0000  cba98765`43210400
kd> !pte 1d0000
                                           VA 00000000001d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000E80
contains 00C000003D395867  contains 014000003A359867  contains 013000001D01A867  contains A3B000001DF4D867
pfn 3d395     ---DA--UWEV  pfn 3a359     ---DA--UWEV  pfn 1d01a     ---DA--UWEV  pfn 1df4d     ---DA--UW-V

kd> !pte 1d0000 1
                                           VA 00000000001d0000
PXE at 00000000001D0000    PPE at 00000000001D0000    PDE at 00000000001D0000    PTE at 00000000001D0000
contains CBA9876543210400
not valid
 Subsection: FFFFCBA987654321
 Protect: 0
kd> !pte 1d0000
                                           VA 00000000001d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000E80
contains 00C000003D395867  contains 014000003A359867  contains 013000001D01A867  contains A3B000001DF4D867
pfn 3d395     ---DA--UWEV  pfn 3a359     ---DA--UWEV  pfn 1d01a     ---DA--UWEV  pfn 1df4d     ---DA--UW-V

kd> dq FFFFF68000000E80 l1
fffff680`00000e80  a3b00000`1df4d867
kd> eq FFFFF68000000E80 0xCBA9876543210400
kd> dq FFFFF68000000E80 l1
fffff680`00000e80  cba98765`43210400
kd> !pte FFFFF68000000E80
                                           VA 00000000001d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000E80
contains 00C000003D395867  contains 014000003A359867  contains 013000001D01A867  contains CBA9876543210400
pfn 3d395     ---DA--UWEV  pfn 3a359     ---DA--UWEV  pfn 1d01a     ---DA--UWEV  not valid
                                                                                  Proto: FFFFCBA987654321

我们看到它被解释为一个原指针。WinDbg是如何假定地址的16个高位被设置为1的。

下面我们看到当相同的值在一个用户模式的地址上时会发生什么(任何在分页结构区域之外的地址都会得到相同的结果)。

kd> !pte 1d0000 1
                                           VA 00000000001d0000
PXE at 00000000001D0000    PPE at 00000000001D0000    PDE at 00000000001D0000    PTE at 00000000001D0000
contains CBA9876543210400
not valid
 Subsection: FFFFCBA987654321
 Protect: 0

这一次,PTE被解释为一个subsection指针。
这证实了相同的值根据其地址有不同的意义,并表明原指针只在分页结构区域被发现。

Section对象和虚拟地址

值得回顾的是一个在共享内存中已经引入的概念:section对象本身并没有建立文件内容在特定进程中可见的虚拟地址。这是在调用MapViewOfFileEx时完成的,信息被存储到VAD中。section对象将原型PTE和文件联系在一起,所以分页I/O可以在文件本身上执行,而不需要参考文件在许多进程中映射的可见地址范围。

2.2.2 首次访问映射文件页

从本节开始,我们将描述分页文件支持的页面和映射文件支持的页面的生命周期之间的相关区别。

一般来说,分页与分页文件支持的对象的情况类似,但内存内容是从映射的文件中读取和写入。

当第一次访问发生时,一个活动页面的_MMPFN被设置,因此它类似于一个分页文件支持的对象的_MMPFN,但有一些区别。

#### Clean/Dirty 状态

一个分页文件支持的页面被初始化为dirty,因为在分页文件中没有其内容的副本,这与私有内存的情况很相似。

一个由映射文件支持的页面是一个不同的情况,因为当第一次访问发生时,它被初始化为从文件中读取的内容。如果访问是读取的,该页被初始化为clean的,因为它与文件副本相同。更具体地说,当这种访问发生时,原型PTE被设置为:

_MMPTE_HARDWARE.Dirty1 = 0, 这是的文件是只读的(处理器定义的R/W位)

_MMPTE_HARDWARE.Dirty = 0, 和Dirty1位一起,表明这个页面是clean的。

_MMPTE_HARDWARE.Write = 1, 编辑页面对于VMM来说是可写的(软件定义的一个位)

这与分页文件支持的页面不同,分页文件支持的页面设置了DirtyDirty1。硬件PTE为这些位存储了相同的值,页面的_MMPFNu3.e1.Modified = 0,而对于分页文件支持的页面,它被设置为1。 因此,所有与脏度有关的位都是清零的,可以看到,当这样一个页面被带出活动状态时,它被直接移到Standby列表,也就是说,它被视为clean的。

这源于映射文件支持的页面和分页文件支持的页面的生命周期的不同:前者是用文件中的内容初始化的,所以当它们被添加到工作集时,它们在磁盘上的内容副本已经存在;后者是用零填充的,在它们被写入分页文件之前,它们的内容副本不存在。

由于映射文件支持的页必须被文件内容填满,对这种页的第一次访问实际上是一个硬错误,而不是一个demand-zero fault,这也意味着首先搜索Free列表以分配物理页。因此,这种错误在下面的状态图中用粗箭头表示。

Untitled 14.png

最后,我们可以看到,当第一次访问是写的时候,原型PTEDirtyDirty1然是清零的,_MMPFN.u3.e1.Modified也是清零的,但是硬件PTE有DirtyDirty1置1,这足以导致该页在退出活动状态时被移到Modified列表中。更重要的是,由于这样的页面可以在不同的进程中共享,每一个被写入页面的地址空间都在其硬件PTE中设置了Dirty和Dirty1。当页面从这样的地址空间中移出时,VMM设置_MMPFN.u3.e1.Modified;如果有其他地址空间映射该页面,后者仍然是活动的,但是修改状态现在已经传递到_MMPFN中,因此,当它从最后一个地址空间移出时,该页面将被添加到Modified list中。

#### OriginalPte的内容

对于一个分页文件支持的页面,这个成员存储了一个带有页面保护的_MMPTE_SOFTWARE,并最终存储了分页文件偏移。对于一个映射文件支持的页面,它存储了一个_MMPTE_SUBSECTION,指向含有映射的该文件页面的PTEsubsection

#### Executable 保护

具有读/写权限的section的原型PTEs被初始化时,其软件保护设置为6,被WinDbg解释为ReadWriteExecute,这与section保护形成对比。当原型PTEs被设置为指向一个物理页时,它们的内容由处理器定义,第63位指定该页是否可执行。原型值的第63位确实是清零的,因此该页是可执行的,WinDbg通过 "E "标志报告了这一事实;下面是一个例子:

kd> !pte fffff8a001dde1f0 1
                                           VA fffff8a001dde1f0
PXE at FFFFF8A001DDE1F0    PPE at FFFFF8A001DDE1F0    PDE at FFFFF8A001DDE1F0    PTE at FFFFF8A001DDE1F0
contains 000000000D808921  contains 000000000D808921  contains 000000000D808921  contains 000000000D808921
pfn d808      -G--A--KREV  pfn d808      -G--A--KREV  pfn d808      -G--A--KREV  pfn d808      -G--A--KREV

然而,最终的硬件PTE实际上是不可执行的,即第63位被置1。

kd> !pte D0000
                                           VA 00000000000d0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000680
contains 02C000002E01E867  contains 01400000211E5867  contains 0130000021AE6867  contains A5B000000D808867
pfn 2e01e     ---DA--UWEV  pfn 211e5     ---DA--UWEV  pfn 21ae6     ---DA--UWEV  pfn d808      ---DA--UW-V

这表明传递给CreateFileMappingMapViewOfFileEx的参数所指定的保护与原型PTE保护之间的不一致只是表面现象;重要的是要始终记住,原型PTE只是一个工作副本,实际的硬件PTE是由它衍生出来的。

通过一些实验可以验证,为了使一个页面可执行,必须调用MapViewOfFileEx请求FILE_MAP_EXECUTE访问,如果映射对象是在PAGE_READWRITE保护下创建的,则会失败。一般来说,要想拥有一个真正可执行的页面,必须在所有级别上指定这种访问,即:

  • 当文件创建时
  • 当section对象创建时
  • 当view映射时

2.2.3 从工作集中移除映射文件页面

对于分页文件支持的页面和映射文件支持的页面,页面移除的处理方式类似。硬件PTE存储了一个指向VAD的原型指针;当页面共享数下降到0时,原型PTE被设置为Transition,并指向物理页面;该页面被带入ModifiedStandby列表中。

2.2.4 将映射的文件页写入其文件中

分页文件支持的页面和映射文件支持的页面之间的一个重要区别是,VMM使用一个专门的线程来写入映射文件,称为映射页面写入器。
它的工作是在Modified列表中写入被用于映射文件的页面内容,也就是说与Modified页面写入器对分页文件支持的页面所执行的任务基本相同。

当页面从Modified列表转到Standby列表时,可以观察到另一个小的区别。对于一个分页文件支持的页面,这时分页文件的偏移量被分配并记录到_MMPFN.OriginalPte,直到这一刻,它存储了一个demand-zero的PTE。_MMPFN.OriginalPte将是存储分页文件偏移量的唯一地方,直到该页最终被重新使用,其值被复制到原型PTE中。对于一个由映射文件支持的页面,_MMPFN.OriginalPte不被更新:它总是存储一个_MMPTE_SUBSECTION,指向包含该页面的subsection。这足以让我们知道页面内容在文件中的存储位置,当页面被重新使用时,这个值将被放回原型PTE中。

2.2.5 映射文件页面的重新利用

当这种情况发生时,原型PTE被设置回subsection指针。对于一个分页文件支持的页面,原型PTE将偏移量存储到分页文件中。

2.2.6 写入一个clean映射文件页面

对于一个分页文件支持的页面,当这个事件发生时,存储该页面副本的分页文件块被释放,因为它已经过时了,并且_MMPFN.OriginalPte.u.Soft.PageFileLowPageFileHigh都设置为0。当再次将该页写入磁盘的时间到来时,将分配一个新的分页文件块。

对于一个由映射文件支持的页面,OriginalPte没有被修改,因为它存储的是subsection指针。该页的磁盘拷贝显然总是在文件开头的相同偏移处。当该页被写入磁盘时,相应的文件块将被更新。

2.2.7 释放映射文件内存

我们以前说过Windows是如何释放分页文件支持的section内存。这个过程与映射文件支持的section类似,但有一个重要区别。

一个分页文件支持的section是用来管理一个或多个进程映射的虚拟内存区域的。当所有这些进程都取消了它们的视图并关闭了它们对该section的句柄时,内存内容可以被销毁。毕竟,没有人希望VMM在任何地方保存取消分配了的内存内容,也没有必要将其写入分页文件。因此,当最后一个进程关闭其句柄时,该section就被销毁了。

映射文件支持的section的情况是不一样的:该section的任何脏页都必须写入文件,因为文件映射的设计是为了确保内存中的任何修改都反映在底层文件中。这甚至在映射该section的进程终止后也会发生:当VMM决定这样做时,脏页由映射页面写入器写入,这与进程的寿命不同步。

因此,对于映射的文件,只要有脏页,section对象就会被保留。如果没有进程正在映射该section的视图,这些页面将被保留在Modified列表中。即使没有进程对该section有一个打开的句柄,后者也会被保留,只要它在Modified列表上有页面。

我们可以通过实验观察到,大文件(例如1GB)的section会被保留一段时间,即使它们的页面已经clean,这意味着它们被保留在Standby列表中。这可能是一种缓存的形式:当一个大文件被带入内存后,它被保留在Standby列表中(如果有足够的空闲内存),如果再次需要它,它可以被快速检索。如果以后有进程为同一个文件调用MapViewOfFileEx,现有的section就会被重新使用,待机页中的所有内容都可以随时使用,不需要I/O

图49所示的状态图仍然有效,另外还要考虑到,只有在脏页被刷入文件后,映射的文件支持的section才会被销毁,即使在这之后,该section还可以保留一段时间。

我们说,当一个页面通过修剪从工作集中移除时,它就进入了Modified状态。我们现在可以理解,对于映射的文件来说,在内存取消分配时也会进入Modified状态。

2.2.8 映射文件和常规文件系统I/O

#### 通过Section Object 进行I/O

映射文件不仅仅是程序员访问文件内容的一种方便的方式:它们是Windows中文件系统I/O的核心。当通过ReadFileWriteFile访问一个文件时,在幕后会创建一个section对象。缓存管理器在系统区域的虚拟地址范围内映射该section的视图,从那里检索和更新文件数据。

我们现在要通过深入文件数据结构来看到这种情况的发生,使用I/O APIs调用来读取一个1GB文件的第一个字节。关键的信息是_FILE_OBJECTSectionObjectPointer成员:它存储了_SECTION_OBJECT_POINTERS实例的地址,其DataSectionObject成员持有该section__CONTROL_AREA的地址。(这节是对实验2.7.4的理论讲解)

在对CreateFile的调用之后,在对ReadFile的调用之前,所有的
_SECTION_OBJECT_POINTERS成员,包括DataSectionObject,都被设置为空。下面是该文件的!handle输出:

kd> !handle 0x30

PROCESS fffffa8002d66630
    SessionId: 1  Cid: 0b80    Peb: 7fffffd7000  ParentCid: 08e8
    DirBase: 278fa000  ObjectTable: fffff8a0017cb5c0  HandleCount:  12.
    Image: MemTests.exe

Handle table at fffff8a0017cb5c0 with 12 entries in use

0030: Object: fffffa8002c1dd60  GrantedAccess: 0012019f Entry: fffff8a0017e80c0
Object: fffffa8002c1dd60  Type: (fffffa8000d0ea30) File
    ObjectHeader: fffffa8002c1dd30 (new version)
        HandleCount: 1  PointerCount: 1
        Directory Object: 00000000  Name: \Users\31231\Desktop\memtests.tmp {HarddiskVolume1}

下面是_SECTION_OBJECT_POINTERS的内存:

kd> ?? ((nt!_FILE_OBJECT*)0xfffffa8002c1dd60)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`01527848
   +0x000 DataSectionObject : (null) 
   +0x008 SharedCacheMap   : (null) 
   +0x010 ImageSectionObject : (null)

在调用完ReadFile读取首个字节后,DataSectionObject存储了一个地址:

kd> ?? ((nt!_FILE_OBJECT*)0xfffffa8002c1dd60)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`01527848
   +0x000 DataSectionObject : 0xfffffa80`025e6d60 Void
   +0x008 SharedCacheMap   : 0xfffffa80`02d8f010 Void
   +0x010 ImageSectionObject : (null)

如果我们使用!ca扩展命令查看这个地址,我们能看到控制区的内容:

kd> !ca 0xfffffa80`025e6d60

ControlArea  @ fffffa80025e6d60
  Segment      fffff8a000d04c00  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                  20  Mapped Views                1
  User Ref                    0  WaitForDel                0  Flush Count                 0
  File Object  fffffa8002c1dd60  ModWriteCount             0  System Views                1
  WritableRefs                0  PartitionId                0  
  Flags (8080) File WasPurged 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a000d04c00
  ControlArea       fffffa80025e6d60  ExtendInfo    0000000000000000
  Total Ptes                   40000
  Segment Size              40000000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa80025e6de0
  ControlArea  fffffa80025e6d60  Starting Sector        0  Number Of Sectors 40000
  Base Pte     fffff8a001c00000  Ptes In Subsect    40000  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

值得注意的是,这个subsection包括了0x40000个PTEs,也就是说,有足够的PTEs来映射整个文件:即使只有一个字节被读取,1GB的文件已经被完全映射了。然而,内核只是建立了数据结构来跟踪映射,它实际上并没有分配0x40000个物理页。我们可以通过查看subsection数据中列为Base Pte地址的原型PTEs来确认这一点:

kd> dq fffff8a001c00000 L30
fffff8a0`01c00000  00000000`260fa921 00000000`254dc921
fffff8a0`01c00010  00000000`256dd921 00000000`2611e921
fffff8a0`01c00020  00000000`2601f921 00000000`265e0921
fffff8a0`01c00030  00000000`25961921 00000000`25e62921
fffff8a0`01c00040  00000000`270a3921 00000000`25524921
fffff8a0`01c00050  00000000`25ce5921 00000000`26326921
fffff8a0`01c00060  00000000`260a7921 00000000`262e8921
fffff8a0`01c00070  00000000`263e9921 00000000`2682a921
fffff8a0`01c00080  00000000`25f2b921 00000000`2632c921
fffff8a0`01c00090  00000000`260ed921 00000000`264ae921
fffff8a0`01c000a0  00000000`2642f921 00000000`25ff0921
fffff8a0`01c000b0  00000000`26a31921 00000000`260f2921
fffff8a0`01c000c0  00000000`25ef3921 00000000`25f34921
fffff8a0`01c000d0  00000000`25d35921 00000000`26b36921
fffff8a0`01c000e0  00000000`26477921 00000000`261b8921
fffff8a0`01c000f0  00000000`266f9921 00000000`260ba921
fffff8a0`01c00100  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00110  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00120  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00130  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00140  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00150  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00160  fa80025e`6de004c0 fa80025e`6de004c0
fffff8a0`01c00170  fa80025e`6de004c0 fa80025e`6de004c0

书上说只有第一个PTE指向一个物理页,而其他的是_MMPTE_SUBSECTION的实例指向subsection。但是我调试的时候可以清楚地看到映射了不止一个,查看前几个PTE,和后几个:

kd> !pte fffff8a002400000 1
                                           VA fffff8a002400000
PXE at FFFFF8A002400000    PPE at FFFFF8A002400000    PDE at FFFFF8A002400000    PTE at FFFFF8A002400000
contains 000000002C782921  contains 000000002C782921  contains 000000002C782921  contains 000000002C782921
pfn 2c782     -G--A--KREV  pfn 2c782     -G--A--KREV  pfn 2c782     -G--A--KREV  pfn 2c782     -G--A--KREV

kd> !pte fffff8a001c000F8 1
                                           VA fffff8a001c000f8
PXE at FFFFF8A001C000F8    PPE at FFFFF8A001C000F8    PDE at FFFFF8A001C000F8    PTE at FFFFF8A001C000F8
contains 00000000260BA921  contains 00000000260BA921  contains 00000000260BA921  contains 00000000260BA921
pfn 260ba     -G--A--KREV  pfn 260ba     -G--A--KREV  pfn 260ba     -G--A--KREV  pfn 260ba     -G--A--KREV
kd> !pte fffff8a001c00100 1
                                           VA fffff8a001c00100
PXE at FFFFF8A001C00100    PPE at FFFFF8A001C00100    PDE at FFFFF8A001C00100    PTE at FFFFF8A001C00100
contains FA80025E6DE004C0
not valid
 Subsection: FFFFFA80025E6DE0
 Protect: 6 - ReadWriteExecute

因此只有少部分物理页被实际映射了。虽然原型PTEs确实消耗了物理内存,但是它们在分页池中,所以只要PTEs不指向物理页,VMM最终可以回收这些页面。

鉴于我们刚才所看到的,我们可以理解,系统中任何文件的读取实际上都是作为一个页面错误来处理的:数据被请求给缓存管理器,它映射出该section的一个视图并进行访问。如果数据在内存中(即已经被缓存),它就会被简单地返回给调用者,否则就会发生页面错误,数据就会被带入内存。

我们可以对一个文件的写入进行类似的实验,得到类似的结果:为文件创建一个映射,并分配存储被修改的字节的页面。这告诉我们,文件内容首先被带入内存,然后被修改。之后,映射页面写入者将更新文件系统上的实际内容。

当我们为一个不存在控制区的文件调用CreateFileMapping时,即_SECTION_OBJECT_POINTERS.DataSectionObject被设置为null,同一成员在调用后被设置为控制区的地址。换句话说,不管控制区是由文件读/写隐式创建的还是由CreateFileMapping显式创建的,它的地址都存储在DataSectionObject中。

#### 更多关于Section对象和文件缓存的细节

缓存管理器所产生的页面错误也可能是一个软错误。这可能是因为以前的活动页被放在了一个过渡列表中,但也可能是由于预取。在解决集群内分页的硬错误时,可以预取subsection页面,但这并不是唯一的原因,因为缓存管理器也会执行预取。

我们现在可以理解,Standby列表是文件缓存的核心:缓存的文件内容被保存在Transition页面中,最终成为Standby页面。它们可以是以前的Active页面,也可以是通过预取逻辑直接放入Standy列表的。无论它们的 "过去历史 "如何,它们都可以使文件内容在高速缓存管理器的虚拟地址空间中可见,而不需要访问文件系统。只要VMM不需要重新使用页面,这些数据就可以保留在Standby列表的内存中。

观察一下文件缓存是如何通过原型PTEs实现的是很有意思的。对于一个不属于某个sectionStandby页,在某个进程的分页结构中必须有一个硬件PTE指向它(_MMPTE_TRANSITION的实例,第0位清零)。反过来,页面的_MMPFN通过PteAddressu4.PteFrame指向这个硬件PTE。。因此如果没有分配给它虚拟地址空间槽,这样的Standby页是不可能存在的:由PTE映射的4k范围。这也适用于通过集群内分页预取的非subsection页:它们指向过渡中的PTE

如果缓存管理器预取的Standby页以这种方式管理,缓存的大小将受到系统区域内虚拟空间可用性的限制,因为每个Standby页将消耗一个PTE

而对于section对象,原型PTEs指向Standby页,并由_MMPFN指回,所以作为section一部分的Standby页(或Modified页)并不与任何特定的虚拟地址相联系,也不消耗硬件PTE。

对于每个缓存文件,都有一个section及其原型PTEs数组,内核可以自由地建立它认为合适的subsection和数组,因为没有必要同时映射所有这些PTEs。如果是这样的话,系统中指向缓存文件数据的PTE的总数就必须小于或等于缓存管理器可用的虚拟地址范围除以页面大小。

相反当缓存管理器需要访问这些sections的内容时,它会映射这些sections的视图。它有自己的逻辑来跟踪它是如何使用自己的虚拟地址范围的,但是没有必要把所有的原型PTEs都一次性塞进它。这允许拥有无限的Standby页,也就是说,只要物理内存可用,就可以缓存文件数据。

这不仅意味着缓存管理器可以保留被访问的数据:还意味着它可以自由地预取数据。如果有空闲的物理内存,缓存管理器可以尝试预测文件的访问,并在实际需要之前预取页面。Superfetch是一个利用这个方案的组件。

#### 内存Sections共享

当一个文件被多个进程访问时,内存section在它们之间共享,这就是实现并发文件访问的原因。这就自动地使缓存共享,因为它应该是这样的:所有的进程都要经过相同的内存区,如果一个页面被缓存在Standby中,那么所有的进程都可以使用。

另外,如果我们为一个已经有section(由其SectionObjectPointer指出)的文件调用CreateFileMapping,同一个控制区就会被重复使用。下面是前面测试中的一个例子;在读取文件后,程序调用CreateFileMapping,得到的句柄如下:

kd> !handle 0x38

PROCESS fffffa8002d66630
    SessionId: 1  Cid: 0b80    Peb: 7fffffd7000  ParentCid: 08e8
    DirBase: 278fa000  ObjectTable: fffff8a0017cb5c0  HandleCount:  14.
    Image: MemTests.exe

Handle table at fffff8a0017cb5c0 with 14 entries in use

0038: Object: fffff8a00138d980  GrantedAccess: 000f0007 Entry: fffff8a0017e80e0
Object: fffff8a00138d980  Type: (fffffa8000d0fb80) Section
    ObjectHeader: fffff8a00138d950 (new version)
        HandleCount: 1  PointerCount: 2
        Directory Object: fffff8a000a77360  Name: map
kd> ?? (nt!_SECTION_OBJECT*) 0xfffff8a00138d980
struct _SECTION_OBJECT * 0xfffff8a0`0138d980
   +0x000 StartingVa       : 0xfffffa80`02d66630 Void
   +0x008 EndingVa         : 0xfffff8a0`00a77360 Void
   +0x010 Parent           : 0xfffff880`05b25901 Void
   +0x018 LeftChild        : (null) 
   +0x020 RightChild       : 0xfffff8a0`00a772f0 Void
   +0x028 Segment          : 0xfffff8a0`00d04c00 _SEGMENT_OBJECT
kd> ?? (nt!_MAPPED_FILE_SEGMENT*) 0xfffff8a0`00d04c00
struct _MAPPED_FILE_SEGMENT * 0xfffff8a0`00d04c00
   +0x000 ControlArea      : 0xfffffa80`025e6d60 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : 0x40000
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : 0
   +0x018 SizeOfSegment    : 0x40000000
   +0x020 ExtendInfo       : (null) 
   +0x020 BasedAddress     : (null) 
   +0x028 SegmentLock      : _EX_PUSH_LOCK

控制区的地址与之前在FILE_OBJECT.SectionObjectPointer->DataSectionObject中发现的一样。

一种解释是CreateFileMappingMapViewOfFileEx允许应用程序代码直接执行I/O管理器和缓存管理器在幕后的工作。它们使用VMM管理器提供的服务,这些API向程序暴露了相同的服务。

2.3 Section偏移

值得总结一下我们刚才看到的一些概念,来定义Secttion偏移的概念。

一个Section对象包括一个有序的原型PTE列表,由第一个subsection的PTE组成,然后是下一个subsectionPTE,以此类推。

这些原型PTE中的每一个在逻辑上都与一个文件数据块相关联,也就是说,当它们被设置为指向一个新的物理页面时,该页面将以从文件中读取的某一偏移量的内容被初始化。另外,如果页面内容被修改,它将在某一时刻替换相同的块到文件中。

对于映射的数据文件,文件数据和原型PTE之间的关系是直接的:第一个PTE映射文件数据的前4k,第二个映射文件偏移量0x1000 - 0x1fff之间的数据,以此类推。但这并不是唯一的可能性:我们即将看到,对于镜像文件,这种关系是不同的。然而,在这两种情况下,每个原型PTE在逻辑上都是指文件中某处的4k数据。

一个Section,通过它的原型PTEs列表,定义了虚拟内存范围的内容,该section的视图被映射。映射视图,只是确定了范围可见的起始地址,但范围的内容是由section单独定义的,以及它与文件内容的关系。

如果我们把section内容想象成它在地址空间中的映射,并从0开始给它的字节分配地址,我们就会得到一个与section内容的每个字节相关的逻辑地址。我们这个逻辑地址称为section偏移。当section的视图被映射到一个起始地址时,每个字节的VA是起始地址和section偏移量的总和。section偏移量是在文件内容的内存布局通过关联(即映射)每个文件块到其原型PTE而建立后,从section的第一个字节开始的偏移。

MapViewOfFileEx允许通过其dwFileOffsetHighdwFileOffsetLow参数指定视图开始的section对象的偏移。它们的意义就是我们刚才定义的section偏移:在视图起始地址映射的字节是section偏移等于指定值的字节。

值得注意的是,函数文档指出,这些值指定了文件的偏移量,正如它们的名字所暗示的那样,但这只对映射的数据文件准确,因为section偏移量和文件偏移量是相等的。对于我们将要讲解的镜像文件,这两个值有不同的意义,因为取决于文件是如何在内存中映射的,dwFileOffsetHigh, dwFileOffsetLow指定了一个Section偏移,即在内容被映射到原型PTEs之后的偏移。

2.4 映射镜像文件

2.4.1 镜像文件映射的使用

镜像文件映射是Windows在内存中加载可执行文件的方式:一个进程的可执行文件和它所加载的DLLs在虚拟地址空间中都是作为映射镜像文件可见的。当一个进程调用LoadLibrary来动态加载一个DLL时,镜像文件映射就在幕后发生。VMM对映射文件进行分页的能力使它有可能对可执行文件进行分页,就像对虚拟地址空间的其他部分一样。

由于可执行文件是映射文件,它们有一个section对象,例如,可以通过包含虚拟区域的VAD找到它。下面是Calc.exe实例的!vad输出的节选:

kd> !process 0 1 calc.exe
PROCESS fffffa8002ded060
    SessionId: 1  Cid: 08ac    Peb: 7fffffd9000  ParentCid: 08e8
    DirBase: 30750000  ObjectTable: fffff8a001b829a0  HandleCount:  93.
    Image: calc.exe
    VadRoot fffffa8002ba0170 Vads 157 Clone 0 Private 1346. Modified 0. Locked 0.
    DeviceMap fffff8a000cef260
    Token                             fffff8a001bf7060
    ElapsedTime                       00:00:13.474
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
    QuotaPoolUsage[PagedPool]         182248
    QuotaPoolUsage[NonPagedPool]      18784
    Working Set Sizes (now,min,max)  (2978, 50, 345) (11912KB, 200KB, 1380KB)
    PeakWorkingSetSize                2978
    VirtualSize                       93 Mb
    PeakVirtualSize                   96 Mb
    PageFaultCount                    3220
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      1518

kd> !vad fffffa8002ba0170
VAD           Level     Start       End Commit
...
fffffa80028126a0  6     ff8f0     ff9d2      6 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\calc.exe

这个VAD实例给出了第一个subsection的地址:

kd> ?? ((nt!_MMVAD*)0xfffffa80028126a0)->Subsection
struct _SUBSECTION * 0xfffffa80`02a2cc40
   +0x000 ControlArea      : 0xfffffa80`02a2cbc0 _CONTROL_AREA
   +0x008 SubsectionBase   : 0xfffff8a0`01b3a058 _MMPTE
   +0x010 NextSubsection   : 0xfffffa80`02a2cc78 _SUBSECTION
   +0x018 PtesInSubsection : 1
   +0x020 UnusedPtes       : 0
   +0x020 GlobalPerSessionHead : (null) 
   +0x028 u                : <unnamed-tag>
   +0x02c StartingSector   : 0
   +0x030 NumberOfFullSectors : 3

通过subsection,我们可以得到控制区域的地址:

kd> ?? (((nt!_MMVAD*)0xfffffa80028126a0)->Subsection)->ControlArea
struct _CONTROL_AREA * 0xfffffa80`02a2cbc0
   +0x000 Segment          : 0xfffff8a0`01b3a010 _SEGMENT
   +0x008 DereferenceList  : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x018 NumberOfSectionReferences : 1
   +0x020 NumberOfPfnReferences : 0x8a
   +0x028 NumberOfMappedViews : 1
   +0x030 NumberOfUserReferences : 2
   +0x038 u                : <unnamed-tag>
   +0x03c FlushInProgressCount : 0
   +0x040 FilePointer      : _EX_FAST_REF
   +0x048 ControlAreaLock  : 0n0
   +0x04c ModifiedWriteCount : 0
   +0x04c StartingFrame    : 0
   +0x050 WaitingForDeletion : (null) 
   +0x058 u2               : <unnamed-tag>
   +0x068 LockedPages      : 0n1
   +0x070 ViewList         : _LIST_ENTRY [ 0xfffffa80`02812700 - 0xfffffa80`02812700 ]

ControlArea的地址可以直接丢给!ca扩展;我们必须使用@@( )操作符来指定使用C++评估器(之前不需要,因为??默认为C++)。

kd> !ca @@( (((nt!_MMVAD*)0xfffffa80028126a0)->Subsection)->ControlArea)

ControlArea  @ fffffa8002a2cbc0
  Segment      fffff8a001b3a010  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                  8a  Mapped Views                1
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  fffffa80027273b0  ModWriteCount             0  System Views             ffff
  WritableRefs         c000000f  PartitionId                0  
  Flags (a0) Image File 

      \Windows\System32\calc.exe

Segment @ fffff8a001b3a010
  ControlArea       fffffa8002a2cbc0  BasedAddress  00000000ff8f0000
  Total Ptes                      e3
  Segment Size                 e3000  Committed                    0
  Image Commit                     5  Image Info    fffff8a001b3a770
  ProtoPtes         fffff8a001b3a058
  Flags (820000) ProtectionMask 

Subsection 1 @ fffffa8002a2cc40
  ControlArea  fffffa8002a2cbc0  Starting Sector        0  Number Of Sectors    3
  Base Pte     fffff8a001b3a058  Ptes In Subsect        1  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

Subsection 2 @ fffffa8002a2cc78
  ControlArea  fffffa8002a2cbc0  Starting Sector        3  Number Of Sectors  307
  Base Pte     fffff8a001b3a060  Ptes In Subsect       61  Unused Ptes          0
  Flags                       6  Sector Offset          0  Protection           3

Subsection 3 @ fffffa8002a2ccb0
  ControlArea  fffffa8002a2cbc0  Starting Sector      30a  Number Of Sectors   88
  Base Pte     fffff8a001b3a368  Ptes In Subsect       11  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

Subsection 4 @ fffffa8002a2cce8
  ControlArea  fffffa8002a2cbc0  Starting Sector      392  Number Of Sectors   27
  Base Pte     fffff8a001b3a3f0  Ptes In Subsect        5  Unused Ptes          0
  Flags                       a  Sector Offset          0  Protection           5

Subsection 5 @ fffffa8002a2cd20
  ControlArea  fffffa8002a2cbc0  Starting Sector      3b9  Number Of Sectors   33
  Base Pte     fffff8a001b3a418  Ptes In Subsect        7  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

Subsection 6 @ fffffa8002a2cd58
  ControlArea  fffffa8002a2cbc0  Starting Sector      3ec  Number Of Sectors  314
  Base Pte     fffff8a001b3a450  Ptes In Subsect       63  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

Subsection 7 @ fffffa8002a2cd90
  ControlArea  fffffa8002a2cbc0  Starting Sector      700  Number Of Sectors    2
  Base Pte     fffff8a001b3a768  Ptes In Subsect        1  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

我们可以看到,该section是由多个subsection组成的,我们将很快分析镜像文件结构和Section对象之间的关系。

通过映射加载可执行文件的一个重要影响是,它们是共享的:如果我们启动第二个calc实例,我们会看到它使用相同的Section对象,所以只有一份可执行代码存在于物理内存中。这种共享在可执行文件和DLL中都有发生,虽然它对节省宝贵的物理内存非常有用,但它也提出了一个问题:每个进程如何能拥有一份包含在可执行文件或DLL中的静态变量的私有拷贝?当我们在后面讨论写时拷贝保护时,答案将变得清晰。

通过读/写API访问的文件或作为数据文件映射的文件在控制区的地址上存储在_SECTION_OBJECT_POINTERS.DataSectionObject. 当一个文件被映射为镜像文件时(我们将很快解释如何实现),控制区的地址被存储在_SECTION_OBJECT_POINTERS.ImageSectionObject。因此,两种不同类型的内存section使用不同的成员。

2.4.2 镜像文件格式概述

本节包含对可执行文件格式的简短介绍。

x64镜像文件采用可移植的可执行32+格式(PE32+),由一组头文件和称为节的块组成,这相当不幸,因为它们不应该与文件的section对象相混淆。为了避免混淆,我们在提到文件内容时将使用术语file section

代码、数据和其他存储到可执行文件中的信息(如资源)有单独的文件section

文件section在文件中是对齐的,也就是说,它们从文件开始的偏移量是一个叫做文件对齐(file alignment) 值的整数倍。文件对齐可以有不同的值,但最常见的是0x200,这也是大多数硬盘上的磁盘扇区的大小。

一个PE32+文件也有一个section对齐,它指定了文件section的数据在内存中的对齐方式,一旦它被映射到一个地址空间。一个文件section的第一个字节的VA必须是section对齐值的倍数。section对齐本身必须是页面大小的倍数,通常等于页面大小。

因此最常见的对齐值是0x200用于文件对齐,0x1000用于section对齐。

一个PE32+文件指定了每个文件section必须被映射的偏移量(如上一节所定义)。在 PE32+ 的术语中,该section偏移量被称为相对虚拟地址。作为一个例子,下面是 calc.exe 转储的摘录:

D:\MicrosoftOs>dumpbin /HEADERS calc.exe
Microsoft (R) COFF/PE Dumper Version 14.29.30137.0
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file calc.exe

PE signature found

File Type: EXECUTABLE IMAGE
[...]
SECTION HEADER #1
   .text name
   60CC9 virtual size
    1000 virtual address (0000000100001000 to 0000000100061CC8)
   60E00 size of raw data
     600 file pointer to raw data (00000600 to 000613FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read

  Debug Directories

        Time Type        Size      RVA  Pointer
    -------- ------- -------- -------- --------
    4A5BC9D4 cv            21 00061CA8    612A8    Format: RSDS, {E95BB5E0-8CE6-40A0-9C3D-BF3DFA3ABCB4}, 2, calc.pdb
    4A5BC9D4 (   A)         4 00061CA4    612A4    BB03197E

SECTION HEADER #2
  .rdata name
   10EC4 virtual size
   62000 virtual address (0000000100062000 to 0000000100072EC3)
   11000 size of raw data
   61400 file pointer to raw data (00061400 to 000723FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         Read Only

SECTION HEADER #3
   .data name
    4E80 virtual size
   73000 virtual address (0000000100073000 to 0000000100077E7F)
    4E00 size of raw data
   72400 file pointer to raw data (00072400 to 000771FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         Read Write
...

virtual address前显示的数值是每个section的RVA或section偏移量。第一个文件section必须在内存section开始的+0x1000处,第二个在+0x62000处,以此类推。这个转储是由Windows SDK中的工具dumpbin.exe创建的。

2.4.3 映射镜像文件的Section Object

SEC_IMAGE传递到CreoteFileMappingflProtect参数中的来映射一个镜像文件。这就是告诉函数检查文件内容并相应地映射它。如果我们不指定这个标志,镜像文件将被视为数据文件,并被映射为一个连续的块。如果我们很调皮,为一个不是可执行文件的文件传递了SEC_IMAGECreateFileMapping就会失败,GetLastError返回ERROR_BAD_EXE_FORMAT

值得注意的是,错误是由CreateFileMapping返回的,而不是由MapViewOfFileEx返回的:是前者创建了Section对象,并将原型PTEs与文件内容联系起来,它是通过 "理解 "PE32+格式来实现的。

在一个有效的可执行文件中,一个Section对象被创建,它似乎对每个文件section都有一个不同的section实例。让我们再看一下calc.exe的转储。

Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file calc.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
            8664 machine (x64)
               6 number of sections
        4A5BC9D4 time date stamp Tue Jul 14 07:57:08 2009
               0 file pointer to symbol table
               0 number of symbols
              F0 size of optional header
              22 characteristics
                   Executable
                   Application can handle large (>2GB) addresses

OPTIONAL HEADER VALUES
...
SECTION HEADER #1
   .text name
   60CC9 virtual size
    1000 virtual address (0000000100001000 to 0000000100061CC8)
   60E00 size of raw data
     600 file pointer to raw data (00000600 to 000613FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read
SECTION HEADER #2
  .rdata name
   10EC4 virtual size
   62000 virtual address (0000000100062000 to 0000000100072EC3)
   11000 size of raw data
   61400 file pointer to raw data (00061400 to 000723FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         Read Only
...

对于第一个文件section,我们可以查看他section对象的!ca输出:

kd> !vad fffffa80028f21d0
VAD           Level     Start       End Commit
...         
fffffa80017b2e10  6     ff980     ffa62      6 Mapped  Exe  EXECUTE_WRITECOPY  \Users\31231\Desktop\calc.exe
...
kd> !ca @@( (((nt!_MMVAD*)0xfffffa80017b2e10)->Subsection)->ControlArea)

ControlArea  @ fffffa80015949d0
  Segment      fffff8a0016c0850  Flink      0000000000000000  Blink        fffffa8002888a68
  Section Ref                 1  Pfn Ref                  4e  Mapped Views                1
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  fffffa8002c6edc0  ModWriteCount             0  System Views             ffff
  WritableRefs         c000000f  PartitionId                0  
  Flags (40a0) Image File Accessed 

      \Users\31231\Desktop\calc.exe

Segment @ fffff8a0016c0850
  ControlArea       fffffa80015949d0  BasedAddress  00000000ff980000
  Total Ptes                      e3
  Segment Size                 e3000  Committed                    0
  Image Commit                     5  Image Info    fffff8a0016c0fb0
  ProtoPtes         fffff8a0016c0898
  Flags (820000) ProtectionMask 

Subsection 1 @ fffffa8001594a50
  ControlArea  fffffa80015949d0  Starting Sector        0  Number Of Sectors    3
  Base Pte     fffff8a0016c0898  Ptes In Subsect        1  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

Subsection 2 @ fffffa8001594a88
  ControlArea  fffffa80015949d0  Starting Sector        3  Number Of Sectors  307
  Base Pte     fffff8a0016c08a0  Ptes In Subsect       61  Unused Ptes          0
  Flags                       6  Sector Offset          0  Protection           3
...

让我们首先注意到,文件转储显示,对于文件的第1部分,file pointer to raw data的值是0x600。这是该文件section从文件开始的偏移量,告诉我们在它之前还有0x600字节的其他数据(文件头)。这个初始数据块由第1小节映射。

我们可以通过映射文件视图和并检查第一页的内容来确认这一点,这与文件内容是一样的。这是文件内容的一个摘录:

Untitled 15.png

下面是内存中的内容:

kd> db 0`ff980000
00000000`ff980000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
00000000`ff980010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
00000000`ff980020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`ff980030  00 00 00 00 00 00 00 00-00 00 00 00 f0 00 00 00  ................
00000000`ff980040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
00000000`ff980050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
00000000`ff980060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS 
00000000`ff980070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

我们可以看到subsection 1的Starting Sector设置为0,这意味着页面内容从文件偏移0的地方读取。

subsection 2的情况变得更加有趣。首先,让我们把注意力集中在第1个字节的section偏移上:subsection是由单个原型PTE组成的(Ptes In Subsect等于1),所以subsection 2的section偏移是0x1000。同样的值被报告为文件内容中文件section #1的RVA,作为virtual address被转储。这个事实告诉我们,subsection 2存储的是文件section #1的内容。此外,文件section #1列出的虚拟大小等于0x60cc9,subsection 2有0x61个原型PTEs,所以它完整存储了文件section #1

subsection 2Starting Sector 被设置为3。如果我们把它乘以该文件的对齐方式,即0x200,我们得到0x600(该文件的对齐方式被dumpbin列在文件的可选头值中,这里省略)。文件转储显示,文件section #1的原始数据的文件指针等于相同的值,所以Starting Sector用于记录在文件中找到subsection数据的位置:它等于数据的文件偏移量除以文件对齐大小。

到目前为止,我们所看到的告诉我们,CreateFileMapping创建了一个subsection来映射文件头,并为文件section 1创建了一个subsection,它紧随其后。

如果我们检查其他文件sections和subsections,我们可以确认每个subsection前面的原型PTE的数量是这样的,即subsection偏移量等于文件节的RVA。例如,Subsection 2的Ptes In Subsect等于0x61,所以subsection 1和subsection 2总共含有了0x62个PTE或0x62000字节的虚拟地址空间,而下一个文件section(#2)的RVA是0x62000。

下面分别是dumpbin的输出和!ca的输出,他们是相互对应的:

SECTION HEADER #2
  .rdata name
   10EC4 virtual size
   62000 virtual address (0000000100062000 to 0000000100072EC3)
   11000 size of raw data
   61400 file pointer to raw data (00061400 to 000723FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         Read Only
Subsection 3 @ fffffa8001594ac0
  ControlArea  fffffa80015949d0  Starting Sector      30a  Number Of Sectors   88
  Base Pte     fffff8a0016c0ba8  Ptes In Subsect       11  Unused Ptes          0
  Flags                       2  Sector Offset          0  Protection           1

文件偏移计算:

Starting Sector x 文件对齐 = 0x30a x 0x200 = 61400 = file pointer to raw data

有兴趣的可以自己实验检查其余的文件sections(有6个),并确认每个section都是由自己的subsection映射的。

还值得注意的是,这个分析表明,起始扇区必须乘以文件对齐方式才能得到文件偏移,而对于数据文件,必须乘以页面大小。

文件section #3 看起来特别有意思:

SECTION HEADER #3
   .data name
    4E80 virtual size
   73000 virtual address (0000000100073000 to 0000000100077E7F)
    4E00 size of raw data
   72400 file pointer to raw data (00072400 to 000771FF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         Read Write

转储显示,虚拟大小比原始数据大0x80字节。对于其他文件section则相反,原始数据等于虚拟大小,四舍五入为文件对齐的倍数,但在这里,我们发现内存中的内容比磁盘上的内容要长。这意味着CreateFileMapping不仅仅是将文件section的内容读入内存,而是,它以某种方式转换了存储在文件中的数据。

这就给我们带来了一个问题:如果页面内容被修改了,考虑到它在加载时已经被转换了,怎么能把它写回文件中呢?答案是不能。如果我们看一下该subsection的保护,我们会发现它等于5,这意味着写时复制。我们将在后面更详细地描述这种保护,它的作用是原始文件不被更新。

总之一个镜像文件是由一个section对象映射的,每个映射的文件section都有不同的subsection;这些subsection的排列方式是,每个subsection开始的偏移量等于文件中指定的偏移量;一些subsection的内容是从文件section内容中衍生出来的,而不是简单的复制。

将每个文件section映射到一个不同的subsection,允许文件section有不同的内存保护,因为后者是在subsection级别上指定的。这使得镜像文件某些部分只读、某些部分可执行等成为可能。

由此看来,CreateFileMapping实际上实现了可执行加载器的一部分。然而,我们不能忘记,实际加载需要的不仅仅是映射文件section(例如,必须解决依赖关系)。

2.4.4 映射镜像文件的硬件PTEs

硬件PTE的处理方式与映射数据文件的处理方式略有不同:当页面从工作集中删除时,相应的硬件PTE被设置为具有实际原型PTE地址的proto-pointer。对于数据文件,地址部分通常被设置为0xF...F,意思是 "引用VAD"。

2.5 Sections与文件关联起来的影响

一个由分页文件的支持的section可能只引用分页文件的内容,而不涉及系统中的其他文件。

相反,一个映射文件支持的section引用的是是系统上的普通文件,这些文件不像分页文件那样由VMM独家控制。考虑以下情况:一个进程映射了一个大文件,在内存中更新它,然后退出。VMM保留section对象和修改列表上的页面,直到映射页面写入器完成对文件的写入。在写入过程中(或开始之前),另一个进程删除了该文件。这是完全可能的,因为在第一个进程终止后,没有人对该文件有一个打开的句柄。在这种情况下,VMM被告知该文件不再存在,并立即释放脏页。观察这种行为是非常有趣的,后面我们会进行单独的实验。

一般来说,负责文件I/O的组件必须与I/O管理器交互,以说明这些影响。

这给我们带来了进一步的问题:让我们再次考虑一个已经被更新并关闭的映射文件,假设它的内容仍然在修改列表中,那么磁盘上的文件实际上并没有被更新,至少没有完全被更新。由于该文件已经关闭,没有人拥有它的句柄,另一个进程可以打开它,并用ReadFile来读取它。这个进程会看到磁盘上拷贝的陈旧内容吗?也许是被破坏的内容,只是部分的更新?不会,这要归功于一个事实,即对文件的所有访问都使用了一个单一的section对象,包括那些用ReadFileWriteFile进行的访问。

我们现在可以理解,所有的文件访问都要经过section对象是很基本的流程:因为outpaging是由映射页面写入器异步完成的,文件内容的唯一更新副本是在section对象中。磁盘上的副本将在稍后的、未确定的时间内得到更新。只要section对象存在,就必须用它来访问文件。

2.6 写拷贝

我们已经知道镜像文件section是在进程之间共享的。当时我们提出一个进程如何能拥有一个模块静态变量的私有拷贝。答案是在section页的保护中,它被设置为写拷贝。

当这样的页面被激活时,硬件PTE_MMPTE.Hard.Dirty1 clear,这使得它在处理器看来是一个只读页,而_MMPTE.Hard.CopyOnWrite set,这被处理器忽略,并告诉VMM这是一个写时复制页。

试图写入该页会导致一个页面错误,处理例程会分配一个新的物理页,将共享页的内容复制到其中,并将硬件PTE指向该页。新的页面对于发生错误的进程来说是私有的,也就是说,它与内存区没有关系。_MMPFN.PteAddress和_MMPFN.u4.PteFrame指的是硬件PTE,而不是原型PTE_MMPFN.u4.PrototypePte是clear的; _MMPFN.OriginalPte不再被设置为subsection指针,而是设置为一个_MMPTE_SOFTWARE,只有保护成员根据页面保护设置(减去写时复制状态--通常被设置为读/写)。如果这样的页面后来被写入备份存储,它将进入分页文件,而不是进入该section映射的文件。简而言之,该进程现在有一个页面的私有拷贝。

这显然具有使共享页不被触及的效果,并为需要更新它的进程提供了一个私有副本,在那里进行内存写入。这就是镜像中的静态变量被更新时的情况。

当这种情况发生时,内存section会被相应地更新,也就是说,共享页的共享计数会被递减,就像更新页面的进程调用了UnmapViewOfFile一样,这与共享页不再被映射在进程VA空间的事实是一致的。

一个镜像文件将每个文件section的保护作为其内容的一部分来指定,所以文件section是由具有要求的保护的内存subsection来映射的。读/写文件sections,如.data(发现静态变量的地方)被隐含地映射为写时复制。当我们调用CreateFileMapping将一个文件映射为镜像时,我们不需要明确指定写时拷贝保护。

然而,写时拷贝保护并不只保留给镜像文件。如果我们调用CreateFileMapping时将PAGE_WRITECOPY传入flProtect,我们就可以为数据文件创建一个具有写时拷贝保护的映射。在分配非section内存时,我们不能把PAGE_WRITECOPY传给VirtualAllocEx,因为这没有意义。写时拷贝是为了自动创建section页的私有拷贝。

写拷贝页上的副本具有等于5的软件保护。对映射数据的文件,这个值被存储在_MMVAD.u.VadFlags;对于用于镜像文件的写拷贝subsection
则存储在_SUBSECTION.u.SubsectionFlags中。当一个物理页被映射时,这个软件保护告诉VMM如何设置硬件PTE

2.7 映射文件相关实验

2.7.1 MemTests 程序的使用

MemTests程序有很多关于VMM APIs的测试项。它有一个主菜单的选项,可以映射一个私有内存区域、打开一个文件、映射打开的文件、访问一个内存区域等。

大多数测试从询问用户所有需要的参数开始,然后在每个重要的API调用之前提示用户。在每个提示下,用户可以选择放弃测试,继续测试,或者中断测试。最后的选择是调用DebugBreak函数,进入一个调试器。这个调用是为了在程序运行的系统上附加了内核调试器时使用的。DebugBreak进入调试器时,MemTests是当前进程,所以我们可以立即访问_EPROCESS和地址空间,而不必寻找进程和切换上下文。当调试器控制时,使用$proc伪寄存器是非常方便的,它被设置为当前进程的_EPROCESS地址,而$thread则指向当前的_ETHREAD

因为MemTests我们是有调试符号的,当断在KERNELBASE!DebugBreak是,我们可以使用下面这个命令返回到程序中:

g @$ra

如果调试器没有显示当前行的源码,可能我们需要重新加载以下符号:

.reload /user

加载完符号,我们就能进行源码级别的调试了。在g @$ra之后,我们进入了MemTests函数,该函数提示用户进行选择,所以我们必须单步调试,直到它返回,到达即将进行的操作。

MemTests包括一组对文件进行操作的测试,例如读取或创建映射,这些测试使用一个保存在静态变量中的文件句柄。一个菜单选项允许打开一个文件并将句柄保存在静态变量中,而其他选项对打开的文件进行操作,包括关闭它。静态变量被用来保证句柄在各种测试中的可用性,直到它被关闭。由于MemTests是一个测试程序,对用户的操作几乎没有检查,例如,有可能打开一个新的文件而不关闭已经打开的文件,这就 "泄露 "了句柄。

另一个静态句柄用于section对象,可以用Memory section测试选项来创建。这里的整体逻辑是类似的:一旦一个映射被创建,它就可以被其他的菜单选项重复使用。

MemTests可以分配两个不同的内存区域:一个是用VirtualAllocEx分配的非section区域,一个是用MapViewOfFileEx分配的section视图。这些区域的地址也被保存在静态变量中,所以其他的菜单选项可以访问这些区域页面。

2.7.2 使用数据断点断到VMM的注意事项

观察VMM工作的一个好方法是在MemTests工作时,用ba命令在PTEs_MMPFNs上放置数据断点。例如,MemTests允许在访问一个内存区域之前立即中断。当调试器处于控制状态时,我们可以在即将被访问的PTE上放置一个断点,以便在VMM更新时重新获得控制。在这个问题上,有几件事需要注意。

PTE访问的断点必须只对特定的地址空间启用,因为分页结构区域每个进程都自己有一份。因此我们必须使用ba/p选项。如果断点是针对当前的地址空间,我们可以写成如下形式:

ba access size /p @$proc address

(推荐使用@字符,以帮助调试器理解$proc是一个伪寄存器,而不是一个调试符号)。相反,在_MMPFN成员上的断点不需要/p选项,因为PFN数据库是在全局系统区域。

如果我们想要一个只对当前线程有效的断点,我们必须使用/t选项:

ba access size /t @$thread address

在许多原型PTE的更新上不可能用数据断点进行断点,因为这些PTE经常被映射到系统区域的一个工作虚拟地址上,并使用这个临时虚拟地址而不是常规地址进行更新。

跟踪一个页面的生命周期的一个好方法,例如从ActiveModified,再到Standby,等等,就是在内存写入清除_MMPFN.PteAddress的0位时设置一个条件断点。这个位是用来锁定一个特定的_MMPFN实例进行更新的,也就是说,在改变实例内容之前,这个位是专门设置的。当更新完成并且_MMPFN处于一致状态时,该位被清空。通过在它被清零时断点,我们可以在每次状态改变后立即观察_MMPFN。要设置这样一个断点,可以输入下面的命令:

ba w 1 mmpfn_address +10 "j ((poi(mmpfn_address + 10) & 1) == 0) '';'gc'"

_MMPFN地址+0x10是_MMPFN.PteAddress的偏移量。;j命令评估括号中的表达式,它使用poi( )来解引用地址;如果整个表达式为非零(当==操作符左边的项为零时就会发生),则不执行进一步的命令,调试器会获取控制;如果表达式为零,则执行gc命令,导致恢复执行。这样的断点在每次更新PteAddress最右边的8位时都会被触发,并在0位被设置时自动恢复执行,只有在被清除时才停止。

2.7.3 创建一个带有多个subsections的内存section

作为数据映射的文件的一个section可以有多个subsection。然而,通常情况下,当我们映射一个文件时,即使是一个大文件,VMM也会创建一个subsection来映射它。我们发现多个subsection的一种情况是,当我们逐步增加文件的时候。例如,如果我们打开一个新的文件进行写入,然后向其写入,我们看到VMM分配了一个初始subsection,通常由0x100个PTE组成,覆盖1MB的文件内容。如果文件的增长超过这个限制,就会增加额外的subsection

然后我们选择在文件被创建之前中断,所以我们可以在调用CreateFileWrCreateFile的一个封装)之后再步入,并在文件刚刚被创建时查看section对象指针。(由于我的程序被优化内联了,导致不能源码级别单步调试。F10直接单步跑飞了,若发生这种情况,需重启虚拟机,不然会被缓存管理器影响和干扰。)。中断后进行汇编级调试,也能做这个实验。

kd> !handle 0x30

PROCESS fffffa8002c30060
    SessionId: 1  Cid: 0978    Peb: 7fffffd7000  ParentCid: 0848
    DirBase: 27ed4000  ObjectTable: fffff8a001976910  HandleCount:  12.
    Image: MemTests.exe

Handle table at fffff8a001976910 with 12 entries in use

0030: Object: fffffa8002c8f310  GrantedAccess: 0012019f Entry: fffff8a001a3a0c0
Object: fffffa8002c8f310  Type: (fffffa8000d06a30) File
    ObjectHeader: fffffa8002c8f2e0 (new version)
        HandleCount: 1  PointerCount: 1
        Directory Object: 00000000  Name: \Users\31231\Desktop\memtests.tmp {HarddiskVolume1}

kd> ?? ((nt!_FILE_OBJECT*) 0xfffffa8002c8f310)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`02d20488
   +0x000 DataSectionObject : (null) 
   +0x008 SharedCacheMap   : (null) 
   +0x010 ImageSectionObject : (null)

然后我们恢复执行,并在MemTests的提示下,再次选择在文件被展开之前,也就是在WriteFile被第一次调用之前中断。我们一直走到对WriteFileWr的第一次调用返回,并再次转储section对象指针。

kd> !ca 0xfffffa80`02d7d610

ControlArea  @ fffffa8002d7d610
  Segment      fffff8a001baa4e0  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                   1  Mapped Views                1
  User Ref                    0  WaitForDel                0  Flush Count                 0
  File Object  fffffa8002c8f310  ModWriteCount             0  System Views                1
  WritableRefs                0  PartitionId                0  
  Flags (8080) File WasPurged 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a001baa4e0
  ControlArea       fffffa8002d7d610  ExtendInfo    0000000000000000
  Total Ptes                     100
  Segment Size                100000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa8002d7d690
  ControlArea  fffffa8002d7d610  Starting Sector        0  Number Of Sectors  100
  Base Pte     fffff8a001c79010  Ptes In Subsect      100  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

这个控制区有一个带有0x100个PTEs的subsection

之后,我们让执行继续进行,直到再次显示菜单,然后我们通过选择Debug->Break来获得调试器控制,并再次转储控制区。

kd> !ca 0xfffffa80`02d7d610

ControlArea  @ fffffa8002d7d610
  Segment      fffff8a001baa4e0  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                1001  Mapped Views                1
  User Ref                    0  WaitForDel                0  Flush Count                 0
  File Object  fffffa8002c8f310  ModWriteCount             0  System Views                1
  WritableRefs                0  PartitionId                0  
  Flags (c080) File WasPurged Accessed 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a001baa4e0
  ControlArea       fffffa8002d7d610  ExtendInfo    0000000000000000
  Total Ptes                    1100
  Segment Size               1100000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa8002d7d690
  ControlArea  fffffa8002d7d610  Starting Sector        0  Number Of Sectors  100
  Base Pte     fffff8a001c79010  Ptes In Subsect      100  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa8002782cf0  Blink   fffffa8002404dd0  MappedViews          0

Subsection 2 @ fffffa800279ab10
  ControlArea  fffffa8002d7d610  Starting Sector      100  Number Of Sectors  200
  Base Pte     fffff8a001ca1000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa800244fae0  Blink   fffffa8002b89f20  MappedViews          0

Subsection 3 @ fffffa800244fa90
  ControlArea  fffffa8002d7d610  Starting Sector      300  Number Of Sectors  200
  Base Pte     fffff8a001c8f000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa8002b4c140  Blink   fffffa800279ab60  MappedViews          0

Subsection 4 @ fffffa8002b4c0f0
  ControlArea  fffffa8002d7d610  Starting Sector      500  Number Of Sectors  200
  Base Pte     fffff8a001ca3000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa80024b16a0  Blink   fffffa800244fae0  MappedViews          0

Subsection 5 @ fffffa80024b1650
  ControlArea  fffffa8002d7d610  Starting Sector      700  Number Of Sectors  200
  Base Pte     fffff8a001c72000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa8002c6c350  Blink   fffffa8002b4c140  MappedViews          0

Subsection 6 @ fffffa8001420670
  ControlArea  fffffa8002d7d610  Starting Sector      900  Number Of Sectors  200
  Base Pte     fffff8a001c78000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa8002d94100  Blink   fffffa8002c6c350  MappedViews          0

Subsection 7 @ fffffa8002d940b0
  ControlArea  fffffa8002d7d610  Starting Sector      b00  Number Of Sectors  200
  Base Pte     fffff8a001c94000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa800255d510  Blink   fffffa80014206c0  MappedViews          0

Subsection 8 @ fffffa800255d4c0
  ControlArea  fffffa8002d7d610  Starting Sector      d00  Number Of Sectors  200
  Base Pte     fffff8a001c06000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        fffffa8000d5ba80  Blink   fffffa8002d94100  MappedViews          0

Subsection 9 @ fffffa8002c04770
  ControlArea  fffffa8002d7d610  Starting Sector      f00  Number Of Sectors  200
  Base Pte     fffff8a001c42000  Ptes In Subsect      200  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

这一次,该section有9个subsection,这些subsection是在文件增长时创建的。最初的subsection由0x100个原型PTE组成,其他8个subsection的长度为0x200个PTE,所以控制区总共覆盖了0x1100个PTE,可以跨越0x1100000或17MB,覆盖整个文件。

我们也可以确认这个subsection与我们用以下步骤调用CreateFileMapping时得到的是一样的。

恢复执行后,在主菜单上选择Memory section test(m),并选择映射类型为mapped file(f)(另一种类型为shared memory,是针对paging file backed section)。输入一个有效的保护,例如4,为大小输入0,这意味着我们要映射整个文件,并为映射输入一个名称。剩下的提示是对MapViewOfFileEx的调用,这个测试也会执行;输入一个有效的所需访问权限,比如6,大小和偏移量都是0(即视图覆盖整个部分);选择不指定NUMA节点以避免额外的询问。程序会显示输入数据的摘要,并提示输入一个键,然后在调用CreateFileMapping之前,它又提示了一下。我们选择在这里中断,直到调用CreateFileMapping之后再进行,转储返回的句柄。

kd> r rax
rax=0000000000000038
kd> !handle 0x38

PROCESS fffffa8002c30060
    SessionId: 1  Cid: 0978    Peb: 7fffffd7000  ParentCid: 0848
    DirBase: 27ed4000  ObjectTable: fffff8a001976910  HandleCount:  14.
    Image: MemTests.exe

Handle table at fffff8a001976910 with 14 entries in use

0038: Object: fffff8a0012c9b20  GrantedAccess: 000f0007 Entry: fffff8a001a3a0e0
Object: fffff8a0012c9b20  Type: (fffffa8000d07b80) Section
    ObjectHeader: fffff8a0012c9af0 (new version)
        HandleCount: 1  PointerCount: 2
        Directory Object: fffff8a000edf470  Name: map

然后我们使用一个C++表达式来转变数据类型后查看控制区地址。这个表达式有点复杂,因为我们需要将_SECTION.Segment,它被声明为一个指向_SEGMENT_OBJECT的指针,变成一个指向_MAPPED_FILE_SEGMENT的指针:

kd> ? @@( ((nt!_MAPPED_FILE_SEGMENT*) ((nt!_SECTION_OBJECT*) 0xfffff8a0012c9b20)->Segment)->ControlArea)
Evaluate expression: -6047266253296 = fffffa80`02d7d610

我们得到了一个控制区的地址,这个地址与存储在文件的_SECTION_OBJECT_POINTERS中的地址完全相同。

2.7.4 因为文件读取而创建的Section Object

这个实验展示了内核如何为一个文件创建一个section对象来处理对ReadFile的调用。

首先,我们要创建一个1GB的文件,利用创建的section对象比它所映射的文件长的事实来扩展该文件。我们在MemTests的主菜单中选择创建测试文件,并提供一个名字(注意:如果文件已经存在,它会被覆盖),然后我们选择不进行增量扩展,并在提示符下输入一个键来继续。这样做的结果是为该文件调用CreateFile,而不向其写入任何东西,所以我们有一个空文件的句柄。

现在我们为这个文件创建一个1GB的映射。我们在主菜单中选择memory section test(m),选择maped file(f),保护值4(读/写),大小0x40000000(1GB),一个有效的名字,视图访问权限6(读/写),偏移量0,大小0(视图是针对整个映射的),我们没有指定NUMA节点。从视图访问开始,我们指定的数据是调用MapViewOfFileEx的参数。它们实际上是不需要的,因为我们会在调用CreateFileMapping之后中止测试,但是MemTests在进行这两个调用之前会询问所有的参数。MemTests显示了一个输入数据的摘要,并提示输入一个键来继续。我们继续前进,MemTests在调用CreateFileMapping之前再次提示。我们再次前进,下一次提示是在调用CreateViewOfFileEx之前(这时MemTests打印出 "about to map the view"的信息)。当我们在这里的时候,CreateFileMapping已经创建了一个1GB的映射,这将文件扩展到相同的大小,所以我们可以在提示中选择cancel,然后在主菜单中关闭MemTests。我们看到在当前目录下创建了一个1GB的文件。

Untitled 16.png

为了进行一个干净环境下的实验,我们需要确保将我们刚刚创建的section对象从内存中驱逐出去,因为即使在程序终止后,它也可以继续驻留。确保这一点的最简单方法是重新启动系统。

现在我们要从我们的文件中读出1个字节,并查看section对象。

在主菜单上,我们选择File read test(f),输入上一步创建的文件名,0xc0000000代表访问(读/写),在文件被打开之前我们中断。我们一直走到对MyOpenFile的调用返回并检查文件对象:

kd> !handle 0x30

PROCESS fffffa8002e066f0
    SessionId: 1  Cid: 0ae8    Peb: 7fffffd9000  ParentCid: 08c0
    DirBase: 20e72000  ObjectTable: fffff8a00232ae70  HandleCount:  12.
    Image: MemTests.exe

Handle table at fffff8a00232ae70 with 12 entries in use

0030: Object: fffffa8002bcd110  GrantedAccess: 0012019f Entry: fffff8a0023980c0
Object: fffffa8002bcd110  Type: (fffffa8000d05640) File
    ObjectHeader: fffffa8002bcd0e0 (new version)
        HandleCount: 1  PointerCount: 1
        Directory Object: 00000000  Name: \Users\31231\Desktop\memtests.tmp {HarddiskVolume1}
kd> ?? ((nt!_FILE_OBJECT*)0xfffffa8002bcd110)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`02e36468
   +0x000 DataSectionObject : (null) 
   +0x008 SharedCacheMap   : (null) 
   +0x010 ImageSectionObject : (null)

到目前为止,该文件不存在任何section

我们恢复执行,在偏移量上输入0,在长度上输入1,以读取1个字节。MemTests在调用SetFilePointerEx之前会有提示,SetFilePointerEx是用来移动到指定的偏移量。我们对观察这个调用不感兴趣,所以我们输入一个键来继续。

下一个提示是在对ReadFile的调用之前,所以我们在这里中断。我们在调用ReadFile之前选中中断,并确认section ojbect的指针仍然被设置为null。

kd> ?? ((nt!_FILE_OBJECT*)0xfffffa8002bcd110)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`02e36468
   +0x000 DataSectionObject : (null) 
   +0x008 SharedCacheMap   : (null) 
   +0x010 ImageSectionObject : (null)

然后我们执行对ReadFile的调用,再次检查它们:

kd> .process /i fffffa8002e066f0
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03e78490 cc              int     3
kd> ?? ((nt!_FILE_OBJECT*)0xfffffa8002bcd110)->SectionObjectPointer
struct _SECTION_OBJECT_POINTERS * 0xfffffa80`02e36468
   +0x000 DataSectionObject : 0xfffffa80`027d7290 Void
   +0x008 SharedCacheMap   : 0xfffffa80`02c1ae00 Void
   +0x010 ImageSectionObject : (null)

现在,DataSectionObject指向一个控制区,我们dump查看:

kd> !ca 0xfffffa80`027d7290

ControlArea  @ fffffa80027d7290
  Segment      fffff8a0023b8540  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                  20  Mapped Views                1
  User Ref                    0  WaitForDel                0  Flush Count                 0
  File Object  fffffa8002bcd110  ModWriteCount             0  System Views                1
  WritableRefs                0  PartitionId                0  
  Flags (8080) File WasPurged 

      \Users\31231\Desktop\memtests.tmp

Segment @ fffff8a0023b8540
  ControlArea       fffffa80027d7290  ExtendInfo    0000000000000000
  Total Ptes                   40000
  Segment Size              40000000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa80027d7310
  ControlArea  fffffa80027d7290  Starting Sector        0  Number Of Sectors 40000
  Base Pte     fffff8a002400000  Ptes In Subsect    40000  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   0000000000000000  MappedViews          1

数据部分有一个跨越0x40000 PTEs的subsection,即0x40000000字节或1GB,所以读取一个字节会导致为整个文件创建一个section

3 调试 Modified Page Writer (MPW) 和 Mapped Page Writer (MPAW)

3.1 介绍

通过这些实验,我们将阻塞修改页写入器和映射页写入器,这样VMM就不能再重新利用物理页了。没有这两个线程,脏页就会无限期地留在修改列表中。

这个实验可以观察到被修改的页面在被移到Standby列表之前的状态,它也很有趣,从实验上证实了这两个线程的基本作用。

我们会使用到测试驱动,注意修正偏移,避免蓝屏。

此外,当所有的物理内存都用完时,系统就会出现死锁,而两个写程序则一直处于阻塞状态。在连接调试器的情况下,这实际上是很有趣的观察,因为在超时后,控制台中会打印出一条信息,警告说如果没有连接调试器,系统就会崩溃。

我们可以通过在_MMPFN.u3.e1上下数据写入断点来确定修改页面写入器(MPW)和映射页面写入器(MAPW)。

3.2 阻塞MPW线程

3.2.1 概述

使用断点,我们可以看到MPW是一个属于System进程的线程,执行MiModifiedPageWriter函数。它的状态,在空闲时,是这样的。

kd> !thread fffffa8000d08a10
THREAD fffffa8000d08a10  Cid 0004.0058  Teb: 0000000000000000 Win32Thread: 0000000000000000 WAIT: (WrFreePage) KernelMode Non-Alertable
    fffff8000407c520  Gate
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8000ce3040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      35476          Ticks: 21466 (0:00:05:34.871)
Context Switch Count      74             IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.109
Win32 Start Address nt!MiModifiedPageWriter (0xfffff80003e5d230)
Stack Init fffff880047acc70 Current fffff880047ac940
Base fffff880047ad000 Limit fffff880047a7000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`047ac980 fffff800`03ed2992 : 00000000`00000001 fffffa80`00d08a10 fffffa80`01e35310 fffffa80`00000010 : nt!KiSwapContext+0x7a
fffff880`047acac0 fffff800`03e94b8b : 00000000`00000048 fffff800`00b96080 00000000`00000000 fffff800`03e5d230 : nt!KiCommitThreadWait+0x1d2
fffff880`047acb50 fffff800`03e5d28a : fffffa80`00d08a10 00000000`00000001 fffffa80`00ce3040 00000000`00000000 : nt!KeWaitForGate+0xfb
fffff880`047acba0 fffff800`0416acce : fffffa80`00d08a10 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x5a
fffff880`047acc00 fffff800`03ebefe6 : fffff800`0403fe80 fffffa80`00d08a10 fffffa80`00d06660 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`047acc40 00000000`00000000 : fffff880`047ad000 fffff880`047a7000 fffff880`047ac940 00000000`00000000 : nt!KiStartSystemThread+0x16

这个线程通过调用KeWaitForGateMiModifiedPageWriter + 0x55处等待工作,KeWaitForGate是未导出的,没有文档。调用前的代码将一个名为MmModifiedPageWriterGate的静态地址加载到rcx中,表明第一个函数参数是gate的地址。

通过这种技术,我们实际上是在MPW即将进入等待状态时捕获它,而不是在它被唤醒时捕获它。然而,我们可以观察到,当可用的物理内存只占总数的一小部分,并且有许多脏页时,MPW会重复执行MiModifiedPageWriter + 0x55的调用,从等待状态过渡到运行状态,同时向磁盘写入一些页块。因此,我们将看到,当仍有许多脏页等待被写入分页文件时,我们可以拦截它。

用于处理KGATES的函数是未文档化的,也没有被内核导出。通过分析VMM代码,我们可以看到MPW gate是通过调用KeSignalGateBoostPriority发出信号的。这种调用的例子可以在MiObtainFreePages + 0x4b找到,其中rcx被设置为MmModifiedPageWriterGate的地址,这表明函数的第一个参数是被激活的gate的地址。

测试驱动用自己的KGATE地址调用KeSignalGateBoostPriority,当MPW在等待它的时候,这就正确地唤醒了它。在线程恢复后,该门似乎被自动设置为非激活态,也就是说,如果我们使MPW再次等待同一gate,它就会阻塞,直到我们再次向它发出信号。

通过对函数名称的一点猜测,我们看到内核有一个名为KelnitializeGate的函数。为了安全起见,WrkEvent.sys在其初始化程序中调用这个函数。

由于门函数没有被ntkrnlmp.exe导出,驱动程序使用从KeSetEvent导出的偏移量计算它们的地址。这些偏移量是用WinDbg为Windows 7 x64的RTM版本提取的,对于任何其他Windows版本来说都可能是不正确的。这就是为什么测试驱动在不同的Windows版本上会崩溃的原因:它将尝试对无效的地址进行函数调用。

3.2.2 实验

这个实验我们需要三个东西:

  • WrkEvent.sys —— 测试驱动
  • WrkEvent.exe —— 驱动客户端
  • MemTests.exe —— 内存申请程序

测试环境是在内核调试下进行的。

首先要做的是在不同的进程中启动WrkEvCIientMemTests,这样它们在需要时就能准备好。WrkEvCIient加载驱动程序,反过来,它在调试器控制台中打印出其可等待对象的地址。我们应该注意到KGATE的地址,因为我们后面会需要它。

[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA80024EB548
[Log]: WrkEvent[1] address: 0XFFFFFA80024EB560
[Log]: WrkEvent[2] address: 0XFFFFFA80024EB578
[Log]: WrkEvent[3] address: 0XFFFFFA80024EB590
[Log]: WrkEvent[4] address: 0XFFFFFA80024EB5A8
[Log]: Ev ptr array address: 0XFFFFFA80024EB5C0
[Log]: Gate address 0XFFFFFA80024EB5E8
[Log]: Device closed

然后我们分配一个私有内存区域,其大小相当于可用物理内存的%40,我们将用它来生成脏页。我们可以用VirtualAllocEx()测试选项来做这个,为flAllocationType指定0x3000(即MEM_COMMlT | MEM_RESERVE),为flProtect指定4(读/写)。

接下来,我们用调试器中断,并设置以下断点,在MPW调用KeWaitForGate之前捕捉它。

bp nt!MiModifiedPageWriter + 0x55

然后我们用访问区域选项向该区域写入,在内存写入过程中,断点应该就会被命中。

当这种情况发生时,我们把rcx设置为前面提到的驱动kgate的地址,然后恢复执行。

kd> r rcx=0XFFFFFA80024EB5E8
kd> g

MPW将保持阻塞状态,直到我们向驱动客户端发出信号。如果我们监测物理内存的使用情况,我们应该看到类似于下面的图表(用Sysinternal的Process Explorer获得)。

Untitled 17.png

我们看到内存使用量的突然增加,随后是一条几乎平坦的线,表明脏页没有被写入磁盘。这个测试是在一个拥有1024MB物理内存的虚拟机上进行的,我们分配了200M。

我们可以启动另一个MemTests实例并分配其他物理内存,几乎快耗尽了所有的内存,但由于MPW被封锁,VMM仍然无法释放物理页:

Untitled 18.png

第二次分配是150.000.000字节的大小。143MB

我们可以通过在这两种情况下选择Shrink WS选项来进一步减少工作集的大小,如果我们检查2个MemTests实例的工作集,我们看到它们已经被部分修剪(它们的工作集小于分配大小),而VMM正试图释放物理内存:

Untitled 19.png

但物理内存仍然几乎被完全使用:

Untitled 20.png

从工作集中删除页面以释放内存是不够的。这只是把它们放在Modified列表中,但在写入磁盘之前,它们不会被释放(即准备被重新利用),而且写入器也没有做它的工作。我们还可以注意到修改列表的有很高的值:354,684 kB。这些是等待被写入磁盘的页面。

当我们用Signal gate选项向驱动gate发出信号时,内存就被释放了,过了一会儿,断点又被击中。这意味着MPW正试图再次进入等待状态。我们可以禁用断点,不打扰MPW。系统信息图清楚地显示了Gate被触发的时刻,物理内存以下就被释放了。

Untitled 21.png

3.3 阻塞MAPW线程

3.3.1 介绍

MAPW是另一个系统线程,它执行MiMappedPageWriter

kd> !thread fffffa80`00d2cb60
THREAD fffffa8000d2cb60  Cid 0004.007c  Teb: 0000000000000000 Win32Thread: 0000000000000000 WAIT: (WrFreePage) KernelMode Non-Alertable
    fffff80004055fc0  SynchronizationEvent
    fffff80004055fd8  SynchronizationEvent
    fffff80004055ff0  SynchronizationEvent
    fffff80004056008  SynchronizationEvent
    fffff80004056020  SynchronizationEvent
    fffff80004056038  SynchronizationEvent
    fffff80004056050  SynchronizationEvent
    fffff80004056068  SynchronizationEvent
    fffff80004056080  SynchronizationEvent
    fffff80004056098  SynchronizationEvent
    fffff800040560b0  SynchronizationEvent
    fffff800040560c8  SynchronizationEvent
    fffff800040560e0  SynchronizationEvent
    fffff800040560f8  SynchronizationEvent
    fffff80004056110  SynchronizationEvent
    fffff80004056128  SynchronizationEvent
    fffff80004056140  SynchronizationEvent
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8000ce2040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      131432         Ticks: 291 (0:00:00:04.539)
Context Switch Count      10             IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.093
Win32 Start Address nt!MiMappedPageWriter (0xfffff80003eca120)
Stack Init fffff88004908c70 Current fffff88004908640
Base fffff88004909000 Limit fffff88004903000 Call 0000000000000000
Priority 17 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`04908680 fffff800`03e95992 : fffffa80`00d2cb60 fffffa80`00d2cb60 00000000`00000000 00000000`00000008 : nt!KiSwapContext+0x7a
fffff880`049087c0 fffff800`03e94eaa : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiCommitThreadWait+0x1d2
fffff880`04908850 fffff800`03eca1cb : 00000000`00000011 fffff880`04908b50 fffffa80`00000001 fffff8a0`00000008 : nt!KeWaitForMultipleObjects+0x272
fffff880`04908b10 fffff800`0412dcce : fffffa80`00d2cb60 00000000`00000080 fffffa80`00ce2040 00000000`00000000 : nt!MiMappedPageWriter+0xab
fffff880`04908c00 fffff800`03e81fe6 : fffff800`04002e80 fffffa80`00d2cb60 fffffa80`00d2c040 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04908c40 00000000`00000000 : fffff880`04909000 fffff880`04903000 fffff880`04907580 00000000`00000000 : nt!KiStartSystemThread+0x16

它在MiMappedPageWriter + 0xa6处调用KeWaitForMultipleObjects等待工作。这个函数是文档化的,需要一个指向可等待对象的数组,比如事件,线程将在这些对象上等待。我们将使用WrkEvent.sys提供的数组来阻止这个线程。

3.3.2 实验

这个实验与上一个实验一样,也需要内核调试环境,已经那几个小工具。

第一步是在不同的进程中启动驱动客户端和MemTests。我们必须注意事件数组的地址(而不是单个事件的地址),这将在后面使用。

现在我们需要映射一个文件来产生脏的映射页,这样MAPW就会被唤醒,并在它试图再次等待时碰到断点。要做到这一点,我们使用MemTestsMemory section test选项,选择映射文件。注意,用共享内存选项分配一个分页文件支持的section是没有意义的,因为它的页面将由MPW而不是MAPW写入。如果一个文件当前没有打开,MemTests允许创建一个文件(最好不要选择增量扩展,以避免等待文件被扩展)。我们将map保护设置为读/写,并使用大约90%的可用物理内存的大小;对于视图,我们也将访问设置为读/写(即6是FILE_MAP_READ | FILE_MAP_WRITE),偏移量和大小为0(视图映射整个文件)。

之后,我们用调试器在MiMappedPageWriter + 0xa6处设置一个断点。

然后我们用Access region选项写映射的区域。我们必须记住写到该区域,因为我们想创建脏页。请注意,与MPW的情况一样,断点在MAPW即将进入等待状态时抓住了它。MAPW的行为看起来与MPW的行为不同:它通常在将大部分页面写入磁盘后就会碰到断点。我们可以从以下事实中看出这一点:当MemTests正在向该区域写入时,所使用的物理内存接近90%,然后在MAPW工作时,这个数值急剧下降,之后断点被命中。

kd> g
Breakpoint 1 hit
nt!MiMappedPageWriter+0xa6:
fffff800`03eca1c6 e875aafcff      call    nt!KeWaitForMultipleObjects (fffff800`03e94c40)

当断点被击中时,MAPW即将调用KeWaitForMultipleObjects,其前两个参数是需要等待的对象的数量和可等待对象数组的地址。我们将这两个分别传入rcx和rdx的参数替换为1的计数和WrkEvent.sys创建的事件数组的地址。

[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA80024EB548
[Log]: WrkEvent[1] address: 0XFFFFFA80024EB560
[Log]: WrkEvent[2] address: 0XFFFFFA80024EB578
[Log]: WrkEvent[3] address: 0XFFFFFA80024EB590
[Log]: WrkEvent[4] address: 0XFFFFFA80024EB5A8
[Log]: Ev ptr array address: 0XFFFFFA80024EB5C0
[Log]: Gate address 0XFFFFFA80024EB5E8
[Log]: Device closed
kd> r rcx=1
kd> r rdx=0XFFFFFA80024EB5C0
kd> g

然后我们可以再次写入整个区域,这样所有的页面都是脏的,并观察到已用物理内存计数器不再减少,因为MAPW不工作了。由于MPW仍然在工作,该计数器可能会略有变化,但是MemTests引起的巨大的内存使用量不再减少。

Untitled 22.png

内存爆满后的最后结果:

Without a debugger attached, the following bugcheck would have occurred.
  eb 34b72 5 1 c0000054
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03f5255f cc              int     3

按下g后

Untitled 23.png

现在我们可以观察到文件映射的一个有趣的功能。这个功能的设计是为了保证更新的内存内容被MAPW刷新到文件中,这样文件就会像人们期望的那样被更新。这可能发生在更新文件的进程结束之后,因为MAPW是异步工作的,但它必须发生。当映射文件的程序终止时,映射到文件的脏页不会被移到Free列表中:它们留在修改列表中,直到它们被写入文件中。这与发生在分页文件支持的内存上的情况完全不同:当最后一个映射这样一个section的进程终止时(或者简单地关闭句柄),这个内存立即被释放到Free列表中,因为没有必要向分页文件写入已经被破坏的地址空间的内容。

因此如果我们关闭MemTests,我们会看到使用的物理内存或多或少保持不变,因为脏页仍然存在。

另一个有趣的影响是,现在没有进程对该文件有句柄,因此我们可以从命令提示符中删除它(这样它就真的被删除了,而不仅仅是被移到回收站)。我们看到,随着脏页的释放,使用的内存急剧下降:由于文件已经消失,没有必要(也没有办法)再去更新它,所以VMM释放了这些页。

同样有趣的是,如果我们在Windows资源管理器中删除该文件,将其移至回收站,物理内存并没有被释放:该文件仍然存在,所以它仍然必须被更新。

为了清理一切,我们应该对WrkEvent.sys的事件0发出信号,解除对MAPW的封锁。通常情况下,它很快就会再次碰到断点。然后我们可以清除断点并继续执行。

注意,如果我们在解除MAPW的阻塞之前关闭驱动客户端,线程仍然在等待存储在驱动的设备扩展中的KEVENT。这个内存在驱动卸载时被释放,所以MAPW被留在一个无效的内存地址上等待,这将导致系统崩溃。

3.4 将他们全部阻塞

最后一个实验显示了如果我们完全耗尽物理内存,通过保持MAPW和MPW的阻塞会发生什么。我们可以重复上一节的步骤,使用一个大于可用物理内存的地图大小。我们也用前面描述的技术封锁MPW和MAPW。如果我们再写到整个区域,所有的物理内存都会被使用。这将导致系统完全冻结,一段时间后,调试器将控制并打印出以下信息:

第一次实验结果:

[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA80029601C8
[Log]: WrkEvent[1] address: 0XFFFFFA80029601E0
[Log]: WrkEvent[2] address: 0XFFFFFA80029601F8
[Log]: WrkEvent[3] address: 0XFFFFFA8002960210
[Log]: WrkEvent[4] address: 0XFFFFFA8002960228
[Log]: Ev ptr array address: 0XFFFFFA8002960240
[Log]: Gate address 0XFFFFFA8002960268
[Log]: Device closed
kd> g
Without a debugger attached, the following bugcheck would have occurred.
  eb 34b0a 5702 1 c0000054
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03f5255f cc              int     3
kd> g
Without a debugger attached, the following bugcheck would have occurred.
  eb 34b0a 5702 1 c0000054
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03f5255f cc              int     3
kd> g

Untitled 24.png

第二次实验结果:

[Log]: Device opened
[Log]: Object addresses: 
[Log]: WrkEvent[0] address: 0XFFFFFA80029031C8
[Log]: WrkEvent[1] address: 0XFFFFFA80029031E0
[Log]: WrkEvent[2] address: 0XFFFFFA80029031F8
[Log]: WrkEvent[3] address: 0XFFFFFA8002903210
[Log]: WrkEvent[4] address: 0XFFFFFA8002903228
[Log]: Ev ptr array address: 0XFFFFFA8002903240
[Log]: Gate address 0XFFFFFA8002903268
[Log]: Device closed
kd> bp MiMappedPageWriter + 0xa6
kd> bp nt!MiModifiedPageWriter + 0x55
kd> g
kd> r rcx=1
kd> r rdx=0XFFFFFA8002903240
kd> g

Untitled 25.png

Bugcheck 0xeb是DIRTY_MAPPED_PAGES_CONGESTION,我们还可以看到该函数的提示性名称MiNoPagesLastChance:

Without a debugger attached, the following bugcheck would have occurred.
  4d 34ef5 156aa 0 0
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03faa55f cc              int     3
kd> ub
nt!MiNoPagesLastChance+0x15a:
fffff800`03faa53a 4c8d057fa6f7ff  lea     r8,[nt! ?? ::FNODOBFM::`string' (fffff800`03f24bc0)]
fffff800`03faa541 8d4a66          lea     ecx,[rdx+66h]
fffff800`03faa544 448bcf          mov     r9d,edi
fffff800`03faa547 48896c2428      mov     qword ptr [rsp+28h],rbp
fffff800`03faa54c 4889442420      mov     qword ptr [rsp+20h],rax
fffff800`03faa551 e8f29bf7ff      call    nt!DbgPrintEx (fffff800`03f24148)
fffff800`03faa556 f605bbea160008  test    byte ptr [nt!MiFlags (fffff800`04119018)],8
fffff800`03faa55d 7401            je      nt!MiNoPagesLastChance+0x180 (fffff800`03faa560)
kd> !thread @$thread
THREAD fffffa8000d0a040  Cid 0004.0060  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa8000ce20b0       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      20460          Ticks: 4489 (0:00:01:10.028)
Context Switch Count      940            IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.062
Win32 Start Address nt!KeSwapProcessOrStack (0xfffff80003f1cd20)
Stack Init fffff880045bac70 Current fffff880045ba5b0
Base fffff880045bb000 Limit fffff880045b5000 Call 0000000000000000
Priority 23 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`045ba770 fffff800`03faaa34 : fffff880`00000000 fffff800`0000004d 00000000`00000000 00000000`00000000 : nt!MiNoPagesLastChance+0x17f
fffff880`045ba860 fffff800`03e7cc91 : 00000000`00000000 fffff880`045ba940 00000000`00000001 00000000`00000001 : nt!MiWaitForFreePage+0xc4
fffff880`045ba8c0 fffff800`03f1cee0 : 00000000`00000001 fffff880`0455d000 00000000`00000000 fffff6fc`40022ad0 : nt! ?? ::FNODOBFM::`string'+0x47dba
fffff880`045baa20 fffff800`03f1d0d0 : fffffa80`00d02b60 fffff880`00000000 00000000`00000000 00000000`00000000 : nt!MiInPageSingleKernelStack+0x134
fffff880`045bab30 fffff800`03f1d05f : 00000000`00000000 00000000`00000001 fffffa80`00ce20b0 00000000`00000080 : nt!MmInPageKernelStack+0x40
fffff880`045bab90 fffff800`03f1cda4 : 00000000`00000000 00000000`00000000 00000000`00000000 fffffa80`00ce2000 : nt!KiInSwapKernelStacks+0x1f
fffff880`045babc0 fffff800`04185cce : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KeSwapProcessOrStack+0x84
fffff880`045bac00 fffff800`03ed9fe6 : fffff800`0405ae80 fffffa80`00d0a040 fffffa80`00d091a0 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`045bac40 00000000`00000000 : fffff880`045bb000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16
kd> g
Without a debugger attached, the following bugcheck would have occurred.
  4d 34eea 156a9 0 0
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03faa55f cc              int     3
kd> !thread @$thread
THREAD fffffa8002484b60  Cid 0638.0668  Teb: 000007fffffd4000 Win32Thread: fffff900c07936e0 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa800250b670       Image:         vmtoolsd.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      29475          Ticks: 3 (0:00:00:00.046)
Context Switch Count      3406           IdealProcessor: 0                 LargeStack
UserTime                  00:00:01.029
KernelTime                00:00:00.561
Win32 Start Address sechost!ScSvcctrlThreadA (0x000007fefd96a808)
Stack Init fffff880040c5c70 Current fffff880040c5670
Base fffff880040c6000 Limit fffff880040bd000 Call 0000000000000000
Priority 15 BasePriority 15 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`040c5830 fffff800`03faaa34 : 00000000`00000000 fffff800`0000004d 00000000`00000000 00000000`00000000 : nt!MiNoPagesLastChance+0x17f
fffff880`040c5920 fffff800`03e7cc91 : 00000000`00000000 fffff880`040c5a00 00000000`00000000 ffffffff`ffffffff : nt!MiWaitForFreePage+0xc4
fffff880`040c5980 fffff800`03ee676e : 00000000`00000001 00000000`003207d8 00000000`00989601 00000000`00000000 : nt! ?? ::FNODOBFM::`string'+0x47dba
fffff880`040c5ae0 000007fe`f86ff578 : 00000000`f640aeb2 00000000`00000000 00000000`00320730 00000000`00000046 : nt!KiPageFault+0x16e (TrapFrame @ fffff880`040c5ae0)
00000000`017df4b0 00000000`f640aeb2 : 00000000`00000000 00000000`00320730 00000000`00000046 00000000`00375b70 : 0x000007fe`f86ff578
00000000`017df4b8 00000000`00000000 : 00000000`00320730 00000000`00000046 00000000`00375b70 00000000`00000005 : 0xf640aeb2

接下来就是不断的中断:

kd> g
Without a debugger attached, the following bugcheck would have occurred.
  4d 34eea 156a9 0 0
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03faa55f cc              int     3
kd> g
Without a debugger attached, the following bugcheck would have occurred.
  4d 34eea 156a9 0 0
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03faa55f cc              int     3
kd> g
Without a debugger attached, the following bugcheck would have occurred.
  4d 34eea 156a9 0 0
Break instruction exception - code 80000003 (first chance)
nt!MiNoPagesLastChance+0x17f:
fffff800`03faa55f cc              int     3

4. 大页

4.1 为什么有大页以及限制

x64处理器有能力映射所谓的大页面。大页面背后的想法是使用较小部分的VA作为分页结构的索引,并使用更多的位作为页面偏移。偏移位的数量决定了页面的大小,所以增加偏移位意味着拥有更大的页面。为什么要经历这些麻烦呢?因为每个物理页起始地址的转换都被缓存在TLB项中。增加页面大小意味着一个TLB项适用于更大范围的物理内存,从而节省TLB空间。由于TLB相当小,这可以带来更好的性能。当然,就像硬币一样,会有反面,我们很快就会看到。

为了减少用作索引的比特数,取消了一级转换:不再索引四项PML4PDPTPDPT,而只使用前三级表,摆脱了PT。因此用于索引到PT的9位现在是偏移量的一部分,从12位增加到21位,对应于2MB的页面大小。存储在PDE中的物理地址成为物理页本身的地址,偏移量被添加到它上面。

这种虚拟到物理的转换,将一个2MB的虚拟区域映射到一个2MB的物理区域,所以为了能够映射一个大的页面,需要一个很大的物理连续空闲范围。此外,处理器架构要求物理页的起始地址以其大小的倍数对齐,即2MB。因此我们需要一个空闲的物理范围,满足这两个约束。2MB大小并在2MB边界上对齐。由于4kB的小页面更为常见,物理内存可能变得非常零散,以至于无法找到一个合适的范围。当这种情况发生时,内存分配API会返回一个错误,表明没有足够的资源可用(例如,GetLastError()返回ERROR_NO_SYSTEM_RESOURCES或1450)。这是我们为提高TLB使用率所付出的代价。举个例子,在一个1GB的系统上,让系统运行大约30',在分配少至4MB的内存(即2个大的页面)时就会导致这个错误。

大页面是不可分页的,有几个原因。首先,对一个大页进行分页意味着要进行2MB的写入,所以通常的VMM分页活动可能会导致过多的I/O被执行。另一个原因是,当一个大页需要被分页时,可能没有合适的空闲物理范围。这将是一个难以处理的情况:内存分配失败返回错误是一回事,但是一旦内存被分配,VMM不能说 "对不起,我把它换出去了,现在我不知道如何把它取回来"。

鉴于此,进程令牌必须拥有SeLockMemoryPrivilege来分配大页,否则就会出现ERROR_PRIVILEGE_NOT_HELD错误。这个权限通常是禁用的,即使是那些拥有这个权限的账户,所以在尝试分配之前必须通过调用AdjustTokenPrivileges来启用。如果账户本身不持有该权限,则必须首先将该权限授予程序运行所使用的账户。之后,在最后执行分配之前,还必须启用该权限。

上面添加权限的过程,需要注销后重新登陆,这个权限和组策略有关。

https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/enable-the-lock-pages-in-memory-option-windows?redirectedfrom=MSDN&view=sql-server-ver16#CommunityContent

Untitled 26.png

4.2 私有大页

4.2.1 申请

这类页面可以通过调用VirtualAllocEx 或运算上MEM_LARGE_PAGESflAllocationType参数进行组合来分配。
如果我们尝试将flAllocationType设置为MEM_RESERVE | MEM_LARGE_PAGES,而不同时OR 上MEM_COMMITVirtualAllocEx会以ERROR_INVALID_PARAMETER(87)失败。这是因为大页一旦被分配就必须驻留在内存中,所以它们也必须被提交。

当我们在调用VirtualAllocEx的时候,flAllocationType设置为MEM_RESERVE,然后在第二次调用它的时候,flAllocationType = MEM_COMMIT | MEM_LARGE_PAGES,就会发生一个有趣的行为。这是被允许的,因为我们可以调用该函数只预定页面,然后再提交它们。发生的情况是,两次调用都成功了,但我们最终使用的是普通页。接下来的章节将显示分页结构和_MMPFNs是如何为大页设置的,但是,在这种情况下,可以看到它们是为普通的4 kB页面设置的。

原则上要实际分配大页,我们必须将flAllocationType设置为:

MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES

并且进程要启用SeLockMemoryPrivilege

Untitled 27.png

4.2.2 PxEs 和 _MMPFN

分配成功后,该地址的分页结构设置如下:

kd> !pte 800000
                                           VA 0000000000800000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000020    PTE at FFFFF68000004000
contains 02D0000021ABA867  contains 014000002397E867  contains 800000001DA008E7  contains 0000000000000000
pfn 21aba     ---DA--UWEV  pfn 2397e     ---DA--UWEV  pfn 1da00     --LDA--UW-V  LARGE PAGE pfn 1da00

PDE控制位被设置为0x8e7,第7位(PS位)被设置为1,这告诉处理器这个PDE直接映射了一个2MB的页面,!pte扩展对PDE内容进行相应的解码。这是调用VirtualAHocEx后紧接着的分页结构的内容,所以物理内存是由这个函数分配的,而不是像普通的、可分页的4kB页那样,在访问虚拟范围时分配。

PFN数据库对物理内存的每个4 kB范围都有一个项。当一个PDE覆盖2MB范围时,这些项仍然存在,所以它们被设置为有意义的值,尽管它们实际上与物理页无关。一个PDE 涉及512个PFN 项。下面我们可以看到上面的PDE的前两个项:

kd> !pfn 1da00
    PFN 0001DA00 at address FFFFFA800058E000
    flink       FFFFFA8002EA5B30  blink / share count 00000001  pteaddress FFFFF6FB40000022
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000001  containing page 02397E  Active       R     
        ReadInProgress            
kd> !pfn 1da01
    PFN 0001DA01 at address FFFFFA800058E030
    flink       FFFFFA8002EA5B30  blink / share count 00000001  pteaddress FFFFF6FB40000022
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000001  containing page 02397E  Active     M       
    Modified

我们可以注意到以下几点:

  • flink (_MMPFN.u1.Flink) 设置为分配改大页的进程的_EPROCESS地址
  • pteaddress (_MMPFN.PteAddress)PDE地址的第1位置1后的结果。由于PxE地址是8的倍数,并且0-2位是清零的,所以这个位起到了某种标志的作用。
  • restore pte (_MMPFN.u4.OriginalPte) 被设置成1,这个成员通常存储软件PTE,当该页被重新使用时,它将被复制到硬件PTE中。这个页面被锁定,而且从未被重新利用,所以这个成员被用于其他目的。
  • containing page(_MMPFN.u4.PteFrame) 被设置成PDPTEPFN
  • 第一个项有ReadlnProgress_MMPFN.u3.e1.ReadlnProgress)的设置。很明显,这有一些特殊的意义,因为这个页面被锁定了,不会有任何的I/O发生在它身上。

我们在前几节分析的所有页面生命周期并不适用于大页,因为它们是不可分页的。一旦被映射,它们就会一直处于活跃状态直到被释放。那么,VMM是如何知道不对它们进行分页的呢。根据[14]:" 该内存是进程私有字节的一部分,但不是工作集的一部分",所以很可能没有为这些页面创建WS条目,WSM只是对它们一无所知,因为它在WS列表中寻找要修剪的页面。这一点通过_MMPFN.u1被设置为_EPROCESS的地址得到了证实:对于那些在工作集里的页面,这个成员存储了该地址的WSL项的索引。

4.3 Memory Section 大页

4.3.1 用大页分配一个Section Object

除了分配私有大页,我们还可以分配一个由大页组成的可共享section对象。这只允许用于分页文件支持的section,而不是映射文件支持的section。这个限制源于这样一个事实,即这些页面不像私有大页那样可以分页,因此所谓的 "分页文件支持的 "页面实际上并不进入分页文件,而且没有办法将大页的内容从另一个(映射的)文件中转移出来。

为了创建这样一个内存区,我们必须调用CreateFileMapping,将SEC_COMMITSEC_LARGE_PAGES OR 运算到fIProtect。当我们这样做时,我们会看到一个像下面这样的Section对象被创建:

kd> !process 0 0 MemTests.exe
PROCESS fffffa8004687060
    SessionId: 1  Cid: 0a4c    Peb: 7fffffdd000  ParentCid: 0ac4
    DirBase: 690e1000  ObjectTable: fffff8a000fc4720  HandleCount:  32.
    Image: MemTests.exe

kd> .process /i fffffa8004687060
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.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ee0490 cc              int     3
kd> !handle 0x80

PROCESS fffffa8004687060
    SessionId: 1  Cid: 0a4c    Peb: 7fffffdd000  ParentCid: 0ac4
    DirBase: 690e1000  ObjectTable: fffff8a000fc4720  HandleCount:  32.
    Image: MemTests.exe

Handle table at fffff8a000fc4720 with 32 entries in use

0080: Object: fffff8a001844660  GrantedAccess: 000f0007 Entry: fffff8a0018fb200
Object: fffff8a001844660  Type: (fffffa8003d38c90) Section
    ObjectHeader: fffff8a001844630 (new version)
        HandleCount: 1  PointerCount: 2
        Directory Object: fffff8a00609ae20  Name: map

kd> ?? (nt!_SECTION_OBJECT*)0xfffff8a001844660
struct _SECTION_OBJECT * 0xfffff8a0`01844660
   +0x000 StartingVa       : 0xfffffa80`04687060 Void
   +0x008 EndingVa         : 0xfffff8a0`0609ae20 Void
   +0x010 Parent           : 0xfffff880`04ae2901 Void
   +0x018 LeftChild        : (null) 
   +0x020 RightChild       : 0xfffff8a0`0609adb0 Void
   +0x028 Segment          : 0xfffff8a0`0193f000 _SEGMENT_OBJECT
kd> ?? (nt!_MAPPED_FILE_SEGMENT*) 0xfffff8a0`0193f000
struct _MAPPED_FILE_SEGMENT * 0xfffff8a0`0193f000
   +0x000 ControlArea      : 0xfffffa80`047e5010 _CONTROL_AREA
   +0x008 TotalNumberOfPtes : 0x400
   +0x00c SegmentFlags     : _SEGMENT_FLAGS
   +0x010 NumberOfCommittedPages : 0x400
   +0x018 SizeOfSegment    : 0x400000
   +0x020 ExtendInfo       : (null) 
   +0x020 BasedAddress     : (null) 
   +0x028 SegmentLock      : _EX_PUSH_LOCK
kd> !ca 0xfffffa80`047e5010

ControlArea  @ fffffa80047e5010
  Segment      fffff8a00193f000  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                   0  Mapped Views                1
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  0000000000000000  ModWriteCount             0  System Views                0
  WritableRefs                0  PartitionId                0  
  Flags (2000) Commit 

      Pagefile-backed section

Segment @ fffff8a00193f000
  ControlArea       fffffa80047e5010  ExtendInfo    0000000000000000
  Total Ptes                     400
  Segment Size                400000  Committed                  400
  CreatingProcess   fffffa8004687060  FirstMappedVa           c00000
  ProtoPtes         fffff8a00193f048
  Flags (80800) LargePages ProtectionMask 

Subsection 1 @ fffffa80047e5090
  ControlArea  fffffa80047e5010  Starting Sector        0  Number Of Sectors    0
  Base Pte     fffff8a00193f048  Ptes In Subsect      400  Unused Ptes          0
  Flags                       8  Sector Offset          0  Protection           4

这些数据结构在该section的任何视图被映射之前就被转储了。观察这个section的原型PTEs是很有趣的,对于小页面,这些PTEs被初始化为demand-zero。这里的情况是不同的:

kd> dq fffff8a00193f048
fffff8a0`0193f048  80000001`36600867 80000001`36601867
fffff8a0`0193f058  80000001`36602867 80000001`36603867
fffff8a0`0193f068  80000001`36604867 80000001`36605867
fffff8a0`0193f078  80000001`36606867 80000001`36607867
fffff8a0`0193f088  80000001`36608867 80000001`36609867
fffff8a0`0193f098  80000001`3660a867 80000001`3660b867
fffff8a0`0193f0a8  80000001`3660c867 80000001`3660d867
fffff8a0`0193f0b8  80000001`3660e867 80000001`3660f867

每个PTE存储4 kB范围的物理地址:第一个指向0x136600000,第二个指向0x136601000,等等。这些地址,除了第一个,对处理器没有实际意义:正如我们将要看到的,当虚拟到物理映射建立时,PDE被设置为指向0x136600000,处理器架构按照2MB范围的物理内存翻译使用,从那里开始。处理器对地址0x136601000等地址一无所知。

但是,为什么还要创建这些原型PTE呢?既然现在的页面大小是2MB,为什么还要创建一堆逻辑PTE来描述页面的每一个4kB的块呢?我们将很快回答这个问题。

我们可以从上面的转储中看到,所有的原型PTEs都被设置为有效的,P位被设置。如果我们检查_MMPFN实例的物理地址,我们会发现它们都处于Active状态,这告诉我们物理内存是在创建section对象的时候分配的。这与小页的情况不同,物理内存只有在虚拟地址被引用时才会被分配。

kd> !pfn 136601
    PFN 00136601 at address FFFFFA8003A32030
    flink       FFFFFA8004687060  blink / share count 00000001  pteaddress FFFFF8A00193F050
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page FFFFFFFFFFFFF  Active      P      
      Shared

与私有大页的情况一样,除了第一个物理地址外,其他物理地址的_MMPFN 项实际上并不引用处理器意义上的 "页"。就处理器而言,将有一个单一的2MB页面,从与第一个项相关的物理地址开始。然而,VMM使用_MMPFN项来跟踪4 kB块的物理内存的状态,所以它更新了页所涵盖的所有项,以记录这些物理内存块被使用的事实,这是在创建section对象时进行的,而不是在引用映射的视图时进行的。在下文中,我们将使用术语块(block)来指代物理内存的4 kB区域,其状态由_MMPFN实例来跟踪,以避免使用术语page

我们以前看过存储原型PTE的内存页可以被换出,因为它们是分页池的一部分。我们还看到,只有当所有的原型PTEs不指向物理页时,这种情况才会发生,因为,否则,_MMPFNs会通过u4.PteFrame指向原型PTE页。这是通过为每个有效或过渡PTE增加原型PTEs页的共享计数来管理的。根据这个逻辑,存储上述原型PTEs的页面不可能被分页,因为所有存储的原型PTEs都是有效的。然而,如果我们检查原型PTE页的共享计数,我们看到它实际上被设置为1,即有效PTE没有被计算在内。当我们考虑到_MMPFN.u4.PteFrame的值时,原型PTEs所指向的每个块的通常在将页面移动到transition list或重新使用时使用,这不会发生在大页上,因为它们是不可分页的。我们很快就会看到对于大页,_MMPFN.u4.PteFrame实际上并不是指向原型PTE页面。

因此尽管我们仍然要理解为什么首先要创建这些原型PTEs,但我们至少可以得出结论,这些原型PTE是可分页的,它们所消耗的物理内存可以被回收。如果发生这种情况,当新的视图映射section时,它们就必须被带回来,因为它们被用来构成硬件PDE。

让我们分析一下该section的第一个块的_MMPFN

kd> !pfn 136600
    PFN 00136600 at address FFFFFA8003A32000
    flink       FFFFFA8004687060  blink / share count 00000001  pteaddress FFFFF8A00193F048
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page FFFFFFFFFFFFF  Active      PR     
      Shared ReadInProgress
  • flink (_MMPFN.u1.Flink) 存储了创建这个section的进程的_EPROCESS的地址
  • share count_MMPFN.u2.ShareCount) 被设置为1,即使没有视图映射这个section。此外,当一个或者多个视图映射这个section时,这个共享计数仍然是1。这个行为和页面不可分页是一致的。
  • pteaddress (_MMPFN.PteAddress) 指向原型PTE
  • containing page (_MMPFN.u4.PteFrame)设置为-1,并不像我们预期那样指向原型PTE的页面。
  • ReadInProgress (_MMPFN.u3.e1.ReadInProgress) 被设置。如同私有大页的情况。它一定有一些特殊的含义,因为这些页面是不可分页的。

如果我们检查下一个_MMPFN实例:

kd> !pfn 136601
    PFN 00136601 at address FFFFFA8003A32030
    flink       FFFFFA8004687060  blink / share count 00000001  pteaddress FFFFF8A00193F050
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page FFFFFFFFFFFFF  Active      P      
      Shared

我们注意到 pteaddress 指向下一个原型 PTE,证实这个成员的设置方式与私有大页的情况不同。另外,这个 _MMPFN 和后面的都没有设置 ReadlnProgress

4.3.2 无偏移映射视图

当我们不带偏移用视图映射一个section时,我们发现一个大页的PDE如下:

kd> !pte 0xc00000
                                           VA 0000000000c00000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000030    PTE at FFFFF68000006000
contains 02D0000069513867  contains 0120000068896867  contains 80000001366008E7  contains 0000000000000000
pfn 69513     ---DA--UWEV  pfn 68896     ---DA--UWEV  pfn 136600    --LDA--UW-V  LARGE PAGE pfn 136600

这并不特别引人注目,它是一个大页的PDE,就像我们看到的私有内存的PDE一样。同样,PDE被初始化为有效,不需要等待内存被实际访问。原型PTE和_MMPFN没有变化。

如果其他进程映射了额外的视图,效果是一样的:在他们的分页结构中初始化了一个PDE,但是原型PTE_MMPFNs不受影响。

4.3.3 带偏移映射视图

现在我们终于要理解为什么要创建原型PTEs了:需要它们来映射一个偏移量在2MB范围内的视图。

这是Section对象API所允许的:创建一个section,然后映射一个视图,该视图从section的第一个字节开始的一个给定偏移。这个偏移量在Windows x64 上必须是64k的倍数,但这是唯一的要求。因此,我们可以映射一个由大页组成的section视图,从第一个字节的+64k处开始。

在映射地址空间中使用大页PDE是无法获得的:处理器要求存储在PDE中的物理地址必须是2MB的倍数。为了解决这个问题,VMM在映射过程中不使用大页。相反,它建立了一组小页PDEsPTEs,起始虚拟地址指向section地址加上偏移量。

因此,我们必须意识到,通过使用视图的偏移量,我们失去了大页所带来的性能提升。另一个有趣的问题是,根据当前活动的地址空间,同一个物理地址可以以不同的方式被映射:它可以落在一个2MB的页面内,也可以落在一个包含在2MB范围内的4kB页面内。

比较一下没有偏移的视图和有偏移的视图的!vad输出是很有意思的。

kd> !vad 0xc00000
VAD           Level     Start       End Commit
fffffa8005582510  4       c00       fff      0 Mapped  LargePagSec READWRITE          Pagefile section, shared commit 0x400
kd> !vad 0x700000
VAD           Level     Start       End Commit
fffffa80057bcd60  5       700       aef      0 Mapped       READWRITE          Pagefile section, shared commit 0x400

第一个列出来的是没有偏移量的视图,而第二个列表是有64kb偏移量的视图,他是没有LargePageSec标签的,这告诉我们VAD的初始化方式不同。

现在让我们来看看使用偏移量的进程的分页结构:

kd> !pte 0x700000
                                           VA 0000000000700000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000018    PTE at FFFFF68000003800
contains 02C00000628BC867  contains 01200000624BF867  contains 26D0000057712867  contains 0000000000000000
pfn 628bc     ---DA--UWEV  pfn 624bf     ---DA--UWEV  pfn 57712     ---DA--UWEV  not valid

PDE的PS位是清零的,所以需要一个PTE来完成映射,目前PTE被设置为0。 这类似于小页的情况,PTE只有在内存被引用的时候才会被初始化。由于VMM在这里必须创建PTE,所以它采用了通常的懒惰技术,只在实际需要的时候才做这项工作。

上面的PTE是无效的,所以我们不能看到将被映射到VA 0x700000的物理地址。然而,我们可以像VMM那样做,看一下VAD:

kd> dt nt!_MMVAD fffffa80057bcd60
   +0x000 u1               : <unnamed-tag>
   +0x008 LeftChild        : (null) 
   +0x010 RightChild       : (null) 
   +0x018 StartingVpn      : 0x700
   +0x020 EndingVpn        : 0xaef
   +0x028 u                : <unnamed-tag>
   +0x030 PushLock         : _EX_PUSH_LOCK
   +0x038 u5               : <unnamed-tag>
   +0x040 u2               : <unnamed-tag>
   +0x048 Subsection       : 0xfffffa80`04d59360 _SUBSECTION
   +0x048 MappedSubsection : 0xfffffa80`04d59360 _MSUBSECTION
   +0x050 FirstPrototypePte : 0xfffff8a0`01a960c8 _MMPTE
   +0x058 LastContiguousPte : 0xfffff8a0`01a98040 _MMPTE
   +0x060 ViewLinks        : _LIST_ENTRY [ 0xfffffa80`04d59350 - 0xfffffa80`04d59350 ]
   +0x070 VadsProcess      : 0xfffffa80`0581ab31 _EPROCESS
kd> !ca 0xfffffa80`04d592e0

ControlArea  @ fffffa8004d592e0
  Segment      fffff8a001a96000  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                   0  Mapped Views                1
  User Ref                    2  WaitForDel                0  Flush Count                 0
  File Object  0000000000000000  ModWriteCount             0  System Views                0
  WritableRefs                0  PartitionId                0  
  Flags (2000) Commit 

      Pagefile-backed section

Segment @ fffff8a001a96000
  ControlArea       fffffa8004d592e0  ExtendInfo    0000000000000000
  Total Ptes                     400
  Segment Size                400000  Committed                  400
  CreatingProcess   fffffa800581ab30  FirstMappedVa           700000
  ProtoPtes         fffff8a001a96048
  Flags (80800) LargePages ProtectionMask 

Subsection 1 @ fffffa8004d59360
  ControlArea  fffffa8004d592e0  Starting Sector        0  Number Of Sectors    0
  Base Pte     fffff8a001a96048  Ptes In Subsect      400  Unused Ptes          0
  Flags                       8  Sector Offset          0  Protection           4

原型PTE在第396页转储的第一个subsection PTE的+0x80处。由于PTE的大小为8个字节,所以视图的第一个PTE在距离第一个PTE的+0x10处,相应的虚拟地址在+0x10000处,即距离该section的第0字节偏移64kb处,这与我们的偏移量一致。记住一个PTE映射4kb的页。

当视图的第一个字节被访问时,VMM执行这些步骤来检索原型PTE并使用其内容来填充硬件PTE。这就是为4 kB块提供原型PTE的原因:如果我们用偏移量来映射一个视图,并且不得不求助于小页,就需要这些原型PTE

根据这些事实,回顾一下大页的概念是很有用的。当我们创建一个section时,我们并没有创建任何实际的虚拟到物理的映射:没有硬件PxEs被更新。我们只是初始化了一组数据结构,以后将用于创建转换;这可能发生在视图被映射的时候,也可能发生在内存被实际引用的时候。因此,一个section不包括实际的、被处理器使用的映射,不管页面大小如何。一个大页section很容易被映射成大页,因为它的物理地址是2MB的倍数,而且它是由2MB的块组成。顺便注意一下,该section所跨越的物理范围不需要整体连续:2MB的块足以映射大页视图;这样的section将由0x200个连续的4 kB块组成,他的后面会跟着其他这样的区域,但这两个区域可以在不相关的地址上。因此,当VMM成功地分配了足够的物理内存块来满足这些要求时,也就是说,当CreateFileMapping没有因为ERROR_NO_SYSTEM_RESOURCES而失败时,我们就有了一个可以通过大页来映射的section,但也可以通过小页来映射,这取决于它的映射方式,也就是说,我们是否使用偏移。

然而我们必须注意,不要以为这样的section与小页section的区别仅仅在于其物理内存的布局。大页section被初始化为不可分页:原型PTEs总是有效,而且_MMPFN.u4.PteFrame并不指原型PTE页。因此处理器意义上的实际页面并不是由该section单独定义的。

鉴于此,我们可以说,尽管存在原型PTEssection物理内存描述为一组4 kB的块。这些块是否会成为处理器意义上的页,还有待观察。

#### 引用视图

在我们引用视图的第一个字节后,PxEs如下:

kd> !pte 0x700000
                                           VA 0000000000700000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000018    PTE at FFFFF68000003800
contains 02C00000628BC867  contains 01200000624BF867  contains 26D0000057712867  contains A8D0000136010867
pfn 628bc     ---DA--UWEV  pfn 624bf     ---DA--UWEV  pfn 57712     ---DA--UWEV  pfn 136010    ---DA--UW-V

请注意,PTE是有效的,它指向从第1个section块(其物理地址是0x13600000)的+0x10000。这是因为该section的前2MB是连续的,所以视图的虚拟偏移量成为物理偏移量。

同样有趣的是,观察到块的共享计数被增加了,这在使用大页时是不会发生的。这可能是因为映射视图的逻辑与小页section使用的逻辑相同。

kd> !pfn 136010
    PFN 00136010 at address FFFFFA8003A20300
    flink       FFFFFA800581AB30  blink / share count 00000002  pteaddress FFFFF8A001A960C8
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page FFFFFFFFFFFFF  Active      P      
      Shared

如果我们检查被触及的页面之后的PTEs,我们会发现还有7个PTEs是有效的,并且用相应的原型PTE填充。看起来VMM对页面进行了 "预映射",可能与它从分页文件进行预取的原因相同:预测进程的内存需求。这个操作很廉价,因为它只需要更新硬件PTEs

kd> dq FFFFF68000003800
fffff680`00003800  a8d00001`36010867 a8e00001`36011867
fffff680`00003810  a9300001`36012867 a9200001`36013867
fffff680`00003820  a9100001`36014867 a9400001`36015867
fffff680`00003830  9c900001`36016867 a3d00001`36017867
fffff680`00003840  00000000`00000000 00000000`00000000
fffff680`00003850  00000000`00000000 00000000`00000000
fffff680`00003860  00000000`00000000 00000000`00000000
fffff680`00003870  00000000`00000000 00000000`00000000

4.4 大页实验的注意事项

读者可以使用MemTests程序来分配大页,包括VirtualAllocEx()测试和Memory section测试。正如我们已经提到的,运行程序的账户必须持有SeLockMemoryPrivilege,并且该权限必须在进程的token中启用,如果这些条件没有得到满足,分配尝试就会失败,GetLastError返回1314(ERROR_PRIVILEGE_NOT_HELD)。

MemTests的主菜单包括Enable SeLockMemoryPrivilege选项,它的作用就像它所说的。这只适用于正在运行的MemTests实例的令牌:如果我们退出MemTests并再次运行它,我们必须再次启用该权限。

如果账户不持有该权限,则启用该权限会失败,消息是 "Privilege SeLockMemoryPrivilege not assigned. Must add privilege to account."。当这种情况发生时,可以通过添加SeLockMemoryPrivilege特权的菜单选项来添加该特权。我们必须注销并再次登录才能使改变生效,但之后,它是永久性的。

如果大页分配失败,出现错误1450(ERROR_NO_SYSTEM_RESOURCES),VMM无法找到足够的具有正确大小和排列的空闲内存块。当这种情况发生时,重新启动系统把虚拟机内存给多些可能会有帮助。

5. 用户模式栈

当一个线程以用户模式运行时,所使用的堆栈是在进程创建时分配的。它被分配为一个1MB的预留内存区域,只有一小部分是实际提交的,guard页会跟在后面。

以前我们讲过,保护页被VMM以一种特殊的方式处理,因此当它被触及时,不会向进程发出STATUS_GUARD_PAGE_VIOLATION(与普通保护页一样)。相反,VMM将更多的页提交到堆栈区域并恢复错误指令。内核模式栈将在以后进行分析。

6. 用户模式堆

6.1 基本的堆管理特性

Windows提供了分配内存块的功能,这些内存块小于一个页面,并且不需要在64 kB边界上对齐,就像用VirtualAllocEx分配的那样。

这些功能是在堆管理器中实现的,它由用户模式代码(在子系统DLLs和ntdll.dll中)以及内核代码组成。

在内部,堆管理器分配虚拟内存页,并使用自己的逻辑和数据结构来分配这些页内内存块的指针。

一个进程可以有多个堆:每个堆是一个单独的虚拟区域,可以从中分配小块的内存。一个程序通过向内存分配函数传递一个堆句柄来指定分配哪个堆。

每个进程都有一个默认的堆,其句柄由GetProcessHeap返回,可以通过调用HeapCreateHeapDestroy来创建和释放额外的堆。

当创建一个堆时,进程可以指定HEAP_NO_SERIALIZE选项,以禁用堆管理器在超过一个线程调用堆函数时使用的内部序列化。这样,堆函数会更快,但这只有在进程线程在调用它们之前,做到线程同步才是安全的(可以使用这个选项的最简单情况是一个单线程进程)。

一个堆的创建可以有一个最大值,由HeapCreatedwMaximumSize参数指定。这样的堆,被称为固定大小的堆,不能超过最大限度的增长。将dwMaximumSize设置为0可以创建一个只要有虚拟地址空间就可以增长的堆。

为了从堆中分配和释放内存,一个进程调用HeapAllocHeapFree

HeapCreate创建的堆包括通过VirtualAlloc分配的私有内存,其内容只在创建进程中可见。系统可以创建由内存section对象支持的特殊堆,其内容可以在不同的进程之间或者在用户模式和内核模式的组件之间共享。

6.2 低碎片堆

当程序从堆中分配和释放内存时,后者会变得碎片化。即使堆中有足够的空闲内存,内存分配也会失败,因为空闲块很小,而且分散在已使用的内存中。如果程序是多线程的,那么堆也会因内部变量的争用而受到影响,因为对这些变量的访问必须在不同的线程之间同步进行。堆管理器拥有减少这两个问题在低碎片堆(LFH)中的影响的逻辑,它被分层在核心堆管理功能之上。在Windows 7中,LFH被默认使用。

在一些特殊情况下,LFH被关闭,只有核心堆管理器被使用。当用HEAP_NO_SERIALIZE选项创建堆时,当它被创建为一个固定大小的堆时,以及当使用某些堆调试工具时,就会发生这种情况。

LFH用来减少碎片的技术包括将堆内存分成几块,称为粒度(granularity)。每个granularity用于特定范围的内存分配大小:粒度1用于请求1到8字节的分配;粒度2用于9到16字节之间的分配,等等。

根据下表,从33到48的粒度的颗粒度为16,编号更高的粒度的颗粒度更高。

Untitled 28.png

超过16 kB的分配不会从任何granularity中得到满足:LFH被绕过,内存由核心堆管理器逻辑分配。

5.3 安全特性以及!heap命令

堆虚拟内存区域被堆管理器用来存储它自己的内部数据,称为元数据,它用这些数据来管理分配。元数据存在于堆区域中,与管理器分配给调用进程的内存块并列。

有一些恶意程序利用应用程序的漏洞来破坏元数据。通过诱使应用程序在分配的堆块结束后或开始前写入,他们可以改变围绕该块的元数据。这反过来会导致执行不被期望执行的恶意代码。

为了防止这种威胁,堆管理器通过随机化来保护元数据,所以它们没有一个简单的众所周知的布局。由于这个原因,很难用调试器简单的转储来检查堆的内容。最好是使用!heap堆命令扩展,它能够解析元数据。

5.4 调试特性

堆管理器提供了一套调试功能,帮助检测堆内存使用中的错误。

尾部检查:在每个分配的内存块的末尾都有一个签名。如果分配的程序写过了块的末尾,签名就会被覆盖,堆管理器就会报告这个错误。

空闲检查:空闲块被填充了一个已知的特征码,堆管理器在调用时作为其逻辑的一部分进行检查。如果发现特征码被修改,这意味着程序正在使用一个游离的指针(也许是一个被分配后又被释放的块)。

参数检查:对传递给堆函数的参数进行额外检查。

堆验证:每次调用堆函数时都会验证整个堆。

支持堆标记和堆栈追踪:堆分配被标记为标签,分配的调用堆栈被追踪。

这些选项可以通过gflags工具启用,它是Windows调试工具包的一部分。启用这些选项中的任何一个都会导致LFH被禁用(堆分配将只使用核心堆管理功能来执行)。

5.5 页堆

上一节中列出的前三个调试选项可能会在堆损坏发生后很久才检测到,这就很难在代码中找出错误。Pageheap是另一个调试功能,它可以在堆内地址的无效访问发生时捕捉到它。

这个功能的工作原理是为每个堆的分配分配一个单独的虚拟页,并将分配的块放在页的末端。如果分配代码访问的地址超过了块的末尾,就会触发一个访问违规的异常,这是由于下一个页的开头有一个无效的地址造成的。

另外,Pageheap可以保护一个存储着已被分配和释放的块的页面,因此任何在页面内的访问都会导致访问违规。因此,一个有缺陷的程序释放了一个堆块,后来试图访问它,就会被当场抓住。

为这些有用的功能付出的代价是对虚拟地址空间和物理内存的巨大浪费:每一个堆分配都需要自己的页面。由于这个原因,Pageheap是一个调试功能,必须用Gflags工具启用。也可以过滤哪些分配是由Pageheap处理的,例如,基于大小、地址范围和分配调用来自哪个DLL。

免费评分

参与人数 4威望 +1 吾爱币 +22 热心值 +4 收起 理由
w797200 + 1 + 1 用心讨论,共获提升!
HongHu106 + 1 + 1 谢谢@Thanks!
yangfusun + 1 谢谢@Thanks!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

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

 楼主| BeneficialWeb 发表于 2022-6-20 13:53
Hmily 发表于 2022-6-20 11:58
BeneficialWeb请问这个文章是翻译还是自己写的?

这是我在读的一本英文书,自己完成的中译。https://bbs.pediy.com/thread-271287.htm

点评

了解了,辛苦  详情 回复 发表于 2022-6-20 15:04
Hmily 发表于 2022-6-20 15:04
BeneficialWeb 发表于 2022-6-20 13:53
这是我在读的一本英文书,自己完成的中译。https://bbs.pediy.com/thread-271287.htm

了解了,辛苦
Hmily 发表于 2022-6-20 11:58
BeneficialWeb请问这个文章是翻译还是自己写的?
yangfusun 发表于 2022-6-20 19:40
感谢分享  先学学
Leeshinhsun 发表于 2022-6-21 01:10
感谢分享!!!!!!!!!
ningrui 发表于 2022-6-21 07:43
感谢分享感谢分享
worni 发表于 2022-6-22 09:26
有鸡肉 有内容 收走了
abcxyzmn 发表于 2022-6-22 11:05
很专业,虽然不懂,但很钦佩
Veterans 发表于 2022-6-22 12:04
这个帖子有深度,我的水平好像还看不太明白。先学习着吧
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-21 20:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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