吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5652|回复: 11
收起左侧

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

  [复制链接]
BeneficialWeb 发表于 2022-8-25 22:30
本帖最后由 BeneficialWeb 于 2022-8-25 22:38 编辑

系统范围内存管理 Part1

1. 系统范围内存管理概述

1.1 系统区域类型

从本章开始,我们将描述VMM如何管理地址范围的上半部分,即从0xFFFF8000'00000000开始的区域。系统范围从0xFFFFF680'000000开始,这个地址以下的所有地址都没有使用(它们的PML4E被清零)。

顾名思义,这个范围是由系统代码使用的,所以它的页面是受保护的,只能由运行在CPL=0的代码访问,也叫0环代码。

它被划分为一组固定的区域,我们将在接下来的章节中详细介绍,例如,一个区域用于分页池,另一个区域用于加载驱动,等等。

我们可以根据这些区域的使用方式,将其分为两类。这将使我们,至少对于这两类中的一类,能够描述一些适用于多个区域的VMM概念。虽然这种分类对于概述不同区域所共有的一般行为是有用的,但每个区域都有自己的特殊性,这一点将在后面详细说明。

对于第一种区域,我们称之为动态分配区域,VMM实现了分配和释放虚拟地址空间的逻辑,因为它们的内容在系统运行时发生了变化。例如,有一个加载内核模式驱动的区域;当一个新的驱动必须被加载到内存中时,VMM会在这个区域内寻找一个可以加载驱动镜像的空闲区域。之后,驱动程序可以被卸载,从而释放出这个区域。

我们将第二类区域称为特殊用途区域。它们不被用于动态分配区域的分配逻辑所管理,而是专门用来存储内核使用的特定数据结构。一个例子是存储进程工作集和其他数据结构的hyperspace区域。

1.2 动态分配区域的虚拟地址空间管理

VMM必须追踪这些区域的虚拟地址,也就是说,它需要知道哪些地址正在使用,哪些是可用的。对于用户范围的内存,这一信息存储在VADUMAB中;对于系统范围,它被保存在位图中。

一个位图由_RTL_BITMAP的实例组成,其Buffer成员指向一个缓冲区,其中每个位代表一个资源。对于这些区域,每个位代表一个PDE:如果该位被设置,说明该PDE正在使用中,即它所映射的虚拟范围已经被分配用于某种目的。这意味着动态分配的区域是以2MB为单位进行分配的,也就是一个PDE所映射的虚拟大小。

然而当发生页面错误时,这些位图并不用于检查错误地址是否有效。在用户模式范围内,当函数调用发生一个PTE上的错误,比如该PTE被设置为0或完全缺失(即PT没有被映射),VMM会检查VAD。在系统范围内,VMM只查看分页结构,忽略这些位图,以决定是否允许尝试访问。

在下面的章节中,我们将详细介绍用于管理动态分配区域的位图。

通常调用MiObtainSystemVa函数,从这些区域中分配一个块,其中一个参数指定从哪个区域分配。这个参数的可能值由_MI_SYSTEM_VA_TYPE枚举定义:

kd> dt nt!_MI_SYSTEM_VA_TYPE
   MiVaUnused = 0n0
   MiVaSessionSpace = 0n1
   MiVaProcessSpace = 0n2
   MiVaBootLoaded = 0n3
   MiVaPfnDatabase = 0n4
   MiVaNonPagedPool = 0n5
   MiVaPagedPool = 0n6
   MiVaSpecialPoolPaged = 0n7
   MiVaSystemCache = 0n8
   MiVaSystemPtes = 0n9
   MiVaHal = 0n10
   MiVaSessionGlobalSpace = 0n11
   MiVaDriverImages = 0n12
   MiVaSpecialPoolNonPaged = 0n13
   MiVaMaximumType = 0n14

我们将在更详细地讨论不同区域时看到其中一些数值是如何使用的。

1.3 页面错误处理

本节包含了对系统范围内页面错误处理的一般描述。这里详述的处理逻辑对于某些区域有一些例外,这些例外将在特定区域的章节中介绍。

一旦MmAccessFault检测到故障地址在系统范围内,它就会检查错误发生时的代码段寄存器的值(保存在堆栈的_KTRAP_FRAME中)是否有第0位清零。0环的CS是0x10,而3环的CS是0x33,所以这是一种检查试图访问系统地址的代码是否在0环运行的方法。因为MmAccessFault只检查0位, 有趣的是,观察到一个0位清零的CS也可能是一个CPL 2的段,即1位设置,。然而Windows只使用CPL 0和3。

如果保存的CS的第0位被设置,MmAccessFault立即返回STATUS_ACCESS_VIOLATION,因为3环代码不允许访问系统内存。

之后,MmAccessFault会检查所有的分页结构,直到PDE。如果其中任何一个结构的P位清零,那么这个地址就是无效的,系统会进行BugChecks

否则,直到页表的所有分页结构都存在,所以VMM会检查PTE。如果PTE为0,则地址无效,同样,这也会使系统蓝屏。如果它是一个有效的PTE,错误被解决,访问被重新尝试。

例如,PTE可能被设置为0x80,这意味着它是一个_MMPTE_SOFTWARE,有读/写保护,并且在分页文件中不存在它的副本(_MMPTE_SOFTWARE.PageFileLowPageFileHigh为0)。在这种情况下,必须通过映射一个零填充的页来解决该错误,MmAccessFault调用MiDispatchFault来完成这项工作。后者是用于解决用户模式范围内大多数错误的函数。

一般来说,如果错误是针对有效的访问,并且必须被解决,MmAccessFault总是调用MiDispatchFault来完成工作。

如果PTE是用于一个重新使用的页面(_MMPTE_SOFTWARE.PageFileLowPageFileHigh不是0),MiDispatchFault返回一个适当的状态码,MmAccessFault调用MiIssueHardFault来执行页面置入,用户模式范围也是如此。

考虑到VMM如何检查这些区域的分页结构,我们可以理解这些结构本身在系统范围内是不可分页的:如果PML4E、PDPTE或PDE的P位清零,VMM会使系统崩溃;不会尝试从分页文件中置入任何东西。

与用户模式地址的情况一样,大页是不可分页的:PDE可以设置PS位,但这将隐含地锁定映射的内存。

当一个页面故障被解决时,_MMSUPPORT的一个实例为该内存区域提供了指向工作集(_MMWSL)的指针。在用户范围内,每个进程都有自己的工作集,而在系统范围内也有工作集,即_MMSUPPORT_MMWSL的实例,用于不同的区域,这将在后面详述。MmAccessFault确定错误地址所在的区域并选择适当的_MMSUPPORT实例。

1.4 同步

MmAccessFault确定必须使用_MMSUPPORT的哪个实例时,它试图专门锁定存储在_MMSUPPORT.WorkingSetMutex成员。如果推锁已经被锁定,线程就会被阻塞(这些步骤发生在IRQL < DISPATCH,所以线程可以阻塞)。这确保系统中只有一个线程可以处理所选工作集覆盖区域的错误。

在成功锁定_MMSUPPORT实例后,MmAccessFault在当前线程的_ETHREAD中设置了一个位标志,以记录该线程拥有工作集的事实。这些标志位在MmAccessFault的初始步骤中被检查,如果任何一个被设置,该函数就会返回未文档化的NTSTATUS值0xD0000006。因此,如果一个线程在已经拥有一个工作集(包括进程工作集,在解决用户模式范围内的错误时使用)的情况下在系统区域内引起一个页面错误,这个值将被返回。

MmAccessFault在返回前释放推锁并清除标志。

还可以观察到,VMM在不同的阶段设置和清除_MMPFN.PteAddress成员的第0位,例如,在将一个分页池页面从相关的工作集移动到修改列表时。这表明这个位被用来锁定一个实例的独占访问,就像VMM解决用户范围地址的错误时发生的那样。

1.5 无效系统地址 vs 无效用户地址

VMM提供了一个名为MmProbeAndLockPages的函数,它允许驱动程序探测用户范围内的虚拟地址,如果该地址有效,则确保它被映射到物理内存中(它实际上锁定了内存页,正如其名称所暗示的那样)。如果地址是无效的,它就会产生一个访问违规的异常,这个异常可以用_try/__except块来捕获。值得解释的是,为什么无效的系统范围地址不是这样的,也就是说,也就是为什么即使有__try/__except块,触摸一个无效的地址也会使系统崩溃(如果它被用于指向一个无效系统地址的MDL的话,即使是对这个函数的调用也会导致系统崩溃。

MmAccessFault检测到对无效用户地址的引用时,它会向其调用者,即KiPageFault返回一个错误NTSTATUS(通常是0xC0000005)。后者用被中断的线程的状态建立了一个_KTRAP_FRAME,如果MmAccessFault返回一个成功代码,它就会重新执行指令。收到错误代码后,KiPageFault修改了保存的上下文,以便在系统代码中恢复执行,该代码为Windows定义的异常创建数据结构并寻找异常处理程序。因此,如果执行无效访问的代码已经安装了一个带有__try/__except的处理程序,它就会恢复控制。

另一方面,当无效地址在系统范围内时,MmAccessFault的行为是不同的:它调用KeBugCheckEx,从而使系统崩溃。这就是为什么异常处理程序没有机会的原因:没有尝试恢复出错的上下文,因为执行从未从MmAccessFault返回。

2 系统区域

以下关于系统范围的布局来自于一份资料,它是关于这个主题的一个很好的信息来源。

Untitled.png

注意:

  1. MmPagedPoolEnd通常被设置为0xFFFFF8BF'FFFFFFF,从0xFFFFF8C0'000000到0xFFFFF900'000000的范围通常不使用。因此,位于0xFFFFF880'00000000的512GB区域被组织如下:
    • 128G用于系统PTEs
    • 128G用于分页池
    • 256GB未使用

当分析这个布局时,记住一个PML4项映射一个512GB的区域,所以上面的几个区域对应于一个PML4E。接下来的章节将对每个区域进行更详细的分析。

阴影区域都在0xFFFFF700'000000-0xFFFFF77F'FFFFFFF范围内,这通常被称为hyperspace。重要的是要记住,这个范围是进程私有的:每个PML4对这个范围都有不同的项。

2.1 8TB限制

在进一步分析各个区域之前,我们要解释一下为什么从VA空间的顶部开始低于8TB的系统地址会受到限制。有趣的是,由于这些限制,有各种资料称系统范围的大小为8TB,而实际上是9.5TB,如上表所示。如果系统范围是8TB大小,那么它将从0xFFFFF800'000000开始,不包括分页结构、超空间和系统工作集列表。

然而所有动态分配的区域确实都超过了8TB的限制。由于为其他内核模式组件分配虚拟范围的DDI从这些区域返回地址,可以说其他组件 "可用 "的系统范围是8TB大小。这也不完全正确,因为8TB以上的范围包括VMM为自己保留的区域,比如PFN数据库。原理上整个系统范围有9.5TB大小,只有一部分可用于内核模式组件的内存分配。

使高8TB如此特别的是链表。Windows使用_SLIST_HEADER数据结构来存储单向链表的头部。系统建立了各种数据结构的列表,由_SLIST_HEADER的实例指向。由于性能原因,能够用一条处理器指令来比较和修改一个_SLIST_HEADER是很重要的。这样就可以独占更新一个列表的头,即确保系统中没有其他处理器更新它,而不必用锁来保护该结构,但这将降低系统的可扩展性。

英特尔架构定义了CMPXCHG指令,它可以用于这个目的。这条指令将处理器寄存器EAX的值与内存中的一个值进行比较。如果这两个值相等,它就用第二个寄存器的内容替换内存中的值,否则就不影响内存。所以如果我们想专门修改内存中的一个值,我们首先把它加载到EAX中,把更新的值加载到第二个寄存器中(例如EBX),然后执行CMPXCHG。如果其他处理器已经修改了内存内容,那么与EAX的比较就会失败,内存就不会被修改。代码可以通过检查处理器标志来检测这一点,并重试更新。另一方面,如果内存内容在加载到EAXCMPXCHG之间没有变化,它就会用我们的新值进行更新。

在这个简短的概述中,我们使用了32位寄存器的名称,这意味着该指令对4个字节的内存进行操作,但对于更小的和更大的内存来说,它还有一些变化。

在早期的64位处理器上,CMPXCHG最多只能操作8个字节,所以任何要用这个指令更新的数据结构,比如_SLIST_HEADER,都必须适合这个大小。一个_SLIST_HEADER必须存储一个指向第一个列表节点的指针和其他列表信息,但在64位Windows上,仅指针就有8个字节长,没有空间容纳额外的数据。为了解决这个问题,指针被截断了。 _SLIST_HEADER是一个联合体,其声明如下(用WinDbg转储):

kd> dt -b nt!_SLIST_HEADER
   +0x000 Alignment        : Uint8B
   +0x008 Region           : Uint8B
   +0x000 Header8          : <unnamed-tag>
      +0x000 Depth            : Pos 0, 16 Bits
      +0x000 Sequence         : Pos 16, 9 Bits
      +0x000 NextEntry        : Pos 25, 39 Bits
      +0x008 HeaderType       : Pos 0, 1 Bit
      +0x008 Init             : Pos 1, 1 Bit
      +0x008 Reserved         : Pos 2, 59 Bits
      +0x008 Region           : Pos 61, 3 Bits
   +0x000 Header16         : <unnamed-tag>
      +0x000 Depth            : Pos 0, 16 Bits
      +0x000 Sequence         : Pos 16, 48 Bits
      +0x008 HeaderType       : Pos 0, 1 Bit
      +0x008 Init             : Pos 1, 1 Bit
      +0x008 Reserved         : Pos 2, 2 Bits
      +0x008 NextEntry        : Pos 4, 60 Bits
   +0x000 HeaderX64        : <unnamed-tag>
      +0x000 Depth            : Pos 0, 16 Bits
      +0x000 Sequence         : Pos 16, 48 Bits
      +0x008 HeaderType       : Pos 0, 1 Bit
      +0x008 Reserved         : Pos 1, 3 Bits
      +0x008 NextEntry        : Pos 4, 60 Bits

Headers8成员将3个成员DepthSequenceNextEntry挤压成8个字节,通过将指针截断到39位。这个成员还包括8个额外的字节(从+008开始的位域),但是当列表内容发生变化时,例如当一个节点被添加或删除时,它们的内容不需要被更新。这使得只用CMPXCHG在前8个字节上更新列表成为可能。

这样做的代价是将指针截断到39位。然而,这并不意味着节点地址真的只限于39比特。列表节点的地址是16的倍数,所以低4位已知为0,这就给出了43位的指针大小,即地址范围从0到8TB-1

对于用户模式范围内的列表,这不是一个问题,因为范围大小实际上是8TB,这实际上也是用户范围大小的来源。

在系统范围内,43位的地址意味着我们必须假设第43-47位都被设置为1(记住,在这个范围内,第48-63位对于规范地址来说总是1),所以列表节点只能驻留在0xFFFFF800'000000-0xFFFF'FFFFFFF范围内,即范围的高8TB。

这意味着系统范围的初始部分不能完全用于所有目的。区域0xFFFFF680'000000-0xFFFFF7FF'FFFFFFF,这1.5TB的大小不能用于属于链表的数据结构。这不是一个大问题,因为下面的1.5TB范围是固定分配的,用来存储不需要链表的内核数据结构。

这种指针截断的另一个含义是,链表头必须位于链表节点的同一范围内:对于用户区域的链表头,43位被假定为代表用户模式的地址,即43-63位设置为0,而对于系统区域的链表头,43-63位被假定为1。这也意味着一个链表不能包括来自系统和用户模式范围的混合节点,因为每个节点都可以成为列表的第一个,并被_SLIST_HEADER所指向。这些限制都不是一个真正的问题,因为Windows不需要混合两个范围的结构实例:用户模式代码甚至不能访问系统范围,系统代码远离用户模式范围,因为它的内容是不受保护的,有可能被应用程序代码的任何形式的错误行为所破坏。

_SLIST_HEADER的另外两个成员Header16HeaderX64实际上是备用布局,因为这个数据结构是一个联合体,这一点从它们的偏移量为0可以清楚地看出。这两个声明本质上是等价的,只能用于较新的执行CMPXCHG16B指令的处理器,它允许专门修改16字节,因为NextEntry成员现在位于偏移量+8。当这种布局可以使用时,它消除了截断指针的需要,指针的长度变成了60位(我们仍然可以使用16的地址倍数而不使用低4位)。

那么这个数据结构有两个布局定义,但使用哪一个呢?看来这个决定是在运行时做出的。

字节+8的第0位被称为HeaderType,在所有布局中都有声明,它确定了链表头的类型:如果该位是清零的,链表头的内容必须根据Header8的声明来解释,而当它被设置时,备用布局是有效的。

例如,RtlpInterlockedPopEntrySList的代码,它从一个列表中取出一个链表项,测试HeaderType并相应地处理_SLIST_HEADER实例。当HeaderTypeset时执行的指令包括一个CMPXCHG16B,它只能在支持它的处理器上执行,但是,只要HeaderType是clear的,该函数采取不同的代码路径,在_SLIST_HEADER的前8字节上执行CMPXCHG。CMPXCHG1B指令,对于老的处理器来说是未知的,它永远不会被执行,它在代码中的存在不会造成任何伤害:它只是处理器永远不会发现的一小撮字节。因此,_SLIST_HEADER实例的初始化方式决定了在运行时是否需要对CMPXCHG16B支持。

本书分析了VMM使用的几个链表,结果发现这些列表有HeaderType set和60位指针。实际上,这些链表的节点恰好位于系统范围的较高区域(高于0xFFFFFA80'000000),因此它们本来可以用43位指针来处理,但它们还是setHeaderType。这个分析是在一个装有英特尔酷睿i7移动处理器的系统上进行的,该处理器支持CMPXCHG16B,这些发现表明,在这样的系统上,内核代码使用Header16/HeaderX64布局,因为它检测到这是可能的(代码可以通过CPUID指令发现处理器是否支持CMPXCHG16B)。可能,在不同的机器上,用旧的处理器,_SLIST_HEADER将按照Header8的布局来set

这意味着如果处理器支持CMPXCHG16B,使用链表的代码已经可以处理系统范围内任何地址的链表节点(甚至低于当前系统范围的起始地址)。然而,Header8布局的代码也存在,为了兼容旧的处理器。

在接下来的章节中,我们将描述系统范围的各个区域。

2.2 未使用的系统空间

顾名思义,这个区域是不使用的。从运行在CPL 0的代码中触摸这个区域内的地址将导致系统崩溃(当用户模式的代码触摸到系统区域内的任何地址时,会产生STATUS_ACCESS_VIOLATION异常)。

值得注意的是,只有一小部分系统范围被使用:整个范围的大小为128TB,其中未使用的区域为118.5TB,只有9.5TB在使用。

2.3 PTE 空间

2.3.1 描述

这个区域是auto-entry映射分页结构的地方,它是一个特殊用途的区域。

2.3.2 虚拟地址空间管理

这个区域的VA空间有其特殊的生命周期。当VMM更新分页结构时,它的一部分会被映射。

考虑一个地址空间的PML4,其中的auto-entry是唯一有效的。在整个虚拟地址空间中唯一有效的页面是PML4本身在地址0xFFFFF6FB'7DBED000处可见,也就是在这个区域内。

在某些时候,VMM将为虚拟地址空间的第一个512GB(在用户模式范围内)创建PML4项;这个PML4E将指向一个PDPT。一旦PML4E被指向一个物理页并且有效,PDPT的内容就会在PTE空间区域中具体化,在PDPTE范围的开始,即在0xFFFFF6FB'7DA00000-0xFFFFF6FB'7DA00FFF范围内。当VMM将第一个PDPTE指向PD的物理页时,PD内容在0xFFFFF6FB'40000000- 0xFFFFF6FB'40000FFF处变得可见,以此类推。

总之,当VMM填充PxEs来映射VA的部分内容时,分页结构内容就会pop到这个虚拟区域。

2.3.3 页面错误处理

VMM通常不会在这个范围内引起页错误。例如考虑一个用户模式的地址ADDR,其页表已经被重新使用,即其PDE指向分页文件。PT不是通过访问PTE地址从分页文件中检索出来的,从而引发并解决了一个页面错误。相反,VMM在解决ADDR的错误时注意到PDE是无效的,并读回PT。换句话说,它并没有因为触及PTE的地址而产生嵌套的页面错误。

至于系统范围地址,分页结构甚至是不可分页的:如果VMM在解决错误时发现其中任何一个地址丢失,它就认为该地址无效,并使系统崩溃。

看一下这个区域的MmAccessFault的逻辑,似乎证实了页面错误不是一种正常现象。该函数的行为是不同的,取决于分页结构所映射的地址

MmAccessFault检测到错误地址在分页结构区域内时,如果分页项存在的话,它计算出将被映射的VA。考虑到映射的VA和PxE VA之间的关系,即使PxE不存在,这也是可能的。执行转换的代码位于函数内部的+0x5e0处。

如果不存在的PxE所映射的VA在系统范围内,MmAccessFault会以错误检查代码0x50使系统崩溃。

如果映射的VA在用户范围内,错误就会被解决,包含PxE的分页结构被映射到一个物理页上。然而,当发生这种情况的进程终止时,系统以代码0x50进行bugchecks,这意味着VMM的状态被破坏了。这可以用MemTests来验证,我们将在后面的实验看到,可能,MmAccessFault的这个代码路径是在特殊条件下执行的,然而事实是,在这个区域的实际页面错误最终会导致崩溃。

重要的是要记住,只有错误的内核模式组件才能导致这些崩溃,因为从用户模式访问系统范围内的任何地址,包括PTE空间,都会导致用户模式下的线程出现访问违规异常。

用户模式范围的PS页的_MMPFN以前我们就讨论过了。关于系统范围的PS的主要区别似乎是_MMPFN.UsedPageTableEntries总是0。这可能是因为系统范围的PS是不可分页的。这个成员在用户模式范围内用来跟踪使用中的项,这样当它下降到1的时候就可以递减共享计数,PS才可以被取消映射。

2.3.4 区域的工作集

在用户范围内映射地址的分页结构在进程工作集中有一个项。作为一个例子,在一个运行中的系统上,explorer.exe的实例有以下分配的VA范围:

kd> !vad fffffa8005190f70
VAD           Level     Start       End Commit
fffffa8005434eb0  7        10        1f      0 Mapped       READWRITE          Pagefile section, shared commit 0x10
fffffa8003dcabc0  8        20        21      0 Mapped       READONLY           Pagefile section, shared commit 0x2
fffffa800530d4a0  6        30        33      0 Mapped       READONLY           Pagefile section, shared commit 0x4
fffffa80046d5320  7        40        41      0 Mapped       READONLY           Pagefile section, shared commit 0x2
fffffa80055d33f0  5        50        cf     22 Private      READWRITE          
fffffa80050ec860  7        d0        d0      1 Private      READWRITE          
fffffa8005434bd0  6        e0       146      0 Mapped       READONLY           \Windows\System32\locale.nls
...

首个范围第一页面的分页结构为:

kd> !pte 10000
                                           VA 0000000000010000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000000    PTE at FFFFF68000000080
contains 02C00000628BC867  contains 01200000624BF867  contains 069000004F140867  contains 99B00000628E1867
pfn 628bc     ---DA--UWEV  pfn 624bf     ---DA--UWEV  pfn 4f140     ---DA--UWEV  pfn 628e1     ---DA--UW-V

页表的PFN是0x4f140,其内容如下:

kd> !pfn 4f140 
    PFN 0004F140 at address FFFFFA8000ED3C00
    flink       00000069  blink / share count 0000003B  pteaddress FFFFF6FB40000000
    reference count 0001    used entry count  003A      Cached    color 0   Priority 5
    restore pte 00000080  containing page 0624BF  Active     M       
    Modified

在工作集列表中被跟踪的页面的flink成员存储了该页面的工作集项的索引,所以我们看一下进程工作集列表项0x69:

kd> ?? ((nt!_MMWSL*)0xfffff70001080000)->Wsle[0x69].u1
union <unnamed-tag>
   +0x000 VirtualAddress   : 0xfffff680`00000009 Void
   +0x000 Long             : 0xfffff680`00000009
   +0x000 e1               : _MMWSLENTRY
   +0x000 e2               : _MMWSLE_FREE_ENTRY

该项为VA 0xFFFFF680'00000000(WS列表项总是跟踪虚拟页号,即页面对齐的虚拟地址,最右边的三位数字等于0;最右边的数字的值9是由编码到它的标志位造成的)。如果我们再看一下!pte的输出,我们可以确认PTE在地址0xFFFFF680'000080处,因此包含它的虚拟页在0xFFFFF680'000000处,即存储在WS列表项中的地址。换句话说,进程WS列表中的这个项是针对页表的。我们可以为PDPDPT找到类似的结果。

另一方面,映射系统范围地址的分页结构似乎没有被追踪到任何工作集,它们的flink成员往往是0。这不应该让我们感到惊讶,因为这些PS是不可分页的。这个规则的一个例外是映射工作集列表本身的VA的分页结构(0XFFFFF700'01080000):

  • PT, 他的VA通常是工作集链表的第三项
  • PD,链表的第一项
  • PDPT, 链表的第一项
  • PML4 自身是链表的第0项

2.4 Hyperspace

这个特殊用途的区域是进程私有的,并且包含整个PML4项,所以每个进程在它自己的PML4中有一个不同的项。它被用来管理每个进程的信息,如工作集列表。

接下来的章节将描述每个hyperspace子区域。

2.4.1 UMAB 子区域

这个16MB的范围是为UMAB保留的。

这个区域的页面错误处理方式类似于用户范围内的错误,如果一个物理页面最终被映射出来,一个项将被添加到进程工作集中。通常情况下,一个进程在这个区域的第一页有一个有效的PTE,而在它之后的所有PTE都被设置为0。 如果一个页面错误发生在一个由zeored PTE映射的地址上,MmAccessFault调用MiCheckVirtualAddress,它确定该地址是无效的,因此该错误会引发。这个行为可以用MemTests来验证

另一方面,如果发生错误的PTE不是0,那么错误可以被处理并映射出一个页面。举例来说,如果PTE被设置为0x80,即committed的读/写页的值,那么错误会导致一个物理页被映射并添加到进程工作集中。这种行为可以通过用WinDbg设置PTE(例如页面0xFFFFF700'00001000)为0x80,并用MemTests 访问页面来验证。

2.4.2 UMPB 子区域

这512kB范围内存储的是UMPB。

在这个范围内的页面错误的处理方式与UMAB区域的很相似。这个区域的无效PTE都被设置为0x80(UMAB的PTE被设置为0),所以页面错误不会导致异常或崩溃,并通过映射一个物理页面和添加一个项到进程WS中来解决。这种行为也可以用MemTests来验证。

2.4.3 WSL 子区域

#### 区域内容

_MMWSL.LastlnitializedWsle是有效VA范围的最后一项的索引(即最后一个映射页的最后8个字节)。我们可以观察到,当MemTests分配并触及一个512MB的范围,从而增加其工作集列表时,映射的范围是如何变化的。

kd> !process 0 0 MemTests.exe
PROCESS fffffa80053f9b30
    SessionId: 1  Cid: 0234    Peb: 7fffffd4000  ParentCid: 0ac4
    DirBase: 69413000  ObjectTable: fffff8a000fc4720  HandleCount:  11.
    Image: MemTests.exe

kd> .process /i fffffa80053f9b30
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> dt nt!_mmwsl 0xfffff700`01080000
   +0x000 FirstFree        : 0x239
   +0x004 FirstDynamic     : 5
   +0x008 LastEntry        : 0x2d9
   +0x00c NextSlot         : 5
   +0x010 Wsle             : 0xfffff700`01080488 _MMWSLE
   +0x018 LowestPagableAddress : (null) 
   +0x020 LastInitializedWsle : 0x36e
   +0x024 NextAgingSlot    : 0
   +0x028 NumberOfCommittedPageTables : 0x1a
   +0x02c VadBitMapHint    : 3
   +0x030 NonDirectCount   : 0
   +0x034 LastVadBit       : 0x7fff
   +0x038 MaximumLastVadBit : 0x7fffffd
   +0x03c LastAllocationSizeHint : 0x39
   +0x040 LastAllocationSize : 0x10
   +0x048 NonDirectHash    : (null) 
   +0x050 HashTableStart   : 0xfffff704`40001000 _MMWSLE_HASH
   +0x058 HighestPermittedHashAddress : 0xfffff706`41004fe4 _MMWSLE_HASH
   +0x060 MaximumUserPageTablePages : 0x400000
   +0x064 MaximumUserPageDirectoryPages : 0x2000
   +0x068 CommittedPageTables : 0xfffff700`01000000  -> 7
   +0x070 NumberOfCommittedPageDirectories : 5
   +0x078 CommittedPageDirectories : [128] 0x13
   +0x478 NumberOfCommittedPageDirectoryParents : 2
   +0x480 CommittedPageDirectoryParents : [1] 0x8001

在申请内存之前,_MMWSL.LastInitializedWsle 被设置为0x36e。因为WSLEs是8字节大小的,那么:

0xFFFFF700`01080488 +0x36E * 8 = 0xFFFFF700'01081FF8

映射这个VA的PTE如下:

kd> !pte 0xFFFFF700`01081FF8
                                           VA fffff70001081ff8
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE000    PDE at FFFFF6FB7DC00040    PTE at FFFFF6FB80008408
contains 0000000068B94863  contains 0000000068F55863  contains 00000000687D6863  contains 8000000051F7B863
pfn 68b94     ---DA--KWEV  pfn 68f55     ---DA--KWEV  pfn 687d6     ---DA--KWEV  pfn 51f7b     ---DA--KW-V

映射第一项的PTE的地址如下:

kd> !pte 0xfffff700`01080488
                                           VA fffff70001080488
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE000    PDE at FFFFF6FB7DC00040    PTE at FFFFF6FB80008400
contains 0000000068B94863  contains 0000000068F55863  contains 00000000687D6863  contains 0000000068E17863
pfn 68b94     ---DA--KWEV  pfn 68f55     ---DA--KWEV  pfn 687d6     ---DA--KWEV  pfn 68e17     ---DA--KWEV

也就是说,这2个PTEs是一前一后的。我们可以检查PTE范围的内容,在最后一个项的内容之外延伸一点(0x10 PTEs)。

kd> dq FFFFF6FB80008400 FFFFF6FB80008408+0x10*8
fffff6fb`80008400  00000000`68e17863 80000000`51f7b863
fffff6fb`80008410  00000000`00000000 00000000`00000000
fffff6fb`80008420  00000000`00000000 00000000`00000000
fffff6fb`80008430  00000000`00000000 00000000`00000000
fffff6fb`80008440  00000000`00000000 00000000`00000000
fffff6fb`80008450  00000000`00000000 00000000`00000000
fffff6fb`80008460  00000000`00000000 00000000`00000000
fffff6fb`80008470  00000000`00000000 00000000`00000000
fffff6fb`80008480  00000000`00000000 00000000`00000000

正如我们所看到的,最后一个项以外的PTEs都被设置为0,所以WSLE现在只消耗了2个物理页。

在MemTests访问512MB区域后进行同样的分析是很有意思的。这个测试是在一个有4GB内存的虚拟机上进行的,所以VMM让MemTests的工作集增长。下面是_MMWSL.LastlnitializedWsleWSLE地址和PTE地址的值:

kd> .process /i fffffa80053f9b30
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> ?? ((nt!_mmwsl *) 0xfffff700`01080000)->LastInitializedWsle
unsigned long 0x2036e
kd> ? 0xfffff700`01080488+0x2036e*8
Evaluate expression: -9895586291720 = fffff700`01181ff8
kd> !pte fffff700`01181ff8
                                           VA fffff70001181ff8
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE000    PDE at FFFFF6FB7DC00040    PTE at FFFFF6FB80008C08
contains 0000000068B94863  contains 0000000068F55863  contains 00000000687D6863  contains 800000003195C863
pfn 68b94     ---DA--KWEV  pfn 68f55     ---DA--KWEV  pfn 687d6     ---DA--KWEV  pfn 3195c     ---DA--KW-V

有效PTE的数量为:

kd> ? (FFFFF6FB80008C08+8-FFFFF6FB80008400)/8
Evaluate expression: 258 = 00000000`00000102

0x102或258个PTEs,因此WSL消耗了258个物理页(约1MB)。如果我们转储PTE的内容,我们可以确认它们与预期的一样(只显示了一个摘录)。

kd> dq FFFFF6FB80008400 FFFFF6FB80008C08 + 0x10*8
fffff6fb`80008400  00000000`68e17863 80000000`51f7b863
fffff6fb`80008410  80000000`8b2dd863 80000000`528de863
[...]
fffff6fb`80008bf0  80000000`31ed9863 80000000`3051a863
fffff6fb`80008c00  80000000`31b1b863 80000000`3195c863
fffff6fb`80008c10  00000000`00000000 00000000`00000000
fffff6fb`80008c20  00000000`00000000 00000000`00000000
fffff6fb`80008c30  00000000`00000000 00000000`00000000
fffff6fb`80008c40  00000000`00000000 00000000`00000000
fffff6fb`80008c50  00000000`00000000 00000000`00000000
fffff6fb`80008c60  00000000`00000000 00000000`00000000
fffff6fb`80008c70  00000000`00000000 00000000`00000000
fffff6fb`80008c80  00000000`00000000 00000000`00000000

WSL的物理页似乎并不是WSL本身的一部分。如果我们检查它们的_MMPFNs,我们会发现_MMPFN.u1.WsIndex通常是0。 作为一个例子,上面的倒数第二个PTE映射PFN 0x31b1b,它有:

kd> !pfn 0x31b1b
    PFN 00031B1B at address FFFFFA8000951510
    flink       00000000  blink / share count 00000001  pteaddress FFFFF6FB80008C00
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 0687D6  Active     M       
    Modified

映射WSL本身的页面(0xFFFFF700'08010000)是例外,它总是WSLE的第4项。

kd> !pte 0xfffff700`01080000
                                           VA fffff70001080000
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE000    PDE at FFFFF6FB7DC00040    PTE at FFFFF6FB80008400
contains 0000000068B94863  contains 0000000068F55863  contains 00000000687D6863  contains 0000000068E17863
pfn 68b94     ---DA--KWEV  pfn 68f55     ---DA--KWEV  pfn 687d6     ---DA--KWEV  pfn 68e17     ---DA--KWEV

kd> dq 0xfffff700`01080488
fffff700`01080488  fffff6fb`7dbed009 fffff6fb`7dbee009
fffff700`01080498  fffff6fb`7dc00049 fffff6fb`80008409
fffff700`010804a8  fffff700`01080009 fffff700`00000009
fffff700`010804b8  fffff700`01000009 fffff700`0107f009
fffff700`010804c8  fffff6fb`7da0f009 fffff6fb`41fff009
fffff700`010804d8  fffff683`fffff009 000007ff`fffa1015
fffff700`010804e8  000007ff`fffa2015 000007ff`fffa3015
fffff700`010804f8  000007ff`fffa4015 000007ff`fffa5015

当工作集缩小时,VMM会释放物理页,WSLE可以被缩短。下面是MemTests调用SetProcessWorkingSetSize来缩减自己的WS后的情况:

kd> .process /i fffffa80053f9b30
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> ?? ((nt!_mmwsl *) 0xfffff700`01080000)->LastInitializedWsle
unsigned long 0x36e
kd> dq FFFFF6FB80008400 FFFFF6FB80008408 + 0x10*8
fffff6fb`80008400  00000000`68e17863 80000000`3034b863
fffff6fb`80008410  00000000`00000000 00000000`00000000
fffff6fb`80008420  00000000`00000000 00000000`00000000
fffff6fb`80008430  00000000`00000000 00000000`00000000
fffff6fb`80008440  00000000`00000000 00000000`00000000
fffff6fb`80008450  00000000`00000000 00000000`00000000
fffff6fb`80008460  00000000`00000000 00000000`00000000
fffff6fb`80008470  00000000`00000000 00000000`00000000
fffff6fb`80008480  00000000`00000000 00000000`00000000

我们又回到了只有两个页的链表。

#### 页面错误处理

MmAccessFault的分析表明,该区域的页面错误由用于系统PTE区域的相同代码路径处理。

这意味着VMM防止页面错误在这个区域发生,因为如果PTE的物理页被映射了,这个地址的项就会被添加到系统PTE工作集,然而这个WS只有一个实例,这个区域的内容又是进程私有的。一个给定的WSLE地址错误可能在不同的进程中发生不止一次,因此同一地址的多个WSL项将被添加到系统PTE WS中,这样处理的话没有任何意义。因此VMM需要在任何错误发生之前增长WSLE,并映射物理页面,避免该区域的页面错误。

这个事实也证实了这一点,这个区域的物理页的_MMPFNs(除了0xFFFF700'01080000页的那个)的_MMPFN.u1.WsIndex=0。如果一个页面被映射为处理错误的一部分,它将有一个与系统PTE WS项相对应的索引。这可以通过为这个区域设置一个zeroed PTE为0x80,然后用MemTests触摸这个范围来验证。这个实验会破坏系统状态,因为这样的WS项是不存在的,所以事后应该重启系统。

2.4.4 Hash 表区域

接下来的两个子区域存储2个工作集哈希表:非直接哈希表和直接哈希表。我们以前已经分析了这两个表的虚拟范围是如何在需要哈希表项时被映射的。

这个区域中的页面错误原因与WSL区域中的错误处理方式相同。

2.5 共享系统页面

这个特殊用途的页面区域用于在用户模式下执行的代码和内核模式下执行的代码之间共享内容。它的布局是由_KUSER_SHARED_DATA结构给出。

PTE映射地址0xFFFFF780'000000禁止从用户模式代码中访问,因为它的U/S位是清零的,然而,每个进程都将这个PTE指向的相同物理页映射到地址0x7FFE0000,这个映射可以从用户模式中访问,尽管是只读的(PTER/W位清零)。

这个特殊的页面实际上在VAD树中有一个项,他有点特殊:

0: kd> !vad fffffa8032c17420
VAD             Level         Start             End              Commit
fffffa8032b3cb90  5              10              1f               0 Mapped       READWRITE          Pagefile section, shared commit 0x10
[...]
fffffa8032c17420  0           7ffe0           7ffef 2251799813685247 Private      READONLY

另外,物理页的_MMPFN_MMPFN.u1设置为KiInitialProcess的地址,这是内核内_EPROCESS的一个静态实例,用于逻辑上的进程Idle,空闲线程(每个处理器一个)属于这个进程的。

2.6 系统工作集

系统WS与进程WS类似,但是对于系统范围来说,有几个区域像用户模式范围一样是可分页的。为了追踪WS的大小和当前映射的VAVMM使用一组_MMSUPPORT实例,每个实例都指向_MMWSL的一个实例。_MMSUPPORT实例是内核镜像中的静态变量,它们的VmWorkingSetList成员指向这个区域中的_MMWSL实例,这是一个特殊用途的实例,专门用于这些数据结构。

如果我们分析名为MiSwapWslEntries的函数,我们会发现它将一个指向_MMSUPPORT实例的指针与两个静态变量MmPagedPoolWsMmSystemPtesWs的地址进行比较,对它们采取的行动与对描述进程用户范围的_MMSUPPORT的 "常规 "实例不同。这表明MmPagedPoolWsMmSystemPtesWs_MMSUPPORT的实例,其内容和MmAccessFault的代码都证实了这一事实。通过一点猜测,我们可以在内核中寻找所有以Ws结尾的符号,得出以下列表:

0: kd> x nt!*ws
...
fffff800`046b3518 nt!MiSessionSpaceWs = <no type information>
...
fffff800`04606ec0 nt!MmSystemPtesWs = <no type information>
...
fffff800`04606dc0 nt!MmSystemCacheWs = <no type information>
fffff800`04606bc0 nt!MmPagedPoolWs = <no type information>

现在,我们将忽略MiSessionSpaceWs,直到我们讨论会话私有内存;其他三个变量看起来实际上是_MMSUPPORT的实例,每个变量都指向不同的_MMWSL

0: kd> dt nt!_mmsupport VmWorkingSetList @@masm(nt!MmSystemPtesWs)
   +0x068 VmWorkingSetList : 0xfffff780`00001000 _MMWSL
0: kd> dt nt!_mmsupport VmWorkingSetList @@masm(nt!MmSystemCacheWs)
   +0x068 VmWorkingSetList : 0xfffff780`c0000000 _MMWSL
0: kd> dt nt!_mmsupport VmWorkingSetList @@masm(nt!MmPagedPoolWs)
   +0x068 VmWorkingSetList : 0xfffff781`c0000000 _MMWSL

这些工作集似乎是由以下值来识别的

_MMSUPPORT.Flags.WorkingSetType:

0: kd> dt nt!_mmsupport Flags.WorkingSetType @@masm(nt!MmSystemCacheWs)
   +0x084 Flags                : 
      +0x000 WorkingSetType       : 0y010  即 2
0: kd> dt nt!_mmsupport Flags.WorkingSetType @@masm(nt!MmPagedPoolWs)
   +0x084 Flags                : 
      +0x000 WorkingSetType       : 0y011  即 3
0: kd> dt nt!_mmsupport Flags.WorkingSetType @@masm(nt!MmSystemPtesWs)
   +0x084 Flags                : 
      +0x000 WorkingSetType       : 0y100  即 4

MiMapProcessMetaPage函数的分析证实了这一点:它包括一个逻辑块,使用指向_MMSUPPORT实例的指针并释放其WorkingSetMutex成员,这是一个保护该实例的推锁。释放后,该函数清除了当前线程_ETHREAD中的一组标志之一,这取决于Flags.WorkingSetType的值。被清除的标志和WorkingSetType之间的关系由下表中给出:

Untitled 1.png

这些标志名称表明,1是会话工作集的类型,以此类推。标志名称所暗示的类型与从_MMSUPPORT实例中提取的值相符。此外,设置和清除这些标志的逻辑在MmAccessFault中也可以找到。

所有这些_MMSUPPORT的实例和它们相关的_MMWSL以与进程WSL相同的方式(通过存储WS的计数器和映射的VA列表,以及它们的年龄)追踪系统VA的范围:

2.6.1 系统缓存工作集

系统缓存工作集(MmSystemCacheWs)记录了Windows文件缓存所使用的物理页,因此MmSystemCacheWs.WorkingSetSize给出了它所使用的物理页的数量。

这些页面被映射在动态内核VA空间区域的地址上,其内容将在后面进行分析。现在,只需要知道这个区域包括可分页的地址就足够了,这些地址是由这个工作集跟踪的。

在Windows 7中,MmSystemCachePage不再存在,性能计数显示的值与从MmSystemCacheWs中提取的值完全相同(尽管是以字节而不是以页为单位)。在 Windows 7 中,这些计数与从 MmPagedPoolWsMmSystemPtesWs 中提取的值完全一致。总之,在Windows 7中,三种系统工作集是由_MMSUPPORT的三个不同实例管理的。在Windows 7中,Cache Bytes counterSystem Cache Resident Bytes counter这两个计数器有相同的值,它们都等于MmSystemCacheWs.WorkingSetSize

下面是这个工作集的_MMWSL实例的转储:

1: kd> ?? ((nt!_MMSUPPORT*) @@(nt!MmSystemCacheWs))->VmWorkingSetList
struct _MMWSL * 0xfffff780`c0000000
   +0x000 FirstFree        : 0x2990
   +0x004 FirstDynamic     : 1
   +0x008 LastEntry        : 0x2d97
   +0x00c NextSlot         : 1
   +0x010 Wsle             : 0xfffff780`c0000488 _MMWSLE
   +0x018 LowestPagableAddress : 0xfffff980`00000000 Void
   +0x020 LastInitializedWsle : 0x2f6e
   +0x024 NextAgingSlot    : 0x1902
   +0x028 NumberOfCommittedPageTables : 0
   +0x02c VadBitMapHint    : 0
   +0x030 NonDirectCount   : 0x55
   +0x034 LastVadBit       : 0
   +0x038 MaximumLastVadBit : 0
   +0x03c LastAllocationSizeHint : 0
   +0x040 LastAllocationSize : 0
   +0x048 NonDirectHash    : 0xfffff781`7ffff000 _MMWSLE_NONDIRECT_HASH
   +0x050 HashTableStart   : 0xfffff781`80000000 _MMWSLE_HASH
   +0x058 HighestPermittedHashAddress : 0xfffff781`c0000000 _MMWSLE_HASH
   +0x060 MaximumUserPageTablePages : 0
   +0x064 MaximumUserPageDirectoryPages : 0
   +0x068 CommittedPageTables : (null) 
   +0x070 NumberOfCommittedPageDirectories : 0
   +0x078 CommittedPageDirectories : [128] 0
   +0x478 NumberOfCommittedPageDirectoryParents : 0
   +0x480 CommittedPageDirectoryParents : [1] 0

转储显示_MMWSL实例在系统工作集区域,WSLE数组紧随其后。

LowestPagableAddress被设置为动态内核VA空间区域的起始地址,该WS跟踪该范围内的VA

NonDirectCount 不为0。

对于一个属于进程工作集的页面,_MMPNF.u1.WsIndex存储了映射到该页的VA的工作集项的索引。当多个进程共享一个物理内存页时,VMM不能总是在所有工作集中使用相同的WSLE,因为在某些WS中,该项可能正在使用。对于那些存储在_MMPNF中的索引不正确的WSsVMM创建一个哈希表项,将VA与正确的索引联系起来。

上述所有情况都源于一个基本事实:映射用户范围地址的页面可以在不同的工作集之间共享。

然而系统缓存工作集跟踪的是系统范围的地址(特别是在动态内核VA区域),我们希望映射这些地址的物理页不属于任何其他工作集。其他的系统工作集,比如系统PTE工作集、非分页池工作集和会话工作集,都是针对其他系统区域的,与系统缓存工作集的VA没有关系。如果缓存工作集中的VA被只能是该WS的一部分页面映射,那么就不需要哈希表项了。_MMPFN.u1.WsIndex可以简单地存储高速缓存工作集的正确索引。

在解释一下内核是如何使用section对象来访问文件的:一个文件总是通过创建一个section对象和对其进行映射来访问。如果一个程序使用像ReadFile这样的文件访问API,内核就会在幕后创建section对象,并与文件系统驱动一起,将section中的数据复制到程序缓冲区中。但是,这个section的视图是在哪里映射的呢?在缓存管理器使用的VA区域中,它位于动态内核VA空间区域内。所以,基本上,当ReadFile返回数据时,内核模式的代码是从分配给缓存管理器的某个VA中读取的,而这个VA中的文件section的视图是被映射的。系统缓存WS跟踪这些VA,也就是说,它的项是针对它们的。

到目前为止我们知道缓存WS中的VA是用来做什么的,但我们仍然不知道为什么它们需要哈希表项。

无论我们如何访问一个文件,都会创建一个唯一的section对象:如果我们使用常规的I/O函数,它由I/O管理器创建;如果我们使用文件映射函数,我们明确地创建它;如果我们做这两件事,同一个section对象被用于两种目的。在整个系统中,对于一个给定的文件来说,只有一个section对象,无论为哪种访问方式创建。唯一的例外是当我们为一个镜像文件创建一个文件映射时:它可以被映射为一个数据文件和一个镜像文件,并创建两个不同的section。然而,对于一个给定的访问模式来说,仍然有一个唯一的section

有一个唯一的section意味着缓存:如果该section部分在物理内存页中,这部分对所有映射该section视图的进程都是可用的。现在,假设我们有一个文件,在一个进程中用ReadFile访问,在另一个进程中用文件映射API访问。缓存管理器在物理页中加载了文件的部分内容,并将其映射到自己的虚拟范围中。映射文件的进程将相同的物理页映射到自己的地址范围。这些页面现在是两个工作集的一部分:系统高速缓存WS和进程WS。这就是为什么可能需要哈希表项。

我们现在要在一个调试会话中观察上述情况。下面是缓存WS的非直接哈希表的部分转储:

1: kd> dq 0xfffff781`7ffff000
fffff781`7ffff000  fffff980`02b00001 00000000`000001a8
fffff781`7ffff010  00000000`00000000 00000000`00000911
fffff781`7ffff020  00000000`00000000 00000000`00000a47
fffff781`7ffff030  fffff980`03503001 00000000`00000422
fffff781`7ffff040  00000000`00000000 00000000`00000b0c
fffff781`7ffff050  00000000`00000000 00000000`00000e9d
fffff781`7ffff060  fffff980`01706001 00000000`00002b93
fffff781`7ffff070  fffff980`01307001 00000000`000000a5

第四行的项显示VA 0xfffff98003503001(最右边的1是由于一个标志位)在**WS**列表中的索引为0x422(记住这些项的类型是_MMWSLE_NONDIRECT_HASH`)。

0: kd> dt _MMWSLE_NONDIRECT_HASH
nt!_MMWSLE_NONDIRECT_HASH
   +0x000 Key              : Ptr64 Void
   +0x008 Index            : Uint4B

我们可以通过检查系统缓存WS中的WSL项来确认这一点:

1: kd> ?? ((nt!_mmwsl*)0xfffff780`c0000000)->Wsle[0x422].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y1
   +0x000 Direct           : 0y0
   +0x000 Protection       : 0y00000 (0)
   +0x000 Age              : 0y001
   +0x000 VirtualPageNumber : 0y1111111111111111111110011000000000000011010100000011 (0xfffff98003503)

VirtualPageNumber成员存储了我们地址的VPN。通过!pte!pfn我们可以查看_MMPFN.u1.Wslndex

1: kd> !pte fffff980`03503000
                                           VA fffff98003503000
PXE at FFFFF6FB7DBEDF98    PPE at FFFFF6FB7DBF3000    PDE at FFFFF6FB7E6000D0    PTE at FFFFF6FCC001A818
contains 00000000A90A2863  contains 00000000A90A1863  contains 000000009FDE1863  contains 422000009BBFB901
pfn a90a2     ---DA--KWEV  pfn a90a1     ---DA--KWEV  pfn 9fde1     ---DA--KWEV  pfn 9bbfb     -G-----KREV

1: kd> !pfn 9bbfb
    PFN 0009BBFB at address FFFFFA8001D33F10
    flink       0000045D  blink / share count 00000001  pteaddress FFFFF8A0003C7218
    reference count 0001    used entry count  0000      Cached    color 0   Priority 5
    restore pte FA8031640D8004C0  containing page 0A3F2C  Active      P      
     Shared

很明显,_MMFPN是一个错误的索引值 0x37F。

现在,由于这个页面是在缓存WS中,它是一个section的一部分。我们知道,上面显示的restore pte是_MMFPN.OriginalPte,对于这样的页面,它是_MMPTE_SUBSECTION的一个实例指向subsection。我们可以用它来得到文件的控制区:

1: kd> ?? ((nt!_mmpfn*) 0xFFFFFA8001D33F10)->OriginalPte.u.Subsect
struct _MMPTE_SUBSECTION
   +0x000 Valid            : 0y0
   +0x000 Unused0          : 0y0000
   +0x000 Protection       : 0y00110 (0x6)
   +0x000 Prototype        : 0y1
   +0x000 Unused1          : 0y00000 (0)
   +0x000 SubsectionAddress : 0y111110101000000000110001011001000000110110000000 (0xfa8031640d80)

上面的subsection 地址只有48位,因为第48-63位已知被设置为F。为了重建完整的地址,我们必须将其与这些被设置的位进行OR运算。

1: kd> ?? (nt!_subsection*) (((unsigned int64) ((nt!_mmpfn*) 0xFFFFFA8001D33F10)->OriginalPte.u.Subsect.SubsectionAddress)|(0xFFFF<<48))
struct _SUBSECTION * 0xfffffa80`31640d80
   +0x000 ControlArea      : 0xfffffa80`31640d00 _CONTROL_AREA
   +0x008 SubsectionBase   : 0xfffff8a0`0039a000 _MMPTE
   +0x010 NextSubsection   : (null) 
   +0x018 PtesInSubsection : 0x8500
   +0x020 UnusedPtes       : 0
   +0x020 GlobalPerSessionHead : (null) 
   +0x028 u                : <unnamed-tag>
   +0x02c StartingSector   : 0
   +0x030 NumberOfFullSectors : 0x8500

这给我们提供了subsection内容,特别是控制区地址,可以给到!ca扩展名,以获得文件名:

1: kd> !ca @@(((nt!_subsection*) (((unsigned int64) ((nt!_mmpfn*) 0xFFFFFA8001D33F10)->OriginalPte.u.Subsect.SubsectionAddress)|(0xFFFF<<48)))->ControlArea)

ControlArea  @ fffffa8031640d00
  Segment      fffff8a000363610  Flink      0000000000000000  Blink        0000000000000000
  Section Ref                 1  Pfn Ref                 f43  Mapped Views               ab
  User Ref                    0  WaitForDel                0  Flush Count                 0
  File Object  fffffa80316419f0  ModWriteCount             0  System Views               ab
  WritableRefs                0  PartitionId                0  
  Flags (c088) NoModifiedWriting File WasPurged Accessed 

      \$Mft

Segment @ fffff8a000363610
  ControlArea       fffffa8031640d00  ExtendInfo    0000000000000000
  Total Ptes                    8500
  Segment Size               8500000  Committed                    0
  Flags (c0000) ProtectionMask 

Subsection 1 @ fffffa8031640d80
  ControlArea  fffffa8031640d00  Starting Sector        0  Number Of Sectors 8500
  Base Pte     fffff8a00039a000  Ptes In Subsect     8500  Unused Ptes          0
  Flags                       d  Sector Offset          0  Protection           6
  Accessed 
  Flink        0000000000000000  Blink   fffff80004648640  MappedViews         ab

我们看到上面的Mapped Views是9b,这告诉我们,除了我们开始的系统范围内的那个视图外,

section还有多个视图,我们先看看控制区的内容:

1: kd> ?? ((nt!_subsection*) (((unsigned int64) ((nt!_mmpfn*) 0xFFFFFA8001D33F10)->OriginalPte.u.Subsect.SubsectionAddress)|(0xFFFF<<48)))->ControlArea
struct _CONTROL_AREA * 0xfffffa80`31640d00
   +0x000 Segment          : 0xfffff8a0`00363610 _SEGMENT
   +0x008 DereferenceList  : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x018 NumberOfSectionReferences : 1
   +0x020 NumberOfPfnReferences : 0xf43
   +0x028 NumberOfMappedViews : 0xab
   +0x030 NumberOfUserReferences : 0
   +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`32dbc408 - 0xfffffa80`3164fc50 ]

ViewList成员将控制区与映射其视图的VAD列表相连接。在_MMVAD结构中,链接成员在+0x60处,所以上面的地址是VAD+0x60的地址。

1: kd> dt nt!_mmvad
   +0x000 u1               : <unnamed-tag>
   +0x008 LeftChild        : Ptr64 _MMVAD
   +0x010 RightChild       : Ptr64 _MMVAD
   +0x018 StartingVpn      : Uint8B
   +0x020 EndingVpn        : Uint8B
   +0x028 u                : <unnamed-tag>
   +0x030 PushLock         : _EX_PUSH_LOCK
   +0x038 u5               : <unnamed-tag>
   +0x040 u2               : <unnamed-tag>
   +0x048 Subsection       : Ptr64 _SUBSECTION
   +0x048 MappedSubsection : Ptr64 _MSUBSECTION
   +0x050 FirstPrototypePte : Ptr64 _MMPTE
   +0x058 LastContiguousPte : Ptr64 _MMPTE
   +0x060 ViewLinks        : _LIST_ENTRY
   +0x070 VadsProcess      : Ptr64 _EPROCESS

有了这个知识,我们可以转储VAD的内容:

1: kd> ?? (nt!_mmvad*)(0xfffffa80`32dbc408-0x60)
struct _MMVAD * 0xfffffa80`32dbc3a8
   +0x000 u1               : <unnamed-tag>
   +0x008 LeftChild        : 0x00000000`00a80040 _MMVAD
   +0x010 RightChild       : 0xfffffa80`32dbc390 _MMVAD
   +0x018 StartingVpn      : 0xfffffa80`32dbc3e0
   +0x020 EndingVpn        : 0xfffff980`0aac0000
   +0x028 u                : <unnamed-tag>
   +0x030 PushLock         : _EX_PUSH_LOCK
   +0x038 u5               : <unnamed-tag>
   +0x040 u2               : <unnamed-tag>
   +0x048 Subsection       : 0xfffff980`0ab00000 _SUBSECTION
   +0x048 MappedSubsection : 0xfffff980`0ab00000 _MSUBSECTION
   +0x050 FirstPrototypePte : 0xfffffa80`32867d80 _MMPTE
   +0x058 LastContiguousPte : 0x00000000`00080040 _MMPTE
   +0x060 ViewLinks        : _LIST_ENTRY [ 0xfffffa80`3268fd50 - 0xfffffa80`31640d70 ]
   +0x070 VadsProcess      : 0xfffff980`0ab40000 _EPROCESS

我们必须记住,一个VAD描述了一个进程的用户范围的一部分。我们看到的是,某个进程在其用户范围内为我们的文件映射了一个视图section

我们看到位于0xFFFFF8A0'01330000的原型PTE被映射到VPN 0x3790。再看一下我们物理页的!'pfn输出,我们看到pteaddress是OxFFFFF8A0'0133OOO8。因此,该页在VAD映射的第一个PTE的+8处映射了一个原型PTE。由于PTE的大小为8字节,这意味着我们的页面正在映射下一个原型PTE,也就是这个地址空间中的下一个虚拟页,即地址0x3791000。

以下的三个Windbg输出复现难度极大,故跳过……

VadsProcess成员给了我们这个地址空间的_EPROCESS地址。由于_EPROCESS结构是在偶数地址上对齐的,我们可以屏蔽掉最右边的1,看到该进程是mmc.exe。

知道了_EPROCESS的地址,我们可以使这个地址空间成为当前的,并检查虚拟地址0x3791000的PTE:

我们看到这个VA被映射到PFN 0x1837d,也就是我们原来的系统地址的同一个物理页,这证实了我们到目前为止的发现。现在我们可以看到,存储在_MMPFN中的WSL索引(即0xC5F),对于缓存WS来说是不正确的,但在这个进程WS中是有效的。

总之,_MMPFN.u1.WsIndex存储了一个对这个(也许还有其他)进程WS有效的索引,但对缓存WS无效,因此需要在后者中设置哈希表项。

2.6.2 分页池工作集

MmPagedPoolWs管理的WS跟踪分配给分页池的VA,我们将在后面讨论这个问题。广义上讲,它是一个可分页内存的范围,通过一组分配/释放函数提供给内核模块,它的作用类似于堆对进程的作用。无论如何,VMM需要管理这个范围的虚拟地址来实现分页池的功能,所以它使用了一个_MMSUPPORT来完成这个工作。

_MMSUPPORT中我们可以看到,_MMWSL_MMWSLE数组都属于系统工作集区域:

0: kd> ?? ((nt!_MMSUPPORT*) @@(nt!MmPagedPoolWs))->VmWorkingSetList
struct _MMWSL * 0xfffff781`c0000000
   +0x000 FirstFree        : 0x6667
   +0x004 FirstDynamic     : 1
   +0x008 LastEntry        : 0x679c
   +0x00c NextSlot         : 1
   +0x010 Wsle             : 0xfffff781`c0000488 _MMWSLE
   +0x018 LowestPagableAddress : 0xfffff8a0`00000000 Void
   +0x020 LastInitializedWsle : 0x696e
   +0x024 NextAgingSlot    : 0x6527
   +0x028 NumberOfCommittedPageTables : 0
   +0x02c VadBitMapHint    : 0
   +0x030 NonDirectCount   : 0
   +0x034 LastVadBit       : 0
   +0x038 MaximumLastVadBit : 0
   +0x03c LastAllocationSizeHint : 0
   +0x040 LastAllocationSize : 0
   +0x048 NonDirectHash    : (null) 
   +0x050 HashTableStart   : 0xfffff782`80000000 _MMWSLE_HASH
   +0x058 HighestPermittedHashAddress : 0xfffff782`80000000 _MMWSLE_HASH
   +0x060 MaximumUserPageTablePages : 0
   +0x064 MaximumUserPageDirectoryPages : 0
   +0x068 CommittedPageTables : (null) 
   +0x070 NumberOfCommittedPageDirectories : 0
   +0x078 CommittedPageDirectories : [128] 0
   +0x478 NumberOfCommittedPageDirectoryParents : 0
   +0x480 CommittedPageDirectoryParents : [1] 0

我们可以看到LowestPagableAddress被设置为分页池的起始地址。

映射分页池VA的物理页只是这个工作集的一部分,所以不需要哈希表项。NonDirectCount是0,直接哈希表甚至没有被映射:

0: kd> dd @@(((nt!_mmsupport*) @@(nt!MmPagedPoolWs))->VmWorkingSetList->HashTableStart)
fffff782`80000000  ???????? ???????? ???????? ????????
fffff782`80000010  ???????? ???????? ???????? ????????
fffff782`80000020  ???????? ???????? ???????? ????????
fffff782`80000030  ???????? ???????? ???????? ????????
fffff782`80000040  ???????? ???????? ???????? ????????
fffff782`80000050  ???????? ???????? ???????? ????????
fffff782`80000060  ???????? ???????? ???????? ????????
fffff782`80000070  ???????? ???????? ???????? ????????

2.6.3 系统PTE工作集

MmSystemPtesWs管理着系统范围内的另一个区域,这个区域也将在后面解释。现在我们可以把它看作是另一块可分页的虚拟地址空间,其当前状态由这个_MMSUPPORT实例来跟踪。

值得注意的是,MmSystemPtesWs.WorkingSetSize正好等于MmSystemDriverPageMmSystemCodePage的总和。

同样,我们可以看到_MMWSLWSLEs都在系统区域内,并且哈希表没有被映射:

0: kd> ?? ((nt!_MMSUPPORT*) @@(nt!MmSystemPtesWs))->VmWorkingSetList
struct _MMWSL * 0xfffff780`00001000
   +0x000 FirstFree        : 0x863
   +0x004 FirstDynamic     : 1
   +0x008 LastEntry        : 0x1602
   +0x00c NextSlot         : 1
   +0x010 Wsle             : 0xfffff780`00001488 _MMWSLE
   +0x018 LowestPagableAddress : 0xfffff800`00000000 Void
   +0x020 LastInitializedWsle : 0x176e
   +0x024 NextAgingSlot    : 0
   +0x028 NumberOfCommittedPageTables : 0
   +0x02c VadBitMapHint    : 0
   +0x030 NonDirectCount   : 0
   +0x034 LastVadBit       : 0
   +0x038 MaximumLastVadBit : 0
   +0x03c LastAllocationSizeHint : 0
   +0x040 LastAllocationSize : 0
   +0x048 NonDirectHash    : 0xfffff780`7ffff001 _MMWSLE_NONDIRECT_HASH
   +0x050 HashTableStart   : 0xfffff780`80000000 _MMWSLE_HASH
   +0x058 HighestPermittedHashAddress : 0xfffff780`a8000000 _MMWSLE_HASH
   +0x060 MaximumUserPageTablePages : 0
   +0x064 MaximumUserPageDirectoryPages : 0
   +0x068 CommittedPageTables : (null) 
   +0x070 NumberOfCommittedPageDirectories : 0
   +0x078 CommittedPageDirectories : [128] 0
   +0x478 NumberOfCommittedPageDirectoryParents : 0
   +0x480 CommittedPageDirectoryParents : [1] 0
0: kd> dd @@(((nt!_mmsupport*) @@(nt!MmSystemPtesWs))->VmWorkingSetList->NonDirectHash)
fffff780`7ffff001  ???????? ???????? ???????? ????????
fffff780`7ffff011  ???????? ???????? ???????? ????????
fffff780`7ffff021  ???????? ???????? ???????? ????????
fffff780`7ffff031  ???????? ???????? ???????? ????????
fffff780`7ffff041  ???????? ???????? ???????? ????????
fffff780`7ffff051  ???????? ???????? ???????? ????????
fffff780`7ffff061  ???????? ???????? ???????? ????????
fffff780`7ffff071  ???????? ???????? ???????? ????????
0: kd> dd @@(((nt!_mmsupport*) @@(nt!MmSystemPtesWs))->VmWorkingSetList->HashTableStart)
fffff780`80000000  ???????? ???????? ???????? ????????
fffff780`80000010  ???????? ???????? ???????? ????????
fffff780`80000020  ???????? ???????? ???????? ????????
fffff780`80000030  ???????? ???????? ???????? ????????
fffff780`80000040  ???????? ???????? ???????? ????????
fffff780`80000050  ???????? ???????? ???????? ????????
fffff780`80000060  ???????? ???????? ???????? ????????
fffff780`80000070  ???????? ???????? ???????? ????????

有趣的是,LowestPagableAddress指向System PTE区域的下方,也就是Initial Loader Mappings区域的开头,该区域存储了在系统启动早期加载的内核模式镜像,例如内核和HAL。由于这些映像有可分页的sections,它们也必须被一个工作集所追踪,这个工作集似乎就是这个。这与MmSystemCodePageMmSystemPtesW.WorkingSetSize的值有贡献的事实是一致的。:前者是用于映射内核镜像的物理页数,这些物理页被映射在初始加载器映射区域。

2.6.4 页面错误处理

MmAccessFault的代码显示,这个区域的页面错误和系统PTE区域的相同逻辑处理的。这表明VMM防止页面错误在这个区域发生(即在访问它们之前将VA映射到物理页面),这个事实也被这个区域的物理页面的_MMPFNs所证实,这些页面有_MMPFN.u1.WsIndex=0。

2.7 初始加载器映射

2.7.1 区域内容

顾名思义,这个区域是用来存储Windows启动时初始化的代码和数据结构。它更类似于动态分配的区域,而不是特殊用途的区域,因为它包含可执行的代码,而且部分区域是可分页的。然而,没有已知的内核函数来分配这个区域部分,它的内容似乎是在系统启动时确定的,并且在系统的生命周期内保持不变。

内核和Hal在这里被加载,还有内核调试器dll也是如此。我们将在后面的章节中分析内核模式的可执行文件是如何加载到内存中的。

这个区域还包括处理器0的_KPCR实例。 这个数据结构被命名为处理器控制区域,用来存储一个处理器的状态信息。它的地址由gs:[0]给出,也就是说,它在GS指向的段,其偏移量为0。GS所指向的段描述符是少数几个base不为零的段描述符之一。相反,它的base被设置为指向处理器的_KPCR的开头。每个处理器都有自己的GDT,虽然所有处理器的GS值都是0x2B,但是每个处理器都有一个不同的GS描述符。所以在每个处理器上,GS:[0]都指向_KPCR的开始。

_KPCR.Self存储了_KPCR实例的VA,它可以通过DS寄存器(当不考虑其他分段时,总是隐含地使用这个寄存器)来访问_KPCR的内容,其基址隐含地是0。 每次我们在调试器命令中使用VA时,它们都被隐含地解释为对DS段的偏移。因此,为了查看_KPCR,我们可以做以下工作:我们知道_KPCR.Self在结构内部的+0x18处,我们可以用dq查看它:

1: kd> dq gs:[18] L1
002b:00000000`00000018  fffff880`04500000

这给了我们_KPCRVA,它可以与dt或C++表达式一起使用。例如,下面的表达式查看了_KPCR. Self,并确认我们的地址是正确的:

1: kd> ?? (void*) ((nt!_KPCR*)0xfffff880`04500000)->Self
void * 0xfffff880`04500000

知道了这一点,我们可以看到0号处理器的_KPCR就在这个区域(~0命令将调试器切换到0号处理器):

0: kd> ~0
0: kd> dq gs:[18] l1
002b:00000000`00000018  fffff800`045f4d00

可能,0号处理器的这个基本数据结构在系统启动时被初始化(很可能id 0被分配给了启动处理器),所以它被存储在这个区域,在启动的早期步骤中使用。其他处理器后来被初始化,它们的_KPCR最终被保存在系统PTE区域,例如:

0: kd> ~1
1: kd> dq gs:[18] l1
002b:00000000`00000018  fffff880`04500000

这个区域还包括一个空闲线程的堆栈。当系统完全运行时,每个处理器都有一个空闲线程,看来0号处理器的线程在这里有堆栈,而其他的线程在系统PTE区域。空闲进程的_EPROCESS地址存储在KiInitialProcess中,所以我们可以用下面的命令检查线程堆栈:

1: kd> !process nt!KiInitialProcess 6
PROCESS fffff800046031c0
    SessionId: none  Cid: 0000    Peb: 00000000  ParentCid: 0000
    DirBase: 00187000  ObjectTable: fffff8a0000017d0  HandleCount: 579.
    Image: Idle

        THREAD fffff80004602cc0  Cid 0000.0000  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
        Not impersonating
        DeviceMap                 fffff8a000008c10
        Owning Process            fffff800046031c0       Image:         Idle
        Attached Process          fffffa8030ef8040       Image:         System
        Wait Start TickCount      60187          Ticks: 3674467 (0:15:55:22.052)
        Context Switch Count      5071069        IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                16:08:24.709
        Win32 Start Address nt!KiIdleLoop (0xfffff8000447a310)
        Stack Init fffff80000b9cc70 Current fffff80000b9cc00
        Base fffff80000b9d000 Limit fffff80000b97000 Call 0000000000000000
        Priority 16 BasePriority 0 PriorityDecrement 0 IoPriority 0 PagePriority 0
        Child-SP          RetAddr               : Args to Child                                                           : Call Site
        fffff800`00b9cc40 00000000`00000000     : fffff800`00b9d000 fffff800`00b97000 fffff800`00b9cc00 00000000`00000000 : nt!KiIdleLoop+0x10d

        THREAD fffff8800450af40  Cid 0000.0000  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 1
        Not impersonating
        DeviceMap                 fffff8a000008c10
        Owning Process            fffff800046031c0       Image:         Idle
        Attached Process          fffffa8030ef8040       Image:         System
        Wait Start TickCount      0              Ticks: 3734654 (0:16:11:00.975)
        Context Switch Count      3221471        IdealProcessor: 1             
        UserTime                  00:00:00.000
        KernelTime                16:09:01.151
        Win32 Start Address nt!KiIdleLoop (0xfffff8000447a310)
        Stack Init fffff88004528c70 Current fffff88004528c00
        Base fffff88004529000 Limit fffff88004523000 Call 0000000000000000
        Priority 16 BasePriority 0 PriorityDecrement 0 IoPriority 0 PagePriority 0
        Child-SP          RetAddr               : Args to Child                                                           : Call Site
        fffff880`04528c40 00000000`00000000     : fffff880`04529000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiIdleLoop+0x10d

从上面的命令输出中,我们看到运行在处理器1上的线程的堆栈是在系统PTE区域。整个情况与_KPCRs的情况类似:初始加载器映射区域用于启动处理器,而系统PTE区域用于其他处理器。

内核栈在后面进行分析。

同样有趣的是,_ETHREAD(显示在上面标签THREAD的右边)的地址也遵循同样的模式:只有运行在处理器0上的那一个在初始加载器映射区域。

2.7.2 页面错误处理

该区域的页面错误与发生在系统PTE区域的错误处理一样。

在这个区域中,有一种情况可能会发生页面错误,就是当内核的一个不存在的地址被引用时。这种情况可能发生,因为内核的某些section是可以被分页的(后面讲解镜像加载时会更详细地描述内核是如何加载和分页的)。当这种情况发生时,错误被解决,新映射的VA被计入系统PTE工作集,因此也有这个区域的项。

作为一个例子,下面我们看到加载内核的地址范围:

1: kd> lm m nt
Browse full module list
start             end                 module name
fffff800`04402000 fffff800`049ec000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\3844DBB920174967BE7AA4A2C20430FA2\ntkrnlmp.pdb

Unable to enumerate user-mode unloaded modules, Win32 error 0n30

地址0xfffff8000489d000就在这个范围内,下面我们可以看到其页面的_MMPFN的细节:

1: kd> !pte fffff8000489d000
                                           VA fffff8000489d000
PXE at FFFFF6FB7DBEDF80    PPE at FFFFF6FB7DBF0000    PDE at FFFFF6FB7E000120    PTE at FFFFF6FC000244E8
contains 0000000000199063  contains 0000000000198063  contains 00000000048009E3  contains 0000000000000000
pfn 199       ---DA--KWEV  pfn 198       ---DA--KWEV  pfn 4800      -GLDA--KWEV  LARGE PAGE pfn 489d
1: kd> !pfn 489d
    PFN 0000489D at address FFFFFA80000D9D70
    flink       00000000  blink / share count 00000001  pteaddress FFFFF6FC000244E8
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000000  containing page 0001E8  Active

这里得到的总是大页,与书上的不一样。

2.8 系统PTE

2.8.1 区域内容

#### 区域的目的和其名字

这是一个动态申请的区域。

这个区域的名字是一个不幸的名字,因为它具有误导性。尽管如此,这个区域并不存储任何PTEs。我们已经知道,处理器使用的实际PTEs被映射在PTE空间区域。即使是原型PTEs,即软件定义的PTEs工作副本,也存储在其他地方,即分页池区域。正如我们即将看到的,这个区域被保留下来,用于动态地映射和取消映射虚拟地址空间中一些与内核模式相关的 "东西"。因此,映射这个(系统)范围的PTEs可以用于这些映射,而虚拟范围的名字就来自于它们。

这个奇特名称的另一个原因可能是,这个区域的虚拟地址范围可以以页为单位进行分配。其他动态分配的区域以PDE单元管理虚拟地址空间,所以每个分配是2MB的倍数,并在2MB边界上对齐。对于这个区域,存在额外的逻辑和数据结构,允许分配单个PTE,而不是PDE,因此,可能是区域名称的来源。我们将在后面研究这个区域的虚拟地址空间管理是如何实现的。

接下来的章节描述了在这个区域发现的不同种类的内容。

#### MDL映射

MDL是一个数据结构,通常被内核模式组件用来与内存管理器交互。MDL的一个常见用途是允许内核模式组件访问由用户模式代码分配的数据缓冲区。例如,这可以在内核模式驱动中完成,它将缓冲区的内容传递给设备,或者用来自设备的数据填充缓冲区。用户模式的缓冲区只在分配它的进程的上下文中被映射;另一方面,对于一个驱动程序来说,经常需要能够在一个任意的进程上下文中访问缓冲区(例如,在一个由中断调用的函数中)。这是通过在系统PTE区域映射缓冲区的物理页来实现的。物理页仍然被映射到原始的用户模式地址,所以会有两个不同的虚拟范围被映射到相同的页面:原始的和系统PTE区域的范围,后者在任何进程上下文中都有效。

一个MDL可以被用来执行这个额外的映射。这个结构可以同时存储PFN数组和映射到物理页的VA范围的地址。最初,VA地址被设置为缓冲区的用户模式地址。在这个阶段,PFN数组的内容是无效的,因为用户模式的范围是可以分页的,所以它的PTEs不一定指向物理页:它们可能处于过渡期,被换掉或者demand-zero。因此,下一步是在分配缓冲区的进程的上下文中调用 MmProbeAndLockPages。这个DDI将虚拟范围映射到实际的内存,并锁定物理页,这样PTE就保持有效,并指向同一个PFN。它还将该范围的PFN复制到MDL的数组中。只要这些页被锁定,这个数组的内容就一直有效。最后,调用MmGetSystemAddressForMdlSafe将数组中的PFN映射到系统PTE区域的一个虚拟范围。

MDL还要其他用途,但我们不打算在这个问题上进一步挖掘。

重要的是不要混淆分配MDLs结构的VA和映射到数组中PFNsVAMDLs被分配在非分页池中,所以它们的地址可以在该区域找到,而数组中的PFNs,当被映射时,在系统PTE区域是可见的。

#### 其他种类的内容

该区域还用于以下类型的内容:

  • 内核模式的驱动程序在这个区域被加载。我们将在后面看到更多关于它们如何被带入内存的细节。
  • 内核堆栈,即线程在内核模式下执行时使用的堆栈。
  • 设备内存映射:一些设备被硬件映射到物理内存地址上,这样处理器就可以通过在这些地址上读写来与它们进行交互。像往常一样,处理器指令用虚拟地址工作,所以这些特殊的物理地址必须在虚拟空间的某个地方进行映射,这个区域就用于这个目的。
  • 系统中每个会话的一个的实例被映射到这个区域内。

2.8.2 虚拟地址空间管理

#### 分配粒度问题

本节详细描述了区域VA管理。由于这里发现的许多概念都是没有记载的,所以后面将给出支持性证据。

这个区域的地址空间由一个名为MiSystemPteBitmap的位图控制,每个位代表一个PDE,横跨2MB的虚拟地址空间。

然而,在这个区域进行的许多分配是针对VA大小远远小于2MB的。例如,这个区域被用来映射MDL描述的缓冲区,它可以小到1页。另外,线程内核堆栈也被分配在这里,它们的大小达到了几千字节。强制每个这样的分配在2MB的边界上开始会浪费大量的虚拟地址空间,所以VMM实现了逻辑——以页大小的块管理这个区域。

这种行为的一个例子是MmMapLockedPagesSpecifyCache DDI,它在这个区域中映射一个由MDL描述的缓冲区,并返回页面对齐的,而不是PDE对齐的地址。另一个例子是线程内核模式堆栈,它是由页对齐的地址划分的。

#### 解决方案:更多的位图

VMM维护两个_MI_SYSTEM_PTE_TYPE的静态实例来管理这个区域的分配,名为MiSystemPtelnfoMiKemelStackPtelnfo。下面是WinDbg dt命令中的类型声明:

1: kd> dt _MI_SYSTEM_PTE_TYPE
nt!_MI_SYSTEM_PTE_TYPE
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : Uint4B
   +0x014 Hint             : Uint4B
   +0x018 BasePte          : Ptr64 _MMPTE
   +0x020 FailureCount     : Ptr64 Uint4B
   +0x028 Vm               : Ptr64 _MMSUPPORT
   +0x030 TotalSystemPtes  : Int4B
   +0x034 TotalFreeSystemPtes : Int4B
   +0x038 CachedPteCount   : Int4B
   +0x03c PteFailures      : Uint4B
   +0x040 SpinLock         : Uint8B
   +0x040 GlobalMutex      : Ptr64 _KGUARDED_MUTEX

Bitmap成员就像它的名字一样,每个位代表系统PTE区域的一个页面。通过调用MiReservePtes 来进行该区域的分配,MiReservePtes的第一个参数是用于分配的_MI_SYSTEM_PTE_TYPE实例的地址。

在详细研究这些结构是如何使用的之前,让我们集中讨论一下为什么有两个结构。根据资料,原因是为了避免VA碎片化:一个实例用于保留时间较长的分配,如内核模式栈,另一个用于保留时间较短的分配,如MDLs。因此,有两个实例覆盖不同的子范围可以避免,比如说,两个堆栈之间有一个由已经分配和释放的MDL引起的未使用的缺口。

有两个实例的另一个可能的好处是,它减少了对位图本身的独占访问的争夺。例如,MDL分配不会阻止堆栈分配,等等。

#### 位图缓冲区最大大小和地址

两个位图都有一个足够大的缓冲区来覆盖整个系统PTE区域

该区域横跨128GB,并且知道位图缓冲区在这个范围内的每个页面都必须有一个位,我们可以计算出缓冲区的最大尺寸为:

0x20`00000000 / 0x1000 / 8 = 0x400000 bytes = 4MB

先计算页数,再计算比特数,因为考虑到每个位图字节有8位。

下面是我们从两个实例中转储的缓冲区地址:

1: kd> ?? ((nt!_MI_SYSTEM_PTE_TYPE*) @@(nt!MiSystemPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0x18800
   +0x008 Buffer           : 0xfffff880`00000000  -> 0xffffffff
1: kd> ?? ((nt!_MI_SYSTEM_PTE_TYPE*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0x6a00
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff

首先,我们可以看到,这些缓冲区被存储在System PTE区域本身。然后我们看到MiKernelStackPtelnfo的缓冲区紧挨着MiSystemPtelnfo的缓冲区。两者的大小都是4MB,所以系统PTE区域的前8MB被预留出来用于此目的。我们不应该在0xFFFFF880'000000-0xFFFFF880'00800000范围内看到MDL、内核栈或加载的镜像文件。

位图中的每一位都代表系统PTE范围的一个页面:第一个字节的第0位对应于0xFFFFF880'000000-0xFFFFF880'000FFF范围,等等。有趣的是,由于该范围的前8MB被用于缓冲区本身,所以第一个位图的字节将总是被设置为0xFF。

0x800000 / 0x1000 / 8 = 0x100

#### 缓冲区实际大小和扩展

尽管每个位图有4MB的虚拟地址,但这个范围中只有一部分被映射到物理页。_RTL_BITMAP.SizeOfBitmap给出了位图的有效位数,通常比4MB * 8低得多。只有当区域内的分配不能满足当前的位图内容时,这两个位图才会被扩展。

如果我们看一下系统启动时位图是如何被初始化的,这就更容易理解了。负责初始化两个实例的函数是MiInitializeSystemPtes ([2]),它将SizeOfBitmap设置为8,并将缓冲区的所有虚拟页映射为一个充满0xFF的物理页。这很有意思:两个4MB缓冲区的虚拟内容代表了一个完全被利用的位图,但只消耗了一个物理页。

因为所有的位图位都被设置为1,所以是SizeOfBitmap告诉我们这些1中有多少是真正在使用的页面。它的初始值为8,意味着位图是完全空的,也就是说,它的缓冲区没有任何部分是有效的。

主位图MiSystemPteBitmap被初始化,其第一个字节被设置为0xF,其余的缓冲区被设置为0。 这表示前4个PDE,即该区域的前8MB正在使用中(因为它们被保留给从位图)。

在初始化过程中,从从位图进行的第一次分配通常从MiKernelStackPtelnfo申请0x40个页。一个涵盖0x200页的PDE被从主位图中预留出来,其第一个字节变成0x1F,从位图被更新。

其缓冲区的第一个虚拟页被映射到一个实际的页面,因此其内容可以被设置为每个字节中的0xFF以外的内容。

SizeOfBitmap被设置为0xA00。为了理解这个值,我们必须记住,该区域的前8MB用于缓冲区。这对应于0x800个页面,即位图位,这些位图位将总是被设置为1。然后,一个跨越0x200位的PDE刚刚被添加到位图中,所以它的最终大小是0xA00。这意味着,在位图缓冲区可见的位中,0xA00有实际意义,即0表示空闲页,1表示分配页,而其他的必须被忽略。

通常情况下,第一个缓冲区页面中索引0xA00以外的位被设置为1,就像它们代表正在使用的页面一样。缓冲区的所有其他页仍然被映射到充满0xFF的物理页上,所以它们的位也是1。换句话说,0xA00之外的所有位(即SizeOfBitmap被设置为1)。

第一个0xA00位,即位图的0x140字节被设置为代表PTE实际状态的值:第一个0x800位被设置为1,由于分配的是0x40页,接下来的0x40位也被设置为1。其余的位,直到位0x9FF都是清零的,代表这些虚拟页面,现在由从位图管理,可以自由分配。

请注意,为了把从主位图分配的页面包括在从位图中,我们必须调整其内容,以便正确表示前面的0x800使用中的页面。只要SizeOfBitmap是8,位图的内容其实并不重要。现在它变成了0xA00,所有的0xA00位必须被设置为正确的值。

每当两个从位图中的一个必须被扩展时,就会重复这个过程:查阅主位图,以找到空闲的PDE,然后调整缓冲区的大小和内容,以包括分配给位图的新的空闲页和所有与已经分配的页相对应的必须包括的设定位。

这种扩展是由MiExpandPtes执行的,它调用MiObtainSystemVa,为页面类型指定MiVaSystemPtes

正如我们所看到的,每个主位图在需要时被扩展,扩展其缓冲区所覆盖的虚拟大小,以便获得新的空闲页。

还有一些内核函数直接从主位图MiSystemPteBitmap进行分配。这意味着,在下一次主位图扩展时,必须包括主位图中设置的一定数量的位,才能达到清零位。这与对应于缓冲区范围的第一个0x800位发生的事情相同。换句话说,当扩展一个主位图时,将发现一些跟随扩展前位图大小的PDE在使用。如果它们是由另一个从位图分配的或直接来自主位图,这并不重要。它们只是被纳入正在扩展的位图中,作为相应页面的设置位。

所有这些分配都是随机发生的,所以我们最终得到了图58中描述的情景。

Untitled 2.png

图58中的左图,从MiKernelStackPtelnfo位图的角度描述了虚拟地址空间。阴影框代表没有分配给位图的范围,并在扩展过程中作为设置位纳入。这些范围的位在这个位图中将总是被设置。白色框代表分配给位图的范围,因此它们的位可以被设置或清除,取决于页面状态。右图有同样的含义,但对于MiSystemPtelnfo位图来说。前8MB以及直接从主位图分配的区域在两张图中都有阴影,因为这些范围不受这两个位图中的任何一个控制。

图中显示,由两个位图管理的子区域相互交替,取决于分配的顺序,所以系统PTE范围不是简单地分成两个子区域给两个位图。

最后,VMM可以决定减少一个从位图,将其范围的一部分返回到主位图的控制之下。

#### 额外的系统PTEs

对空闲系统PTEs性能计数器的分析表明,在系统范围的其他一些地方有额外的系统PTEs可用。

使用windbg进行位图分析

这一节展示了上一节中描述的逻辑在系统初始化时的工作。它的目的是为前面解释的概念提供证据,所以愿意相信作者的话的读者可以跳过它。然而这个实验完成后你会对MmAllocateMappingAddress DDI的内部实现有一个小的了解。

为了能够调试位图的初始化,必须使调试器在启动期间很早就控制系统。这使得我们可以在MiInitializeSystemPtes上设置一个断点,并在初始化前后检查位图。当断点被击中时,我们看到以下情况:

kd> bp nt!MiInitializeSystemPtes
kd> g
Breakpoint 0 hit
nt!MiInitializeSystemPtes:
fffff800`04289bb0 488bc4          mov     rax,rsp
kd> ?? ((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))
struct _RTL_BITMAP * 0xfffff800`040c07a0
   +0x000 SizeOfBitMap     : 0x10000
   +0x008 Buffer           : 0xfffff800`0403eb40  -> 0
kd> ln @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer)
Browse module
Set bu breakpoint

(fffff800`0403eb40)   nt!MiSystemPteBits   |  (fffff800`04040b40)   nt!MiSystemVaTypeCount
Exact matches:
    nt!MiSystemPteBits = <no type information>

主位图已经被初始化,大小等于0x10000。由于每个位代表一个PDE,这个大小涵盖了系统PTE范围的128GB:

0x10000*0x200000 = 0x20`00000000 = 128GB

我们还可以注意到,主位图缓冲区本身是nt内部的一个静态区域,其地址的符号是MiSystemPteBits。上面的输出显示,第一个字节被设置为0(缓冲区的其他部分也是如此)。

让我们来看看从位图:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403bf00
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 0
   +0x014 Hint             : 0
   +0x018 BasePte          : (null) 
   +0x020 FailureCount     : (null) 
   +0x028 Vm               : (null) 
   +0x030 TotalSystemPtes  : 0n0
   +0x034 TotalFreeSystemPtes : 0n0
   +0x038 CachedPteCount   : 0n0
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null) 
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0
   +0x008 Buffer           : (null) 
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403c760
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 0
   +0x014 Hint             : 0
   +0x018 BasePte          : (null) 
   +0x020 FailureCount     : (null) 
   +0x028 Vm               : (null) 
   +0x030 TotalSystemPtes  : 0n0
   +0x034 TotalFreeSystemPtes : 0n0
   +0x038 CachedPteCount   : 0n0
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null) 
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0
   +0x008 Buffer           : (null)

可以发现现在控制的结构体都被初始化为0。我们将会看到MiInitializeSystemPtes完成工作返回后,事情就会发生变化。

kd> g @$ra
nt!MmInitNucleus+0x209:
fffff800`043b3619 3bc7            cmp     eax,edi

下面是主位图的内容:

kd> ?? ((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))
struct _RTL_BITMAP * 0xfffff800`040c07a0
   +0x000 SizeOfBitMap     : 0x10000
   +0x008 Buffer           : 0xfffff800`0403eb40  -> 0xf

Buffer的第一个字节现在被设置成0xF,也就是前4位置1了。剩下的buffer依旧是0.

kd> dq 0xfffff800`0403eb40
fffff800`0403eb40  00000000`0000000f 00000000`00000000
fffff800`0403eb50  00000000`00000000 00000000`00000000
fffff800`0403eb60  00000000`00000000 00000000`00000000
fffff800`0403eb70  00000000`00000000 00000000`00000000
fffff800`0403eb80  00000000`00000000 00000000`00000000
fffff800`0403eb90  00000000`00000000 00000000`00000000
fffff800`0403eba0  00000000`00000000 00000000`00000000
fffff800`0403ebb0  00000000`00000000 00000000`00000000

这意味着前8MB的范围被分配了,我们知道它们是为从位图预留的。我们现在要看一下这些位图的控制结构:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403bf00
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 3
   +0x014 Hint             : 0x800
   +0x018 BasePte          : 0xfffff6fc`40000000 _MMPTE
   +0x020 FailureCount     : 0xfffff800`0403bf3c  -> 0
   +0x028 Vm               : 0xfffff800`04013ec0 _MMSUPPORT
   +0x030 TotalSystemPtes  : 0n0
   +0x034 TotalFreeSystemPtes : 0n0
   +0x038 CachedPteCount   : 0n0
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null) 
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 8
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403c760
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 3
   +0x014 Hint             : 0x800
   +0x018 BasePte          : 0xfffff6fc`40000000 _MMPTE
   +0x020 FailureCount     : 0xfffff800`0403c79c  -> 0
   +0x028 Vm               : 0xfffff800`04013ec0 _MMSUPPORT
   +0x030 TotalSystemPtes  : 0n0
   +0x034 TotalFreeSystemPtes : 0n0
   +0x038 CachedPteCount   : 0n0
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null) 
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 8
   +0x008 Buffer           : 0xfffff880`00000000  -> 0xffffffff

控制结构现在被初始化了,包括位图缓冲区地址。下面是第一个缓冲区的映射方式:

kd> !pte @@(((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap.Buffer)
                                           VA fffff88000000000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200000    PTE at FFFFF6FC40000000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BDC2863  contains 000000007BD80121
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7bdc2     ---DA--KWEV  pfn 7bd80     -G--A--KREV

第一页被映射到PFN 0x7bd80。我们可以通过转储PTE地址的内存内容来快速检查以下页面的PTEs:

kd> dq FFFFF6FC40000000
fffff6fc`40000000  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000010  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000020  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000030  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000040  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000050  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000060  00000000`7bd80121 00000000`7bd80121
fffff6fc`40000070  00000000`7bd80121 00000000`7bd80121

PTEs都有相同的值,也就是说,VAs都被映射到了同一个物理页。还请注意上面的!pte输出显示这些映射有只读保护。我们可以通过转储最后两个PTEs来进一步确认所有的缓冲区都是这样映射的。首先,我们再次使用 !pte 来计算它们的地址:

kd> !pte fffff880`00400000-2000
                                           VA fffff880003fe000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200008    PTE at FFFFF6FC40001FF0
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BD41863  contains 000000007BD80121
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7bd41     ---DA--KWEV  pfn 7bd80     -G--A--KREV

kd> dq FFFFF6FC40001FF0 l2
fffff6fc`40001ff0  00000000`7bd80121 00000000`7bd80121

这种映射的是一个充满0xFF的页面:

kd> dq fffff880`00000000
fffff880`00000000  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000010  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000020  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000030  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000040  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000050  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000060  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000070  ffffffff`ffffffff ffffffff`ffffffff

第二个的缓冲区以同样的方式进行映射:

kd> !pte @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer)
                                           VA fffff88000400000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200010    PTE at FFFFF6FC40002000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BE46863  contains 000000007BD80121
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7be46     ---DA--KWEV  pfn 7bd80     -G--A--KREV

kd> dq FFFFF6FC40002000
fffff6fc`40002000  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002010  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002020  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002030  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002040  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002050  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002060  00000000`7bd80121 00000000`7bd80121
fffff6fc`40002070  00000000`7bd80121 00000000`7bd80121

这使我们能够确认第二个缓冲区的大小是4MB,通过观察映射是这样设置的,正好到这个大小产生的结束地址。下面的!pte输出为我们提供了4MB范围内最后一页的PTE地址:

kd> !pte @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer)+400000-1000
                                           VA fffff880007ff000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200018    PTE at FFFFF6FC40003FF8
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BE05863  contains 000000007BD80121
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7be05     ---DA--KWEV  pfn 7bd80     -G--A--KREV

下面是其PTE的内容:

kd> dq FFFFF6FC40003FF8
fffff6fc`40003ff8  00000000`7bd80121 ????????`????????
fffff6fc`40004008  ????????`???????? ????????`????????
fffff6fc`40004018  ????????`???????? ????????`????????
fffff6fc`40004028  ????????`???????? ????????`????????
fffff6fc`40004038  ????????`???????? ????????`????????
fffff6fc`40004048  ????????`???????? ????????`????????
fffff6fc`40004058  ????????`???????? ????????`????????
fffff6fc`40004068  ????????`???????? ????????`????????

确认一个4MB的范围被映射到PFN 0x7bd8。

#### 位图扩展

为了继续我们的分析,我们现在将设置几个断点。第一个断点是在MiExpandPtes上,它被调用来扩展从位图所覆盖的范围(到目前为止是空的)。另一个是写到主位图缓冲区的qword断点,以拦截从那里分配VA的代码。还有两个断点将捕获写到
到_MI_SYSTEM_PTE_TYPE.SizeOfBitMap,这样我们就可以在二级位图被扩展时断下。注意,SizeOfBitmap在_MI_SYSTEM_PTE_TYPE结构内部的偏移量为0,所以断点将在结构实例地址处。这就是我们的断点列表:

kd> bp nt!MiExpandPtes
kd> ba w8 nt!MiSystemPteBits
kd> ba w4 nt!MiKernelStackPteInfo
kd> ba w4 nt!MiSystemPteInfo
kd> bl
     0 e Disable Clear  fffff800`04289bb0     0001 (0001) nt!MiInitializeSystemPtes
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo

第一个断下的是MiExpandPtes上的断点:

kd> g
Breakpoint 1 hit
nt!MiExpandPtes:
fffff800`03ec8108 48895c2408      mov     qword ptr [rsp+8],rbx
kd> k
 # Child-SP          RetAddr           Call Site
00 fffff800`00b9c328 fffff800`03e72064 nt!MiExpandPtes
01 fffff800`00b9c330 fffff800`040caa3c nt!MiReservePtes+0x68c
02 fffff800`00b9c3d0 fffff800`043b3652 nt!MmInitializeProcessor+0x1c
03 fffff800`00b9c400 fffff800`043b7bc5 nt!MmInitNucleus+0x242
04 fffff800`00b9c4a0 fffff800`043bcc25 nt!MmInitSystem+0x15
05 fffff800`00b9c4d0 fffff800`040d77b0 nt!InitBootProcessor+0x385
06 fffff800`00b9c700 fffff800`040d8045 nt!ExpInitializeExecutive+0x10
07 fffff800`00b9c730 fffff800`040c588c nt!KiInitializeKernel+0x875
08 fffff800`00b9cab0 00000000`00000000 nt!KiSystemStartup+0x19c

rcx被设置为要扩展的位图的控制结构的地址,对于本次调用来说是MiKemelStackPtelnfo:

kd> ln @rcx
Browse module
Clear breakpoint 3

(fffff800`0403bf00)   nt!MiKernelStackPteInfo   |  (fffff800`0403c750)   nt!MiPteTrackingBitmap
Exact matches:
    nt!MiKernelStackPteInfo = <no type information>

我们很快也会看到,rdx被设置为分配给位图的页数,所以这个调用分配的是0x40页:

kd> r
rax=0000000000000000 rbx=0000000000000100 rcx=fffff8000403bf00
rdx=0000000000000040 rsi=fffff8000403bf00 rdi=0000000000000040
rip=fffff80003ec8108 rsp=fffff80000b9c328 rbp=fffff8000403c748
 r8=0000000000000000  r9=0000000000000007 r10=0000000000000000
r11=0000000000000040 r12=00000000ffffffff r13=0000000000000000
r14=0000000000000002 r15=0000000000000001
iopl=0         nv up ei ng nz na po cy
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000287
nt!MiExpandPtes:
fffff800`03ec8108 48895c2408      mov     qword ptr [rsp+8],rbx ss:0018:fffff800`00b9c330=635363436c43624f

主位图以2MB块分配虚拟地址,相当于0x200页,所以这个数量实际上将被分配给从位图,只使用其中的0x40,其余的可用于进一步分配。

在恢复执行之前,我们为当前线程在MiExpandPtes的返回地址上再添加一个断点,这样我们就能知道对这个函数的调用何时完成:

bp /t @$thread @$ra

当我们恢复执行时,我们看到主位图缓冲区的断点被击中:

kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> k
 # Child-SP          RetAddr           Call Site
00 fffff800`00b9c170 fffff800`03ec7232 nt!RtlSetBits+0x3d
01 fffff800`00b9c1a0 fffff800`03ec816f nt!MiObtainSystemVa+0x32a
02 fffff800`00b9c290 fffff800`03e72064 nt!MiExpandPtes+0x67
03 fffff800`00b9c330 fffff800`040caa3c nt!MiReservePtes+0x68c
04 fffff800`00b9c3d0 fffff800`043b3652 nt!MmInitializeProcessor+0x1c
05 fffff800`00b9c400 fffff800`043b7bc5 nt!MmInitNucleus+0x242
06 fffff800`00b9c4a0 fffff800`043bcc25 nt!MmInitSystem+0x15
07 fffff800`00b9c4d0 fffff800`040d77b0 nt!InitBootProcessor+0x385
08 fffff800`00b9c700 fffff800`040d8045 nt!ExpInitializeExecutive+0x10
09 fffff800`00b9c730 fffff800`040c588c nt!KiInitializeKernel+0x875
0a fffff800`00b9cab0 00000000`00000000 nt!KiSystemStartup+0x19c

观察MiExpandPtes是如何调用MiObtainSystemVa的,这是一个通用的VA分配例程,也用于其他系统区域,这很有意思。

下面我们发现主位图缓冲区是如何变化的:

kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000001f

第一个字节从0xF变为0x1F,即1个PDE被分配,对应的范围是0xFFFFF880'00800000 - 0XFFFFF88000BFFFFF。

恢复执行,我们在更新二级位图的大小时中断:

kd> g
Breakpoint 3 hit
nt!MiExpandPtes+0x3ca:
fffff800`03ec84d2 e99ffdffff      jmp     nt!MiExpandPtes+0x16e (fffff800`03ec8276)
kd> bl
     0 e Disable Clear  fffff800`04289bb0     0001 (0001) nt!MiInitializeSystemPtes
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo
     5 e Disable Clear  fffff800`03e72064     0001 (0001) nt!MiReservePtes+0x68c
     Match thread data fffff800`0400fcc0

二级位图现在已经改变了。首先,第一个缓冲页现在被映射到一个不同的页面上,有读/写保护:

kd> !pte @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer)
                                           VA fffff88000400000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200010    PTE at FFFFF6FC40002000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BE46863  contains 000000007BE08963
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7be46     ---DA--KWEV  pfn 7be08     -G-DA--KWEV

缓冲区的大小从8个变成了0xAOO:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0xa00
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff

让我们看看这个值是如何计算的:在主位图中,已经为缓冲区范围设置了4位,对应于8MB或0x800页。现在已经分配了一个位,也就是一个PDE,大小为2MB或0x200页。二级位图现在包括整个范围:前0x800页已经包括在使用中,即作为设置的位,最后0x200页作为实际可用于从这个位图分配的页。在缓冲区中,这相当于一个字节范围为:

0xA00/8 = 0x140 bytes

它完全包含在第一个虚拟页中,现在被映射到一个真实的、可更新的页面。在这个阶段,缓冲区范围内充满了0xFF,就像所有的页面都在使用一样,但是我们将看到,在从MiExpandPtes返回后,空闲的页面将被清零:

kd> dq @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer) l140/8
fffff880`00400000  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400010  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400020  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400030  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400040  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400050  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400060  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400070  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400080  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400090  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400100  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400110  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400120  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400130  ffffffff`ffffffff ffffffff`ffffffff

再次恢复执行,我们在MiExpandPtes的返回地址上碰到了断点:

kd> g
Breakpoint 5 hit
nt!MiReservePtes+0x68c:
fffff800`03e72064 33ed            xor     ebp,ebp

现在可以看到有些缓冲区为0:

kd> dq @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer) l140/8
fffff880`00400000  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400010  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400020  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400030  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400040  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400050  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400060  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400070  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400080  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400090  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400100  ffffffff`ffffffff 00000000`00000000
fffff880`00400110  00000000`00000000 00000000`00000000
fffff880`00400120  00000000`00000000 00000000`00000000
fffff880`00400130  00000000`00000000 00000000`00000000

让我们更详细地检查它的内容:字节0-0xFF对应于比特索引0-0x7ff,它覆盖了前8MB的地址空间。这些位已经被并入位图中,因为它们在主位中已经被设置了。从0x100开始,刚刚分配给这个位图的PDE的0x200位。其中,前64位或0x40位被设置,这与MiExpandPtes被调用时rdx的值一致。

这显示了位图是如何被扩展以包括所有已经分配的页面和分配给它的新页面的。

我们还可以看到,MiExpandPtes的返回值是PTE的地址,映射了分配给位图的范围的开始:

kd> r
rax=fffff6fc40004000 rbx=0000000000000100 rcx=0000000000000042
rdx=fffffa800000001e rsi=fffff8000403bf00 rdi=0000000000000040
rip=fffff80003e72064 rsp=fffff80000b9c330 rbp=fffff8000403c748
 r8=0000000000000000  r9=0000000000000001 r10=0000000000000000
r11=0000000000000000 r12=00000000ffffffff r13=0000000000000000
r14=0000000000000002 r15=0000000000000001
iopl=0         nv up ei ng nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000286
nt!MiReservePtes+0x68c:
fffff800`03e72064 33ed            xor     ebp,ebp
kd> !pte @rax
                                           VA fffff88000800000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200020    PTE at FFFFF6FC40004000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BE09863  contains 0000000000000000
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7be09     ---DA--KWEV  not valid

现在我们将删除MiExpandPtes返回地址上的断点并恢复执行。我们将看到重复同样的过程来扩展MiSystemPteInfo二级位图,我们将看到这个位图将包含MiKernelStackPteInfo覆盖的所有范围,并将扩展到额外的0x200页。

kd> bc5
kd> g
Breakpoint 1 hit
nt!MiExpandPtes:
fffff800`03ec8108 48895c2408      mov     qword ptr [rsp+8],rbx

看一下堆栈,看看发生了什么:

kd> k
 # Child-SP          RetAddr           Call Site
00 fffff800`00b9c2a8 fffff800`03e4f345 nt!MiExpandPtes
01 fffff800`00b9c2b0 fffff800`043b62c3 nt!MiZeroPhysicalPage+0x521
02 fffff800`00b9c340 fffff800`043b6fc8 nt!MiInitializeBootProcess+0x3e3
03 fffff800`00b9c410 fffff800`043b7bd3 nt!MiInitSystem+0xb8
04 fffff800`00b9c4a0 fffff800`043bcc25 nt!MmInitSystem+0x23
05 fffff800`00b9c4d0 fffff800`040d77b0 nt!InitBootProcessor+0x385
06 fffff800`00b9c700 fffff800`040d8045 nt!ExpInitializeExecutive+0x10
07 fffff800`00b9c730 fffff800`040c588c nt!KiInitializeKernel+0x875
08 fffff800`00b9cab0 00000000`00000000 nt!KiSystemStartup+0x19c

rcx和rdx给出了被扩展的位图和需要分配的页数:

kd> ln @rcx
Browse module
Clear breakpoint 4

(fffff800`0403c760)   nt!MiSystemPteInfo   |  (fffff800`0403cfa8)   nt!MiAdjustCounter
Exact matches:
    nt!MiSystemPteInfo = <no type information>
kd> r @rdx
rdx=0000000000000001

尽管只请求了一个页面,但位图必须被扩展,因为现在它是空的。

同样,我们在MiExpandPtes的返回地址上设置一个断点,然后继续,在更新主位图的时候断下来了:

kd> bp /t @$thread @$ra
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]

我们可以看到另一个PDE是如何被分配的,这导致位图从0x1F变为0x3F:

kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000003f

这里实际会先中断到对MiSystemPteBits的写断点上。然后恢复执行才是书上的流程。

kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]

恢复执行后,我们在MiSystemPteInfo控制的位图更新过程中断了下来。我们看到,它的缓冲区现在被映射到一个正常的物理页:


kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000003f
kd> bl
     0 e Disable Clear  fffff800`04289bb0     0001 (0001) nt!MiInitializeSystemPtes
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo
     5 e Disable Clear  fffff800`03e4f345     0001 (0001) nt!MiZeroPhysicalPage+0x521
     Match thread data fffff800`0400fcc0

kd> !pte @@(((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap.Buffer)
                                           VA fffff88000000000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200000    PTE at FFFFF6FC40000000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BDC2863  contains 000000007BE0A963
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7bdc2     ---DA--KWEV  pfn 7be0a     -G-DA--KWEV

缓冲区的大小已经增加:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0xc00
   +0x008 Buffer           : 0xfffff880`00000000  -> 0xffffffff

我们之前看到,另一个二级位图被扩展到0xAOO页。现在这个已经扩展到超过0x200页,对应于1个PDE。当我们恢复执行时,我们在MiExpandPtes的返回地址上碰到了断点,在那里我们看到二级位图的缓冲区有预期的零:

kd> dq @@(((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap.Buffer) l @@(((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))->Bitmap.SizeOfBitMap)/40
fffff880`00000000  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000010  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000020  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000030  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000040  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000050  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000060  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000070  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000080  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000090  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`000000f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000100  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000110  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000120  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000130  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00000140  00000000`00000001 00000000`00000000
fffff880`00000150  00000000`00000000 00000000`00000000
fffff880`00000160  00000000`00000000 00000000`00000000
fffff880`00000170  00000000`00000000 00000000`00000000

到+0x13F的缓冲区范围对应于由其他位图管理的页面,其所有的位都被设置。在+0x140处是最后分配的PDE的位,其中只有一个在使用,因为在调用MiExpandPtes时rdx被设置为1。和前面的调用一样,返回的rax给出了分配的PTE的地址。

kd> !pte @rax
                                           VA fffff88000a00000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200028    PTE at FFFFF6FC40005000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000007BE0B863  contains 0000000000000000
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 7be0b     ---DA--KWEV  not valid

如果我们从设定位的索引计算地址,也就是+0x140的字节的第0位,我们得到相同的值,因此系统PTE区域的偏移量是:

offset = 0x14080x1000 = 0xA00000

或者我们可以将其与从主位图分配的位的索引进行比较,主位图是第5位,对应的偏移量为:

5*0x200000 = 0xA00000

移除断点返回地址上的断点并恢复执行,我们看到一些直接从主位图分配的数据。这些可以从MiExpandPtes没有被调用和断点#2(关于主位图更新)被击中这一事实中识别出来。位被分配而没有扩展任何辅助位图。下面我们看到第一个断点以及涉及的函数:

kd> bc5
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> bl
     0 e Disable Clear  fffff800`04289bb0     0001 (0001) nt!MiInitializeSystemPtes
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo

kd> k
 # Child-SP          RetAddr           Call Site
00 fffff800`00b9c0c0 fffff800`03ec7232 nt!RtlSetBits+0x3d
01 fffff800`00b9c0f0 fffff800`0422e6ed nt!MiObtainSystemVa+0x32a
02 fffff800`00b9c1e0 fffff800`04397de5 nt!MiReserveDriverPtes+0xad
03 fffff800`00b9c220 fffff800`043b7000 nt!MiReloadBootLoadedDrivers+0x255
04 fffff800`00b9c410 fffff800`043b7bd3 nt!MiInitSystem+0xf0
05 fffff800`00b9c4a0 fffff800`043bcc25 nt!MmInitSystem+0x23
06 fffff800`00b9c4d0 fffff800`040d77b0 nt!InitBootProcessor+0x385
07 fffff800`00b9c700 fffff800`040d8045 nt!ExpInitializeExecutive+0x10
08 fffff800`00b9c730 fffff800`040c588c nt!KiInitializeKernel+0x875
09 fffff800`00b9cab0 00000000`00000000 nt!KiSystemStartup+0x19c

我们可以看到,主位图有一个额外的位设置:

kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000007f

这样持续了几次直接分配,直到主位图被设置为0x1FFF。由于现在有13位被设置,这意味着最高的分配页偏移量是

13*0x200-1 = 0x19FF

此时恢复执行,仍会多次断在2号断点

kd> bc0
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]

同时,二级位图没有变化,所以MiKernelStackPteInfo只覆盖到页面偏移量0x9FF,MiSystemPtelnfo到0xBFF。最后,再次发生扩展,这次是针对MiKernelStackPtelnfo,其中0x400页(要求2个完整的PDE)。

kd> g
Breakpoint 1 hit
nt!MiExpandPtes:
fffff800`03ec8108 48895c2408      mov     qword ptr [rsp+8],rbx
kd> bl
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo

kd> ln @rcx
Browse module
Clear breakpoint 3

(fffff800`0403bf00)   nt!MiKernelStackPteInfo   |  (fffff800`0403c750)   nt!MiPteTrackingBitmap
Exact matches:
    nt!MiKernelStackPteInfo = <no type information>
kd> r rdx
rdx=0000000000000400

当我们碰到2号断点时,我们看到在主位图中有两个位被设置:

kd> g
Breakpoint 2 hit
nt!RtlSetBits+0x3d:
fffff800`03eafbdd 488b742440      mov     rsi,qword ptr [rsp+40h]
kd> bl
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo

kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000ffff

当这个扩展开始时,MiKernelStackPtelnfo位图覆盖到页面偏移量0x9FF,这对应于其缓冲区中的字节0xl3F。另一方面,主位图记录到0xl9FF以下的页面偏移都在使用中,所以0xAOO - 0x19FF的范围被纳入位图中,并设置了相应的位。就缓冲区中的字节偏移而言,这给出了:

0xA00/8 = 0x140

0x19FF/8 = 0x33F

另外,被分配区域的起始地址将有页偏移0x1A00,在分配了两个完整的PDE后,它们将需要0x400比特或0x80字节,使最终的缓冲区大小达到

0x1A00+0x400 = 0x1E00 比特

0x400+0x80 = 0x480 字节

返回之前

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0xa00
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0xa00
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff
kd> g
Breakpoint 3 hit
nt!MiExpandPtes+0x3ca:
fffff800`03ec84d2 e99ffdffff      jmp     nt!MiExpandPtes+0x16e (fffff800`03ec8276)
kd> bl
     0 e Disable Clear  fffff800`03ec7232     0001 (0001) nt!MiObtainSystemVa+0x32a
     Match thread data fffff800`0400fcc0
     1 e Disable Clear  fffff800`03ec8108     0001 (0001) nt!MiExpandPtes
     2 e Disable Clear  fffff800`0403eb40 w 8 0001 (0001) nt!MiSystemPteBits
     3 e Disable Clear  fffff800`0403bf00 w 4 0001 (0001) nt!MiKernelStackPteInfo
     4 e Disable Clear  fffff800`0403c760 w 4 0001 (0001) nt!MiSystemPteInfo

kd> bc0
kd> dq @@(((nt!_rtl_bitmap*) @@(nt!MiSystemPteBitmap))->Buffer) l1
fffff800`0403eb40  00000000`0000ffff
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0x2000
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff

这与我们从MiExpandPtes返回时看到的情况一致:

kd> bp /t @$thread @$ra
kd> g
Breakpoint 0 hit
nt!MiBuildPagedPool+0xfb:
fffff800`0439d1fb 488bd8          mov     rbx,rax
kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0x2000
   +0x008 Buffer           : 0xfffff880`00400000  -> 0xffffffff
kd> dq @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.Buffer) l @@(((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))->Bitmap.SizeOfBitMap)/40
fffff880`00400000  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400010  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400020  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400030  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400040  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400050  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400060  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400070  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400080  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400090  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004000f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400100  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400110  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400120  ffffffff`ffffffff 00000001`ffffffff
fffff880`00400130  00000000`00000000 00000000`00000000
fffff880`00400140  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400150  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400160  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400170  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400180  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400190  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004001f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400200  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400210  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400220  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400230  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400240  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400250  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400260  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400270  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400280  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400290  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004002f0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400300  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400310  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400320  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400330  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400340  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400350  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400360  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400370  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400380  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00400390  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003a0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003b0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003c0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003d0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003e0  ffffffff`ffffffff ffffffff`ffffffff
fffff880`004003f0  ffffffff`ffffffff ffffffff`ffffffff

我们看到,在上一个位图扩展结束时,在+0x140以下,仍有一些可用的页面,但不足以满足当前对0x400的请求。扩展范围内的所有位都被设置为1,因为分配正好覆盖了2个完整的PDE。最后,MiExpandPtes返回的PTE所对应的地址是:

kd> !pte @rax
                                           VA fffff88001c00000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200070    PTE at FFFFF6FC4000E000
contains 000000007BE04863  contains 000000007BDC3863  contains 000000000489C863  contains 0000000000000000
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 489c      ---DA--KWEV  not valid

这证实了二级位图的扩展可以在主位图的直接分配之后进行。位图被扩展到包括所有使用中的范围和新分配的范围,并被返回给调用者。

##### 位索引和虚拟地址之间的关系(MiSystemPteInfo)

本节进一步证明了MiSystemPtelnfo中的位索引代表了系统PTE区域的页面偏移。

为了捕捉这里的数据,我们在位图缓冲区上使用了一个数据断点。这种方法的问题是需要提前知道缓冲区中的哪些位将被设置以响应位图的分配,以便将数据断点放在正确的QWORD上。这个问题已经通过查看_MI_STSTEN_PTE_TYPE.Hint解决了。这似乎是VMM开始在位图中寻找空闲页的索引。

在这个测试中,我们将使用MemTests来调用MmMapLockedPagesSpecifyCache,它从MiSystemPteInfo中分配。

首先,我们分配一个4页的私有缓冲区,

Untitled 3.png

然后,从系统范围测试菜单中,我们为它初始化一个MDL,并探测和锁定其页面。

Untitled 4.png

最后,我们在MDL上调用MmMapLockePagesSpecifyCache,请求一个内核模式的映射。

当MemTests要调用驱动程序时,它会显示一个提示,我们可以输入b来进入调试器。用户态调试器和内核态调试器同时存在的情况下,DebugBreak调用会让vs断下来直接处理。

当调试器控制时,当前线程是MemTests线程,所以我们可以在MmMapLockedPagesSpecifyCache上放置一个断点,然后继续执行

kd> bp /t @$thread nt!MmMapLockedPagesSpecifyCache
kd> bl
     0 e Disable Clear  fffff800`03eaa7a0     0001 (0001) nt!MmMapLockedPagesSpecifyCache
     Match thread data fffffa80`01e53b60
     1 d Enable Clear  fffff880`04b80080     0001 (0001) +0x1080

kd> g
[Log]: MmMapLockedPagesSpecifyCacheTest
Breakpoint 0 hit
nt!MmMapLockedPagesSpecifyCache:
fffff800`03eaa7a0 488bc4          mov     rax,rsp

然后我们从MiSystemPtelnfo中提取hint:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403c760
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 3
   +0x014 Hint             : 0xea40
   +0x018 BasePte          : 0xfffff6fc`40000000 _MMPTE
   +0x020 FailureCount     : 0xfffff800`0403c79c  -> 0
   +0x028 Vm               : 0xfffff800`04013ec0 _MMSUPPORT
   +0x030 TotalSystemPtes  : 0n31232
   +0x034 TotalFreeSystemPtes : 0n10509
   +0x038 CachedPteCount   : 0n32
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null)

然后我们确认一下hint指向的位是clear的:

kd> dq fffff880`00000000+ea40/8
fffff880`00001d48  00000000`00000000 00000000`0000ffff
fffff880`00001d58  00000000`00000000 00000000`00000000
fffff880`00001d68  fe000000`00000000 ffff0000`0003ff0f
fffff880`00001d78  00380000`00000000 ffffffff`ffffffff
fffff880`00001d88  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001d98  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001da8  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001db8  ffffffff`ffffffff ffffffff`ffffffff

接下来我们在这个位置上下一个写入断点,然后恢复执行:

kd> ba w 8 /t @$thread fffff880`00000000+ea40/8
kd> g
Breakpoint 1 hit
nt!MmMapLockedPagesSpecifyCache+0x28a:
fffff800`03eaaa25 0f854a070000    jne     nt!MmMapLockedPagesSpecifyCache+0x9d5 (fffff800`03eab175)
kd> k
 # Child-SP          RetAddr           Call Site
00 fffff880`0358a740 fffff880`04b8b3df nt!MmMapLockedPagesSpecifyCache+0x28a
01 fffff880`0358a810 fffff880`04b8a1c7 krnlAllocs!MmMapLockedPagesSpecifyCacheTest+0x13f [D:\MicrosoftOs\WmipCodeSamples\KrnlAllocs\KrnlAllocs.cpp @ 408] 
02 fffff880`0358a880 fffff800`041a9f97 krnlAllocs!DriverDeviceControl+0x147 [D:\MicrosoftOs\WmipCodeSamples\KrnlAllocs\DrvMain.cpp @ 149] 
03 fffff880`0358a8d0 fffff800`041aa7f6 nt!IopXxxControlFile+0x607
04 fffff880`0358aa00 fffff800`03e8e8d3 nt!NtDeviceIoControlFile+0x56
05 fffff880`0358aa70 00000000`779e138a nt!KiSystemServiceCopyEnd+0x13
06 00000000`0013f808 000007fe`fdbcb939 ntdll!ZwDeviceIoControlFile+0xa
07 00000000`0013f810 00000000`7788683f KERNELBASE!DeviceIoControl+0x75
*** WARNING: Unable to verify checksum for MemTests.exe
08 00000000`0013f880 00000001`3f7a5bd3 kernel32!DeviceIoControlImplementation+0x7f
09 (Inline Function) --------`-------- MemTests!SendIoCtl+0x3a [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2483] 
0a 00000000`0013f8d0 00000001`3f7a78af MemTests!MmMapLockedPagesSpecifyCacheTest+0x463 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 1851] 
0b 00000000`0013f970 00000001`3f7a74b7 MemTests!SRSChoice+0x5f [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2532] 
0c (Inline Function) --------`-------- MemTests!SystemRangeSubmenu+0x11d [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2620] 
0d 00000000`0013fa10 00000001`3f7a13f2 MemTests!ProcessOption+0x507 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2379] 
0e 00000000`0013fa70 00000001`3f7a8e9c MemTests!main+0x1e2 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 74] 
0f (Inline Function) --------`-------- MemTests!invoke_main+0x22 [d:\a01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
10 00000000`0013faa0 00000000`7788652d MemTests!__scrt_common_main_seh+0x10c [d:\a01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
11 00000000`0013fae0 00000000`779bc521 kernel32!BaseThreadInitThunk+0xd
12 00000000`0013fb10 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

断点命中之后,位图发生了改变:

kd> dq fffff880`00000000+ea40/8
fffff880`00001d48  00000000`00000007 00000000`0000ffff
fffff880`00001d58  00000000`00000000 00000000`00000000
fffff880`00001d68  fe000000`00000000 ffff0000`0003ff0f
fffff880`00001d78  00380000`00000000 ffffffff`ffffffff
fffff880`00001d88  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001d98  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001da8  ffffffff`ffffffff ffffffff`ffffffff
fffff880`00001db8  ffffffff`ffffffff ffffffff`ffffffff

第0,1,2位现在被置1,和我们要锁定的页面一样多。hint所指向的位的字节偏移量由以下公式给出

kd> ? ea40%8
Evaluate expression: 0 = 00000000`00000000

也就是说从第0位开始置1。

如果我们假设位图中的每个位代表系统PTE区域中的一个页面,那么与第一个位置1相对应的地址是

kd> ? ea40*1000+fffff880`00000000
Evaluate expression: -8246091579392 = fffff880`0ea40000

现在,这个地址和接下来的两个页的PTEs是无效的:

kd> !pte ea40*1000+fffff880`00000000
                                           VA fffff8800ea40000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E2003A8    PTE at FFFFF6FC40075200
contains 000000007BE04863  contains 000000007BDC3863  contains 0000000035FFA863  contains 0000000000000000
pfn 7be04     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 35ffa     ---DA--KWEV  not valid

kd> dq FFFFF6FC40075200 l4
fffff6fc`40075200  00000000`00000000 00000000`00000000
fffff6fc`40075210  00000000`00000000 00000000`00000000

现在我们让MmMapLockedPagesSpecifyCache执行完成,看看情况如何变化:

kd> g @$ra
krnlAllocs!MmMapLockedPagesSpecifyCacheTest+0x13f:
fffff880`04b8b3df 4889442448      mov     qword ptr [rsp+48h],rax
kd> r
rax=fffff8800ea40000 rbx=fffffa800383af20 rcx=0000000000000001
rdx=0000000000000000 rsi=fffffa800383af20 rdi=fffffa8002e60700
rip=fffff88004b8b3df rsp=fffff8800358a810 rbp=fffff8800358ab60
 r8=0000000069d43000  r9=0000000000000000 r10=0000000000000000
r11=0000fffffffff000 r12=0000000000000008 r13=0000000000000001
r14=0000000000000001 r15=fffffa8003311e20
iopl=0         nv up ei ng nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000282
krnlAllocs!MmMapLockedPagesSpecifyCacheTest+0x13f:
fffff880`04b8b3df 4889442448      mov     qword ptr [rsp+48h],rax ss:0018:fffff880`0358a858=0000000020206f49

rax中返回的地址是由hint计算出来的。这证实了一个位的索引和系统PTE区域之间的关系。此外,PTEs现在正在映射有效的地址:

kd> dq FFFFF6FC40075200 l4
fffff6fc`40075200  00000000`3a640963 00000000`46501963
fffff6fc`40075210  00000000`2f842963 00000000`00000000

从打印到控制台的信息中,我们看到MDL的地址是0xFFFFFA80`0354c490。从开始的+0x30处是由MDL映射的PFN数组,结果是

kd> dq 0xFFFFFA80`0354c490+30 l 4
fffffa80`0354c4c0  00000000`0003a640 00000000`00046501
fffffa80`0354c4d0  00000000`0002f842 00000000`0007f495

即PTEs所引用的PFN。

最后我们可以看到位图的hint增加了3个单位

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiSystemPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`0403c760
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 3
   +0x014 Hint             : 0xea43
   +0x018 BasePte          : 0xfffff6fc`40000000 _MMPTE
   +0x020 FailureCount     : 0xfffff800`0403c79c  -> 0
   +0x028 Vm               : 0xfffff800`04013ec0 _MMSUPPORT
   +0x030 TotalSystemPtes  : 0n31232
   +0x034 TotalFreeSystemPtes : 0n10506
   +0x038 CachedPteCount   : 0n32
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null)

最后一个有趣的事实是这些位是如何被清除的:我们可以选择调用MmUnmapLockedPages的MemTests选项,在调用驱动程序之前再次进入调试器。通过在即将被清除的位上再次设置数据断点,我们发现不是执行MmUnmapLockedPages的线程在更新这些位。相反,断点是在调用DDI完成后异步击中的,这就是更新它们的线程。

kd> ba w 8 fffff880`00000000+ea40/8
kd> g
Breakpoint 2 hit
nt!MiReplenishBitMap+0x11e:
fffff800`03ec5f18 418d8432ff010000 lea     eax,[r10+rsi+1FFh]
kd> k
 # Child-SP          RetAddr           Call Site
00 fffff880`047b38a0 fffff800`03e95444 nt!MiReplenishBitMap+0x11e
01 fffff880`047b39b0 fffff800`03e7aae9 nt!MiEmptyPteBins+0x10d
02 fffff880`047b3a00 fffff800`03e7a1d8 nt!MiAdjustPteBins+0x29
03 fffff880`047b3a40 fffff800`03e7a6c3 nt!MmWorkingSetManager+0x40
04 fffff880`047b3a90 fffff800`0412ccce nt!KeBalanceSetManager+0x1c3
05 fffff880`047b3c00 fffff800`03e80fe6 nt!PspSystemThreadStartup+0x5a
06 fffff880`047b3c40 00000000`00000000 nt!KiStartSystemThread+0x16

工作集管理器实际上正在做这项工作。据推测,发生这种情况是因为当系统虚拟地址被释放时,相应的TLB条目必须在所有处理器上失效。由于这是一个昂贵的操作,所以分批进行是有意义的,看来WSM就是用于这个目的。

##### 位索引和虚拟地址之间的关系 (MiKernelStackPteInfo)

我们可以进行一个与前面类似的测试,以确认这个位图的位索引和虚拟地址之间的关系。

为此我们将使用MemTests来调用MmAllocateMappingAddress DDI,事实证明,它是从这个位图分配的。这个DDI并没有从MDL中映射物理页,而只是预定了一个地址范围,以后可以用其他函数来使用。有趣的是,这个DDI的函数签名是

PVOID MmAllocateMappingAddress(
  [in] SIZE_T NumberOfBytes,
  [in] ULONG  PoolTag
);

第二个参数很特别,因为保留一个虚拟地址范围与系统池没有关系。在执行这个测试时,我们将看到标签被VMM用来标记预定的范围。这个信息在释放范围时被使用,以检查这是否正确完成。

为了释放这样的范围,需要调用的DDI是MmFreeMappingAddress,其签名如下:

void MmFreeMappingAddress(
  [in] PVOID BaseAddress,
  [in] ULONG PoolTag
);

这个函数检查输入的地址是否是用PoolTag参数中传递的相同标签标记的范围的开始。如果不是这样,DDI会以代码0xDA:SYSTEM_PTE_MISUSE进行错误检查。

对于这个测试,我们在MemTests的System Range Tests菜单中选择MmAllocateMappingAddress()测试,size  = 0x10000。并在调用驱动程序之前进入调试器。当调试器取得控制权时,当前线程就是将调用DDI的线程。我们期望MiReservePtes被调用来执行分配,所以我们在仅对该线程有效的函数地址处放置一个断点,然后恢复执行。

kd> bp /t @$thread nt!MiReservePtes
kd> g
Breakpoint 0 hit
nt!MiReservePtes:
fffff800`03ebe9d8 4489442418      mov     dword ptr [rsp+18h],r8d

正如我们从驱动信息中看到的,在这个测试中,我们保留了0x10个页面。寄存器的值告诉我们一些有趣的事情。

kd> r
rax=0000000000000010 rbx=fffffa8003578b70 rcx=fffff80004088f00
rdx=0000000000000012 rsi=0000000000000010 rdi=000000004b415453
rip=fffff80003ebe9d8 rsp=fffff88002d557a8 rbp=fffff88002d55b60
 r8=0000000000000000  r9=fffff88003f95bf0 r10=fffff8000404f9a0
r11=fffffa800371e880 r12=0000000000000010 r13=0000000000000001
r14=0000000000000001 r15=fffffa8003547060
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!MiReservePtes:
fffff800`03ebe9d8 4489442418      mov     dword ptr [rsp+18h],r8d ss:0018:fffff880`02d557c0=02d55838

首先rcx是MiKernelStackPtelnfo的地址

kd> ln @rcx
Browse module
Set bu breakpoint

(fffff800`04088f00)   nt!MiKernelStackPteInfo   |  (fffff800`04089750)   nt!MiPteTrackingBitmap
Exact matches:
    nt!MiKernelStackPteInfo = <no type information>

这表明我们即将从相应的位图中分配数据,我们将用位图本身的数据中断点来确认这一事实。

第二,rdx被设置为我们所预定的分页数+2。正如我们将看到的,MmAllocateMappingAddress的实现确实保留了两个额外的页面,并将它们的PTEs用于自己:第一个将包含预定的页面数量,第二个是输入标签的修改版本。

继续我们的测试,这里是位图的状态:

kd> ?? ((nt!_mi_system_pte_type*) @@(nt!MiKernelStackPteInfo))
struct _MI_SYSTEM_PTE_TYPE * 0xfffff800`04088f00
   +0x000 Bitmap           : _RTL_BITMAP
   +0x010 Flags            : 3
   +0x014 Hint             : 0x42bd
   +0x018 BasePte          : 0xfffff6fc`40000000 _MMPTE
   +0x020 FailureCount     : 0xfffff800`04088f3c  -> 0
   +0x028 Vm               : 0xfffff800`04060ec0 _MMSUPPORT
   +0x030 TotalSystemPtes  : 0n9216
   +0x034 TotalFreeSystemPtes : 0n2155
   +0x038 CachedPteCount   : 0n0
   +0x03c PteFailures      : 0
   +0x040 SpinLock         : 0
   +0x040 GlobalMutex      : (null)

而下面是Hint给出的索引处的位图内容:

kd> dq 0xfffff88000400000+42bd/8 l1
fffff880`00400857  ffffffff`ffffffff
kd> ? 42bd%8
Evaluate expression: 5 = 00000000`00000005

这是一个有趣的例子,因为hint指向0xfffff880`00400857字节内的第0位,该位被设置为0xFF。这意味着没有任何清零位被发现,全为1。由于我们请求的是0x10(实际上是0x12,原因我们看到了)的页面,所以不能使用hint所对应的范围。

由于我们想把数据断点放在我们期望被更新的字节上,我们将猜测VMM将使用寻找下一个字节开始的空闲范围,

kd> dq 0xfffff88000400000+42bd/8 l2
fffff880`00400857  ffffffff`ffffffff e0000000`007fffff

可以发现0x7F后的字节有足够的位数存放0x12个页面。因此我们这样下断,注意,需要地址进行对齐。

kd> ? 0xfffff88000400000+42bd/8+8
Evaluate expression: -8246333011873 = fffff880`0040085f
kd> ? 0xfffff88000400000+42bd/8+8+1
Evaluate expression: -8246333011872 = fffff880`00400860
kd> ba w 8 /t @$thread 0xfffff88000400000+42bd/8+8+1

恢复执行后看看发生了什么

kd> g $ra
Breakpoint 1 hit
nt!MiReservePtes+0x3e6:
fffff800`03ebedbe 0f84d0fdffff    je      nt!MiReservePtes+0x1bc (fffff800`03ebeb94)
kd> dq 0xfffff88000400000+42bd/8 l2
fffff880`00400857  ffffffff`ffffffff e00001ff`ffffffff

下面是调用栈:

kd> k
 # Child-SP          RetAddr           Call Site
00 fffff880`02d55710 fffff800`041ffbd0 nt!MiReservePtes+0x3e6
01 fffff880`02d557b0 fffff880`03f94e5d nt!MmAllocateMappingAddress+0x48
02 fffff880`02d55830 fffff880`03f94120 krnlAllocs!MmAllocateMappingAddressTest+0xcd [D:\MicrosoftOs\WmipCodeSamples\KrnlAllocs\KrnlAllocs.cpp @ 261] 
03 fffff880`02d55880 fffff800`041f6f97 krnlAllocs!DriverDeviceControl+0xa0 [D:\MicrosoftOs\WmipCodeSamples\KrnlAllocs\DrvMain.cpp @ 114] 
04 fffff880`02d558d0 fffff800`041f77f6 nt!IopXxxControlFile+0x607
05 fffff880`02d55a00 fffff800`03edb8d3 nt!NtDeviceIoControlFile+0x56
06 fffff880`02d55a70 00000000`7714138a nt!KiSystemServiceCopyEnd+0x13
07 00000000`0031fb78 000007fe`fd23b939 ntdll!ZwDeviceIoControlFile+0xa
08 00000000`0031fb80 00000000`76ee683f KERNELBASE!DeviceIoControl+0x75
09 00000000`0031fbf0 00000001`3f294c74 kernel32!DeviceIoControlImplementation+0x7f
0a (Inline Function) --------`-------- MemTests!SendIoCtl+0x33 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2483] 
0b 00000000`0031fc40 00000001`3f2978a5 MemTests!MmAllocateMappingAddressTest+0x184 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 1613] 
0c 00000000`0031fcd0 00000001`3f2974b7 MemTests!SRSChoice+0x55 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2529] 
0d (Inline Function) --------`-------- MemTests!SystemRangeSubmenu+0x11d [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2620] 
0e 00000000`0031fd70 00000001`3f2913f2 MemTests!ProcessOption+0x507 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 2379] 
0f 00000000`0031fdd0 00000001`3f298e9c MemTests!main+0x1e2 [D:\MicrosoftOs\WmipCodeSamples\MemTests\MemTests.cpp @ 74] 
10 (Inline Function) --------`-------- MemTests!invoke_main+0x22 [d:\a01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
11 00000000`0031fe00 00000000`76ee652d MemTests!__scrt_common_main_seh+0x10c [d:\a01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
12 00000000`0031fe40 00000000`7711c521 kernel32!BaseThreadInitThunk+0xd
13 00000000`0031fe70 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

下面的位被置1了:

  • 0x42bd/8+8+2的第7位
  • 下两个字节的所有位
  • 这个字节的第0位

总的位数是

1+8+8+1 = 18 = 0x12

和我们预期的是一样的。

这里的计算和书上情况不太一样,故不做分析。

我们现在还在MiReservePtes函数内,对应的PTEs是空闲的:

我们返回到测试驱动,然后查看得到的地址范围。

kd> r
rax=fffff88004311000 rbx=fffffa8003578b70 rcx=0000000000000000
rdx=0000000000000012 rsi=fffffa8003578b70 rdi=fffffa80035c0980
rip=fffff88003f94e5d rsp=fffff88002d55830 rbp=fffff88002d55b60
 r8=000000004b415453  r9=0000000000000001 r10=0000000000000000
r11=fffff88002d55820 r12=0000000000000010 r13=0000000000000001
r14=0000000000000001 r15=fffffa8003547060
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
krnlAllocs!MmAllocateMappingAddressTest+0xcd:
fffff880`03f94e5d 488b4c2428      mov     rcx,qword ptr [rsp+28h] ss:0018:fffff880`02d55858=fffffa800371e880

返回的地址与前面计算的地址相比,是+2个页面。让我们来看看后者的PTEs。

kd> !pte fffff88004311000-0x2000
                                           VA fffff8800430f000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200108    PTE at FFFFF6FC40021878
contains 000000007BDC4863  contains 000000007BD83863  contains 000000000D02D863  contains 0000001200000000
pfn 7bdc4     ---DA--KWEV  pfn 7bd83     ---DA--KWEV  pfn d02d      ---DA--KWEV  not valid
                                                                                  Page has been freed
kd> dq FFFFF6FC40021878 l2
fffff6fc`40021878  00000012`00000000 00000000`4b415012

第一个PTE在上部的DWORD中存储0x12,即整体范围的长度,第二个PTE与标签有关。测试驱动设置标签的方法如下:

#define MAP_REGION_TAG 'KATS'

可以看到0x4b, 0x41出现在了PTE里。

第一个PTE中的范围长度特别有趣,因为它解释了为什么MmFreeMappingAddress的参数中没有要释放的范围的长度:DDI从隐藏的PTE中读取它,在输入地址的PTE之前有两个PTE。

这个分析证实了由MiKernelStackPtelnfo控制的位图的索引对应于从System PTE区域开始的一个页偏移。这一节和上一节表明,两个位图的索引具有相同的意义。

2.8.3 页面错误处理

在这个区域的页面故障的处理,如第1.3节所解释。。使用的 _MMSUPPORT 实例是 MmSystemPtesWs 的静态实例。

2.8.4 MmSystemPtesWs: 仅用于内核模式镜像的工作集

我们已经提到MmSystemPtesWs.WorkingSetSize等于MmSystemDriverPage和MmSystemCodePage之和。

这两个统计值的含义在资料有解释:内核和内核模式的驱动程序都有可分页sections,因此可以不被映射。这些计数器分别存储了当前用于驱动和内核的映射虚拟页的物理页数量。

因此存储在MmSystemPtes中的工作集大小只占映射内核模式图像的页面。

但是系统PTE区域的其他可能种类的内容呢?它们不是任何工作集的一部分,因为它们不能被工作集管理器分页。让我们一个一个地分析它们。

#### MDL映射

这些VA用于 "查看 "由MDL追踪的物理页,这些物理页在被映射之前必须被锁定在内存中,所以它们是不可分页的。

下面的例子通过分析用户模式缓冲区的MDL映射,进一步阐述了这个观点。

缓冲区被MemTests分配在0x6b0000,大小为1MB,延伸到0x7affff。

0x6b0000
1048576
m
k
b
kd> !process 0 0 MemTests.exe
PROCESS fffffa8003440290
    SessionId: 1  Cid: 0610    Peb: 7fffffdf000  ParentCid: 09e0
    DirBase: 5975e000  ObjectTable: fffff8a002a98b40  HandleCount:  11.
    Image: MemTests.exe

kd> .process /i fffffa8003440290
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`03ede490 cc              int     3
kd> !vad 0x6b0000
VAD           Level     Start       End Commit
fffffa8001c0b170  5       6b0       7af    256 Private      READWRITE

我们为缓冲区创建一个MDL,锁定页面并将其映射到系统PTE区域,最后在0xFFFFF88007B00000 - 0xFFFFF88007BFFFFF范围内。下面是第一页的PTE和_MMPFN

kd> !pte 0xFFFFF880`07B00000
                                           VA fffff88007b00000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E2001E8    PTE at FFFFF6FC4003D800
contains 000000007BDC4863  contains 000000007BDC3863  contains 0000000079E45863  contains 0000000054551963
pfn 7bdc4     ---DA--KWEV  pfn 7bdc3     ---DA--KWEV  pfn 79e45     ---DA--KWEV  pfn 54551     -G-DA--KWEV
kd> !pfn 54551
    PFN 00054551 at address FFFFFA8000FCFF30
    flink       000002E8  blink / share count 00000001  pteaddress FFFFF68000003580
    reference count 0002    used entry count  0000      Cached    color 0   Priority 5
    restore pte 00000080  containing page 0596BA  Active     M       
    Modified

我们看到_MMPFN中的pteaddress(_MMPFN.PteAddress)与系统PTE范围的PTE地址有很大的不同。一开始这个物理页最初是为用户模式缓冲区分配的,所以_MMPFN的内容与进程地址空间中的页面使用情况是一致的。我们来看看用户模式地址的PTE。

kd> .process /P fffffa8003440290
Implicit process is now fffffa80`03440290
.cache forcedecodeptes done
kd> !pte 6b0000
                                           VA 00000000006b0000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000018    PTE at FFFFF68000003580
contains 02D0000059A0F867  contains 0120000059A92867  contains 2DC00000596BA867  contains AE80000054551867
pfn 59a0f     ---DA--UWEV  pfn 59a92     ---DA--UWEV  pfn 596ba     ---DA--UWEV  pfn 54551     ---DA--UW-V

用户模式地址的PTE的地址等于_MMPFN中找到的,而且PTE也是指向正确的物理页。

我们注意到的第二件事是,_MMPFN确实有一个工作集索引(flink是0x2e8),但这不应该让我们吃惊:这是进程工作集中的索引。我们可以用调试器确认这一点。

kd> ?? ((nt!_MMWSL*) 0xfffff70001080000)->Wsle[0x2e8].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 : 0y0000000000000000000000000000000000000000011010110000 (0x6b0)

但这个物理页面是否也是系统PTE工作集的一部分呢?让我们看看后者的相同项。

kd> ?? ((nt!_MMSUPPORT*) @@(nt!MmSystemPtesWs))->VmWorkingSetList->Wsle[0x2e8].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 : 0y1111111111111111111110001000000000000001000111000100 (0xfffff880011c4)

这个条目显然既不是指用户模式的VA,也不是指物理页被映射的系统PTE的VA,所以它是一个其他物理页的项。

我们知道如果页面是共享的,某个VA的WSL项可以与_MMPFN中的索引不同。但这是通过为工作集分配一个哈希表来管理的,我们已经知道,系统PTE WS没有哈希表。

原则上系统PTE WS不考虑MDL映射,因为物理页已经是另一个WS的一部分,而且试图对它们进行分页是没有意义的:它们只有在先前被锁定后才会被映射。

这里描述的测试可以用MemTests来复现,它有分配MDL和锁定及映射其页面的菜单选项。

#### 内核栈

内核堆栈的页面是以一种完全特殊的方式分页的,所以它们不是任何工作集的一部分。

#### 设备内存映射

这些VA甚至没有被映射到物理页面,所以为它们创建工作集项没有任何意义。

#### _MM_SESSION_SPACE实例

_MM_SESSION空间的实例被映射到系统PTE区域。_MM_SESSION空间的实例被映射到系统PTE区域中。存储这些实例的页面似乎不属于任何工作集,因为它们的_MMPFN.u1被设置为映射到它们的VA。

#### 总结

在系统PTE区域中发现的各种内容中,只有驱动代码在MmSystemPtesWs中得到了说明。

最后一个点是,MmSystemPtesWs.LowestPagableAddress指向系统PTE区域的下方,即初始加载器映射区域的开始,该区域存储了在系统启动初期加载的内核模式映像(例如,内核和HAL)。因此,这个工作集也跟踪了这些镜像的分页,严格来说,这些镜像是在系统PTE区域之外的。总之,这个工作集的工作似乎是跟踪所有内核模式映像的可分页物理页,包括初始加载器映射和系统PTE区域。

2.9 分页池

2.9.1 区域内容

这是一个动态分配的区域。

分页池(PP)是一种供内核模式组件使用的可分页内存堆。执行体提供了DDI,以便从它那里分配和释放内存,如ExAllocatePool和ExFreePool。

分页池的上限存储在静态的MmPagedPoolEnd,通常设置为0xFFFFF8BF'FFFFFFF,所以池的虚拟范围有128GB的大小。从0xFFFFF8C0'000000到下一个区域(会话空间在0xFFFFF900'000000)的范围似乎是未使用的,因为映射它的PDPTEs被设置为0。 这个未使用的范围跨越256GB。

VMM定义了一个以上的PP,以减少不同线程之间对用于管理池数据结构的争夺。ExAllocatePool的调用者看不到不同的PP,它只是从PP区域的 "某个地方 "返回一个内存块。不同的内存池也被分配在映射到不同的NUMA节点的虚拟范围内,这样内存池的分配就会在节点之间平均分配。

2.9.2 虚拟内存管理

这个区域的虚拟地址空间由MiPagedPoolVaBitmap的位图控制。当请求MiVaPagedPool内存类型时,MiObtainSystemVa会从这个区域进行分配。

2.9.3 页面错误处理

页面错误处理使用名为MmPagedPoolWs的静态_MMSUPPORT实例作为工作集。

2.9.4 页面置出和重新利用

PP页面的分页和再利用就像用户模式的页面一样:工作集管理器从PP WS中删除页面,当它们脏了的时候,就把它们移到修改过的列表中。Modified页面写入器将它们的内容输出到分页文件中,并将它们移到Standby列表中,在那里它们最终被重新利用。

本节展示了一个调试会话的节选,通过在_MMPFN实例和WSLE上设置断点来跟踪PP页面的转换。

进行分析的页面是映射了分页池的第一个VPN的页面。这是初始情况,页面主动映射了VPN。

kd> !pte fffff8a000000000
                                           VA fffff8a000000000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1400    PDE at FFFFF6FB7E280000    PTE at FFFFF6FC50000000
contains 000000007BDC4863  contains 0000000004A69863  contains 0000000004A68863  contains 8010000004A6A963
pfn 7bdc4     ---DA--KWEV  pfn 4a69      ---DA--KWEV  pfn 4a68      ---DA--KWEV  pfn 4a6a      -G-DA--KW-V

kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000001  blink / share count 00000001  pteaddress FFFFF6FC50000000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Active     M       
    Modified

0x1是该页面的WSL项的索引,所以我们可以查看其内容。

kd> ?? ((nt!_mmsupport*) @@(nt!MmPagedPoolWs))->VmWorkingSetList->Wsle[1].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y1
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y0
   +0x000 Direct           : 0y1
   +0x000 Protection       : 0y00000 (0)
   +0x000 Age              : 0y111
   +0x000 VirtualPageNumber : 0y1111111111111111111110001010000000000000000000000000 (0xfffff8a000000)

到目前为止,这个页面是PP WS的一部分,正在映射VA。为了跟踪该页的生命,我们使用了两个数据断点:一个在_MMPFN内部的+0x10处,即在_MMPFN.PteAddress。这个成员的第0位用于锁定实例独占访问,所以对实例内容的大多数重大改变都是通过访问这个成员来设置和清除锁实现的,。第二个数据断点是在WSL项上。

多个MemTests的实例正在运行,有的访问0x3c0000字节的私有区域,有的访问0x20000000的映射文件。

该系统是一个拥有2GB内存的虚拟机,因此正在运行的程序使用了大量的内存,并迫使系统交换出分页池页面。

在下文中,断点被击中的地址是执行数据访问的指令之后的地址。这是正常的行为,因为当这种断点被击中时,处理器会产生一个陷阱帧。

有趣的断点是在在MiFreeWsleList+0x4b1处对_MMPFN.PteAddress的访问. ,以锁定该实例。

kd> g
Breakpoint 0 hit
nt!MiAgeWorkingSet+0x4fc:
fffff800`03ed0f86 0f82948c0700    jb      nt! ?? ::FNODOBFM::`string'+0x228f4 (fffff800`03f49c20)
kd> !thread @$thread
THREAD fffffa800190f530  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa80018ea040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      867712         Ticks: 1 (0:00:00:00.015)
Context Switch Count      15360          IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.265
Win32 Start Address nt!KeBalanceSetManager (0xfffff80003ed1500)
Stack Init fffff880047b3c70 Current fffff880047b3700
Base fffff880047b4000 Limit fffff880047ae000 Call 0000000000000000
Priority 16 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`047b3810 fffff800`03f5f685 : fffff800`0406abc0 fffff800`00000001 00000000`00000001 fffff880`047b3a70 : nt!MiAgeWorkingSet+0x4fc
fffff880`047b39c0 fffff800`03ed1206 : 00000000`000034cb 00000000`00000000 fffffa80`00000000 00000000`00000006 : nt! ?? ::FNODOBFM::`string'+0x4d886
fffff880`047b3a40 fffff800`03ed16c3 : 00000000`00000008 fffff880`047b3ad0 00000000`00000001 fffffa80`00000000 : nt!MmWorkingSetManager+0x6e
fffff880`047b3a90 fffff800`04183cce : fffffa80`0190f530 00000000`00000080 fffffa80`018ea040 00000000`00000001 : nt!KeBalanceSetManager+0x1c3
fffff880`047b3c00 fffff800`03ed7fe6 : fffff800`04058e80 fffffa80`0190f530 fffffa80`0190fa10 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`047b3c40 00000000`00000000 : fffff880`047b4000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

kd> ub
nt!MiAgeWorkingSet+0x4d6:
fffff800`03ed0f60 ff6641          jmp     qword ptr [rsi+41h]
fffff800`03ed0f63 837e1801        cmp     dword ptr [rsi+18h],1
fffff800`03ed0f67 0f8450fdffff    je      nt!MiAgeWorkingSet+0x233 (fffff800`03ed0cbd)
fffff800`03ed0f6d e9cdfdffff      jmp     nt!MiAgeWorkingSet+0x2b5 (fffff800`03ed0d3f)
fffff800`03ed0f72 440f20c5        mov     rbp,cr8
fffff800`03ed0f76 b802000000      mov     eax,2
fffff800`03ed0f7b 440f22c0        mov     cr8,rax
fffff800`03ed0f7f f0410fba6e1000  lock bts dword ptr [r14+10h],0
kd> g
Breakpoint 0 hit
nt!MiAgeWorkingSet+0x51e:
fffff800`03ed0fa8 400fb6c5        movzx   eax,bpl
kd> g
Breakpoint 0 hit
nt!MiFreeWsleList+0x4b1:
fffff800`03ecf7cd 0f826f61fcff    jb      nt! ?? ::FNODOBFM::`string'+0x21110 (fffff800`03e95942)

下面是断点处的调用堆栈,显示运行线程是工作集管理器:

kd> !thread @$thread
THREAD fffffa800190f530  Cid 0004.005c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa80018ea040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      867779         Ticks: 61 (0:00:00:00.951)
Context Switch Count      15368          IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.405
Win32 Start Address nt!KeBalanceSetManager (0xfffff80003ed1500)
Stack Init fffff880047b3c70 Current fffff880047b35c0
Base fffff880047b4000 Limit fffff880047ae000 Call 0000000000000000
Priority 16 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`047b3720 fffff800`03faeebf : fffff800`0406abc0 fffff880`047b3930 fffffa80`00000000 fffff800`00000000 : nt!MiFreeWsleList+0x4b1
fffff880`047b3910 fffff800`03f5f63d : 00000000`0000001b fffff800`000070e3 00000000`00000001 00000000`00000000 : nt!MiTrimWorkingSet+0x14f
fffff880`047b39c0 fffff800`03ed1206 : 00000000`000034cd 00000000`00000000 00000000`00000000 00000000`00000000 : nt! ?? ::FNODOBFM::`string'+0x4d834
fffff880`047b3a40 fffff800`03ed16c3 : 00000000`00000008 fffff880`047b3ad0 00000000`00000001 fffffa80`00000000 : nt!MmWorkingSetManager+0x6e
fffff880`047b3a90 fffff800`04183cce : fffffa80`0190f530 00000000`00000080 fffffa80`018ea040 00000000`00000001 : nt!KeBalanceSetManager+0x1c3
fffff880`047b3c00 fffff800`03ed7fe6 : fffff800`04058e80 fffffa80`0190f530 fffffa80`0190fa10 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`047b3c40 00000000`00000000 : fffff880`047b4000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

之后不久,再次断下,_MMPFN实例被解锁。_MMPFN还没有被更新,但是PTE现在正在过渡中。

kd> g
Breakpoint 0 hit
nt!MiFreeWsleList+0x4d4:
fffff800`03ecf7f0 410fb6c5        movzx   eax,r13b
kd> ub
nt!MiFreeWsleList+0x4a7:
fffff800`03ecf7c3 450f22c1        mov     cr8,r9
fffff800`03ecf7c7 f00fba6d1000    lock bts dword ptr [rbp+10h],0
fffff800`03ecf7cd 0f826f61fcff    jb      nt! ?? ::FNODOBFM::`string'+0x21110 (fffff800`03e95942)
fffff800`03ecf7d3 488bb424f0010000 mov     rsi,qword ptr [rsp+1F0h]
fffff800`03ecf7db 6644397d18      cmp     word ptr [rbp+18h],r15w
fffff800`03ecf7e0 0f8654fcffff    jbe     nt!MiFreeWsleList+0x11e (fffff800`03ecf43a)
fffff800`03ecf7e6 e9a961fcff      jmp     nt! ?? ::FNODOBFM::`string'+0x2116d (fffff800`03e95994)
fffff800`03ecf7eb f0836510fe      lock and dword ptr [rbp+10h],0FFFFFFFEh
kd> !pte fffff8a000000000
                                           VA fffff8a000000000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1400    PDE at FFFFF6FB7E280000    PTE at FFFFF6FC50000000
contains 000000007BDC4863  contains 0000000004A69863  contains 0000000004A68863  contains 8010000004A6A882
pfn 7bdc4     ---DA--KWEV  pfn 4a69      ---DA--KWEV  pfn 4a68      ---DA--KWEV  not valid
                                                                                  Transition: 4a6a
                                                                                  Protect: 4 - ReadWrite
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000001  blink / share count 00000001  pteaddress FFFFF6FC50000000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Active     M       
    Modified

注意:上面输出中的最后一个Modified标签代表页面是脏的这一事实,并不意味着该页面在Modified列表中。页面状态的标签是Active。关于!pfn的输出的更多细节,也请参见附录A(第565页)。

后来断点再次被击中,当_MMPFN在MiFreeWsleList+0x2e0处被解锁时,它已经被移到了修改的列表中。

kd> ub
nt!MiFreeWsleList+0x215:
fffff800`03ecf531 33c9            xor     ecx,ecx
fffff800`03ecf533 488bf1          mov     rsi,rcx
fffff800`03ecf536 450f20c6        mov     r14,cr8
fffff800`03ecf53a b802000000      mov     eax,2
fffff800`03ecf53f 440f22c0        mov     cr8,rax
fffff800`03ecf543 4c8d6d10        lea     r13,[rbp+10h]
fffff800`03ecf547 8d50ff          lea     edx,[rax-1]
fffff800`03ecf54a f0410fba6d0000  lock bts dword ptr [r13],0
kd> g
Breakpoint 0 hit
nt!MiFreeWsleList+0x2a8:
fffff800`03ecf5c4 410fb6c6        movzx   eax,r14b
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       FFFFFFFFFFFFFFFF  blink / share count 000110F1  pteaddress FFFFF6FC50000000
    reference count 0000    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Modified   M       
    Modified                
kd> ub
nt!MiFreeWsleList+0x284:
fffff800`03ecf5a0 33d2            xor     edx,edx
fffff800`03ecf5a2 488bcd          mov     rcx,rbp
fffff800`03ecf5a5 e8166f0200      call    nt!MiPfnShareCountIsZero (fffff800`03ef64c0)
fffff800`03ecf5aa ba01000000      mov     edx,1
fffff800`03ecf5af 4883fbff        cmp     rbx,0FFFFFFFFFFFFFFFFh
fffff800`03ecf5b3 0f8557010000    jne     nt!MiFreeWsleList+0x3f4 (fffff800`03ecf710)
fffff800`03ecf5b9 bd01000000      mov     ebp,1
fffff800`03ecf5be f041836500fe    lock and dword ptr [r13],0FFFFFFFEh
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       FFFFFFFFFFFFFFFF  blink / share count 000110F1  pteaddress FFFFF6FC50000000
    reference count 0000    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Modified   M       
    Modified

当一个页面在列表中时,flink和blink是下一个和上一个列表节点的PFN。这里flink设置为0xFFFFFFFF'FFFFFFF意味着我们的页面在列表的尾部,所以我们可以很容易地确认它在MmModifiedPageListByColor指向的列表中。

kd> ?? ((nt!_mmpfnlist*) @@(nt!MmModifiedPageListByColor))
struct _MMPFNLIST * 0xfffff800`04054bb0
   +0x000 Total            : 0xf14
   +0x008 ListName         : 3 ( ModifiedPageList )
   +0x010 Flink            : 0x4eb2
   +0x018 Blink            : 0x4a6a
   +0x020 Lock             : 0

头部的Blink指向了我们的PFN。我们也可以确认,由我们的Blink所指向的0x110F1页有flink设置为我们的PFN。

kd> !pfn 110f1
    PFN 000110F1 at address FFFFFA8000332D30
    flink       00004A6A  blink / share count 0001A7C0  pteaddress FFFFF8A00240C8B8
    reference count 0000    used entry count  0000      Cached    color 0   Priority 1
    restore pte 00000080  containing page 013638  Modified   MP      
    Modified Shared

到目前为止,WSL项还没有被更新,但是,当恢复执行时,其断点在MiRemoveWsle+0x62处被击中,我们看到Valid位已经被清除。其他成员将在之后被更新。

kd> g
Breakpoint 1 hit
nt!MiRemoveWsle+0x62:
fffff800`03f0070e 488b5c2440      mov     rbx,qword ptr [rsp+40h]
kd> ?? ((nt!_mmsupport*) @@(nt!MmPagedPoolWs))->VmWorkingSetList->Wsle[1].u1.e1
struct _MMWSLENTRY
   +0x000 Valid            : 0y0
   +0x000 Spare            : 0y0
   +0x000 Hashed           : 0y0
   +0x000 Direct           : 0y1
   +0x000 Protection       : 0y00000 (0)
   +0x000 Age              : 0y111
   +0x000 VirtualPageNumber : 0y1111111111111111111110001010000000000000000000000000 (0xfffff8a000000)

在对_MMPFN进行了几次断点命中后,我们在MiGatherPagefilePages+0x3a5处看到,该页面正在被写入磁盘。

kd> g
Breakpoint 0 hit
nt!MiGatherPagefilePages+0x335:
fffff800`03e75bd5 7340            jae     nt!MiGatherPagefilePages+0x377 (fffff800`03e75c17)
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00004A6B  blink / share count FFFFFFFFFFFFFFFF  pteaddress FFFFF6FC50000001
    reference count 0000    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Modified   M       
    Modified
...
kd> g
Breakpoint 0 hit
nt!MiGatherPagefilePages+0x39d:
fffff800`03e75c3d 0fb6442450      movzx   eax,byte ptr [rsp+50h]
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000000  blink / share count 00000000  pteaddress FFFFF6FC50000000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Modified      W    
          WriteInProgress

可以看到当前的线程是Modified Page Writer:

kd> !thread @$thread
THREAD fffffa800190fa10  Cid 0004.0058  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa80018ea040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      869402         Ticks: 7 (0:00:00:00.109)
Context Switch Count      49             IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:05.366
Win32 Start Address nt!MiModifiedPageWriter (0xfffff80003e76230)
Stack Init fffff880047acc70 Current fffff880047abf50
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`047acac0 fffff800`03e763eb : fffffa80`00004847 00000000`00000000 fffffa80`00004847 00000000`00000000 : nt!MiGatherPagefilePages+0x39d
fffff880`047acba0 fffff800`04183cce : fffffa80`0190fa10 00000000`00000000 00000000`00000080 00000000`00000001 : nt!MiModifiedPageWriter+0x1bb
fffff880`047acc00 fffff800`03ed7fe6 : fffff800`04058e80 fffffa80`0190fa10 fffffa80`0190d660 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`047acc40 00000000`00000000 : fffff880`047ad000 fffff880`047a7000 fffff880`047abf50 00000000`00000000 : nt!KiStartSystemThread+0x16

后来_MMPFN再次被更新,当它在MiUpdatePfnBackingStore+0x8a被解锁(仍然在修改过的页面写入器的上下文中),我们看到restore pte现在在上面的DWORD中存储了分配给页面的分页文件偏移。

kd> g
Breakpoint 0 hit
nt!MiUpdatePfnBackingStore+0x36:
fffff800`03e760d6 732d            jae     nt!MiUpdatePfnBackingStore+0x65 (fffff800`03e76105)
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000000  blink / share count 00000000  pteaddress FFFFF6FC50000001
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 004A68  Modified      W    
          WriteInProgress
kd> g
Breakpoint 0 hit
nt!MiUpdatePfnBackingStore+0x8a:
fffff800`03e7612a 410fb6c4        movzx   eax,r12b
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000000  blink / share count 00000000  pteaddress FFFFF6FC50000000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 485000000080  containing page 004A68  Modified      W    
          WriteInProgress

写入仍在进行中,但是,现在分页文件的偏移量已经分配给了页面内容。

再往前走,MPW在MiWriteComplete+0x1c7处再次碰到断点,我们看到该页已经被移到了Standby列表。

kd> g
Breakpoint 0 hit
nt!MiWriteComplete+0x19c:
fffff800`03f2099c 0f8216aafbff    jb      nt! ?? ::FNODOBFM::`string'+0x5117c (fffff800`03edb3b8)
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       00000000  blink / share count 00000000  pteaddress FFFFF6FC50000001
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 485000000080  containing page 004A68  Modified      W    
          WriteInProgress
kd> g
Breakpoint 0 hit
nt!MiWriteComplete+0x1c7:
fffff800`03f209c7 0fb6442430      movzx   eax,byte ptr [rsp+30h]
kd> !pfn 4a6a
    PFN 00004A6A at address FFFFFA80000DF3E0
    flink       FFFFFFFFFFFFFFFF  blink / share count FFFFFFFFFFFFFFFF  pteaddress FFFFF6FC50000000
    reference count 0000    used entry count  0000      Cached    color 0   Priority 0
    restore pte 485000000080  containing page 004A68  Standby

同样,flink告诉我们,我们的页面是列表的最后一个,在我们的例子中,这将是优先级0的备用列表,所以值得看一下链表头。

kd> ?? ((nt!_mmpfnlist*) @@(nt!MmStandbyPageListByPriority)+0)
struct _MMPFNLIST * 0xfffff800`0406ac80
   +0x000 Total            : 1
   +0x008 ListName         : 2 ( StandbyPageList )
   +0x010 Flink            : 0x4a6a
   +0x018 Blink            : 0x4a6a
   +0x020 Lock             : 0

概括地说,到目前为止,我们已经看到工作集管理器修剪了PP工作集,并将页面移到了Modified列表中,然后Modified页面写入器将页面内容复制到分页文件中,并将页面移到Standby列表中。现在页面已经干净了,其复制的分页文件的偏移量被存储到_MMPFN.OriginalPte中。

当MemTests的两个实例继续要求获得内存时,_MMPFN最终被再次访问以重新使用。第一次命中是在MiRemoveLowestPriorityStandbyPage+0x91在MemTest的线程上下文中。复现过程中实际断在了System.exe里,

kd> g
Breakpoint 0 hit
nt!MiRemoveLowestPriorityStandbyPage+0xdf:
fffff800`03f33d73 0f82be76f4ff    jb      nt! ?? ::FNODOBFM::`string'+0x11e5c (fffff800`03e7b437)
kd> !thread @$thread
THREAD fffffa800190c040  Cid 0004.004c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa80018ea040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      871483         Ticks: 68 (0:00:00:01.060)
Context Switch Count      13333          IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.124
Win32 Start Address nt!ExpWorkerThreadBalanceManager (0xfffff800041cee88)
Stack Init fffff88004797c70 Current fffff88004797680
Base fffff88004798000 Limit fffff88004792000 Call 0000000000000000
Priority 14 BasePriority 14 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`04797280 fffff800`03ec86cb : 00000580`00000000 00000580`00000000 fffff880`04797340 00000000`00000001 : nt!MiRemoveLowestPriorityStandbyPage+0xdf
fffff880`04797300 fffff800`03ed42da : fffff6fc`400187c0 00000000`00000002 fffffa80`01bf2b61 00000000`00000006 : nt!MiAllocateKernelStackPages+0x2e7
fffff880`047973c0 fffff800`0411c643 : fffffa80`000030e9 00000000`00000000 00000000`000030f7 fffff800`00000000 : nt!MmCreateKernelStack+0x3cf
fffff880`047974b0 fffff800`041d2d28 : fffffa80`01bf2b60 fffffa80`018ea000 fffffa80`01bf2f18 00000000`00000000 : nt!KeInitThread+0x1b3
fffff880`04797510 fffff800`041d247e : fffffa80`018ea040 fffff880`04797a20 00000000`00000000 00000000`00000000 : nt!PspAllocateThread+0x773
fffff880`04797730 fffff800`04183c19 : 00000000`00000000 00000000`00000000 00000000`00000000 ffffd233`0334ddee : nt!PspCreateThread+0x1d2
fffff880`047979b0 fffff800`041575e4 : 00000000`00000000 00000000`00000000 fffff800`04058e80 fffff800`03ef21c0 : nt!PsCreateSystemThread+0x125
fffff880`04797aa0 fffff800`0422edea : fffff800`04083644 fffffa80`0190c040 00000000`00000000 fffff800`041cee88 : nt!ExpCreateWorkerThread+0x70
fffff880`04797b20 fffff800`041cef6e : fffffa80`0190c040 fffff880`04797b90 fffffa80`00000001 00000000`00000000 : nt! ?? ::NNGAKEGL::`string'+0x404a3
fffff880`04797b50 fffff800`04183cce : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ExpWorkerThreadBalanceManager+0xe6
fffff880`04797c00 fffff800`03ed7fe6 : fffff800`04058e80 fffffa80`0190c040 fffffa80`0190ab60 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04797c40 00000000`00000000 : fffff880`04798000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

kd> g
Breakpoint 0 hit
nt!MiRemoveLowestPriorityStandbyPage+0x210:
fffff800`03f33ea4 410fb6c7        movzx   eax,r15b
kd> !thread @$thread
THREAD fffffa800190c040  Cid 0004.004c  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0
Not impersonating
DeviceMap                 fffff8a000008c10
Owning Process            fffffa80018ea040       Image:         System
Attached Process          N/A            Image:         N/A
Wait Start TickCount      871483         Ticks: 71 (0:00:00:01.107)
Context Switch Count      13333          IdealProcessor: 0             
UserTime                  00:00:00.000
KernelTime                00:00:00.156
Win32 Start Address nt!ExpWorkerThreadBalanceManager (0xfffff800041cee88)
Stack Init fffff88004797c70 Current fffff88004797680
Base fffff88004798000 Limit fffff88004792000 Call 0000000000000000
Priority 14 BasePriority 14 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`04797280 fffff800`03ec86cb : 00000580`00000000 00000580`00000000 fffff880`04797340 00000000`00000001 : nt!MiRemoveLowestPriorityStandbyPage+0x210
fffff880`04797300 fffff800`03ed42da : fffff6fc`400187c0 00000000`00000002 fffffa80`01bf2b61 00000000`00000006 : nt!MiAllocateKernelStackPages+0x2e7
fffff880`047973c0 fffff800`0411c643 : fffffa80`000030e9 00000000`00000000 00000000`000030f7 fffff800`00000000 : nt!MmCreateKernelStack+0x3cf
fffff880`047974b0 fffff800`041d2d28 : fffffa80`01bf2b60 fffffa80`018ea000 fffffa80`01bf2f18 00000000`00000000 : nt!KeInitThread+0x1b3
fffff880`04797510 fffff800`041d247e : fffffa80`018ea040 fffff880`04797a20 00000000`00000000 00000000`00000000 : nt!PspAllocateThread+0x773
fffff880`04797730 fffff800`04183c19 : 00000000`00000000 00000000`00000000 00000000`00000000 ffffd233`0334ddee : nt!PspCreateThread+0x1d2
fffff880`047979b0 fffff800`041575e4 : 00000000`00000000 00000000`00000000 fffff800`04058e80 fffff800`03ef21c0 : nt!PsCreateSystemThread+0x125
fffff880`04797aa0 fffff800`0422edea : fffff800`04083644 fffffa80`0190c040 00000000`00000000 fffff800`041cee88 : nt!ExpCreateWorkerThread+0x70
fffff880`04797b20 fffff800`041cef6e : fffffa80`0190c040 fffff880`04797b90 fffffa80`00000001 00000000`00000000 : nt! ?? ::NNGAKEGL::`string'+0x404a3
fffff880`04797b50 fffff800`04183cce : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ExpWorkerThreadBalanceManager+0xe6
fffff880`04797c00 fffff800`03ed7fe6 : fffff800`04058e80 fffffa80`0190c040 fffffa80`0190ab60 00000000`00000000 : nt!PspSystemThreadStartup+0x5a
fffff880`04797c40 00000000`00000000 : fffff880`04798000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x16

后面的复现不太容易,直接贴书上的图。

Untitled 5.png

_MMPFN仍然处于Standby状态,但现在它被锁定了,大概是因为它即将被重新使用。

https://www.notion.so

到此为止,PP地址0XFFFFF8A0000000的PTE一直保持不变,即处于过渡期,但仍然指向我们的页面。

Untitled 6.png

当_mmpfn在MiRemoveLowestPriorityStandbyPage+0x1d0处被解锁时,PTE已经改变,现在指向分页文件,在之前写入页面的偏移处。

Untitled 7.png

至于_MMPFN,它已经从列表中解锁,恢复pte不再存储分页文件偏移,并且暂时处于 "Bad "状态。

Untitled 8.png

进一步的断点命中显示该页正在进行读操作,然后最终成为MemTests工作集的一部分。

综上所述,我们看到VMM页面的系统范围地址与用户模式的地址非常相似:它们是工作集管理器检查的工作集的一部分,所以最终被移到过渡列表中并重新使用。

通过在该页的PTE地址上设置断点,我们也可以看到该页正在从分页文件中重新加载。在断点被击中几次后,我们看到PTE在MilnitializeReadlnProgressSinglePfn+0x246处被改变。

Untitled 9.png

PTE现在处于过渡期,并指向一个不同的物理页,对该页的读I/O正在进行。

Untitled 10.png
观察一下指向分页文件的PTE是如何被保存到_mimpfn . 原始的Pte(显示为restore pte)。这个页面开始时是干净的,因为它的一个副本存在于分页文件中。

现在我们有了一个PFN,我们可以在写到_mmpfn .Lock的时候设置一个额外的断点。锁定,以进一步跟踪页面置入。

最终PTE被指向该页,并为其创建了一个WSL条目。

Untitled 11.png

我们可以注意到,该页已经 "丢失 "了它的分页文件副本,这可以从恢复pte为0x80看出。这意味着导致故障的访问是一个写操作,页面内容即将被修改,分页文件副本已经被释放,因为它很快就会过期。

在触及两个断点的函数中,我们发现。MiResolvePageFileFault、MilnitializeReadlnProgressSinglePfn、MiFindActualFaultingPte(由MilssueHardFault间接调用)、MiWaitForlnPageComplete。通过在_MMPFN.ul.Wslndex上的额外断点,我们还可以捕捉到MMPFN.ul.Wslndex的数据。我们也可以捕捉到MiUpdateWsle建立WSL条目。这些函数与用户模式地址的硬故障所调用的函数相同。

2.9.5 不属于任何工作集的页面

所有的分页活动似乎都是围绕着工作集列表进行的:工作集管理器扫描这些列表中老化的页面,并在可用内存不足时删除最近访问最少的页面。这意味着,一个主动映射虚拟地址的页面,但不在任何工作集列表中,是隐含的不可分页的,因为对工作集管理器来说是不可及的。这样的页面可以被识别,因为它是活动的,并且有一个有效的_mmpfn.PteAddress,而同时_mmpfn.ul.Wslndex=0。

作为一个例子,我们已经看到,系统范围的分页结构是不能被分页的。他们的_mmpfn通常有u1.WsIndex = 0,例如

kd> !pte fffff880`00000000
                                           VA fffff88000000000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200000    PTE at FFFFF6FC40000000
contains 000000007BE04863  contains 000000007BD83863  contains 000000007BD82863  contains 000000007BE0A963
pfn 7be04     ---DA--KWEV  pfn 7bd83     ---DA--KWEV  pfn 7bd82     ---DA--KWEV  pfn 7be0a     -G-DA--KWEV
kd> !pfn 7bd82
    PFN 0007BD82 at address FFFFFA8001738860
    flink       00000000  blink / share count 00000002  pteaddress FFFFF6FB7E200000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 00000080  containing page 07BD83  Active     M       
    Modified

0x7bd82是页表的PFN。我们可以看到许多__MMPFN成员与页映射这个VA是一致的:pteaddress是正确的(是PDE的地址),包含页指向PDPT页,甚至共享和引用计数都设置正确。然而flink,即_mmpfn.u1.wslndex为0,并且没有工作集管理系统范围的分页结构(有,唉,系统PTE工作集,它的名字在这方面有很大的误导性;它不是系统PTE的工作集,而是,管理系统PTE区域的工作集,它根本不存储PTE)

这并不意味着这些页面从未被释放过。例如,我们有理由认为,当VMM没有映射任何页面时,它为系统范围释放了一个PT,也许是因为它所映射的整个虚拟范围已经被释放。一般来说,VMM会释放和回收那些不使用的不可分页的页面,但是,只要它们被使用,它就不会分页,也就是说,只要它们被使用,它们的VA就有效。

所以对于系统分页结构一般来说,大多数特殊用途区域的许多页面都是不可分页的,因为它们不能被主要的分页角色,即工作集管理器所触及。

2.10 会话空间

2.10.1 会话空间的目的

我们可以把这个区域归为特殊用途区域,因为MiObtainSystemVa似乎不允许从这个区域分配范围。然而内核模式的驱动被加载到这个区域,其中一部分被用于分页池,所以这个区域也像其他动态分配的区域一样被VMM(内部)管理。

这个区域的主要目的是有一个虚拟范围,其内容在属于一个特定会话的所有进程之间共享,同时,在不同的会话之间是私有的。换句话说,属于不同会话的进程对它有一个不同的PML4E,并将不同的物理页映射到这个范围。

系统必须为每个活动会话维护一套单独的数据结构。例如,每个会话必须有自己的用户界面的状态。Windows GUI是基于系统对象的层次结构:会话、窗口站和桌面。最后我们在屏幕上看到的是属于名为Winsta0的交互式窗口站的桌面状态,其会话目前与屏幕和键盘相连。在这一过程中,系统中还存在其他会话,每个会话都包括Windows站和桌面。因此系统必须维护每个会话的所有这些状态信息,而这是通过将其存储到这个区域来实现的。

这个区域被划分为如下内容:

  • 会话控制结构
  • 会话空间镜像文件
  • 会话分页池
  • 会话映射视图

下一节将介绍每一种内容。

2.10.2 区域内容

#### 会话控制结构

这些是用于管理会话区域本身的数据结构。在这一节中,我们将提供一些例子,尽管很可能有许多其他未文档化的数据结构存在于会话空间中。

##### _MM_SESSION_SPACE

系统为每个会话创建一个_mm_session_space的实例,每个进程都有_EPROCESS.Session设置为其所属会话的实例地址,这个地址在会话空间之外(它在系统PTE区域),因此,无论当前地址空间属于哪个会话,都会被映射到同一个物理页。

通过对_mm_session_space实例的地址进行一些探索,我们发现一个有趣的细节。下面我们看到explorer.exe实例的地址:

nt!RtlpBreakWithStatusInstruction:
fffff800`03e89490 cc              int     3
kd> !process 0 0 explorer.exe
PROCESS fffffa8002f98b30
    SessionId: 1  Cid: 09e8    Peb: 7fffffdf000  ParentCid: 09c0
    DirBase: 79a9f000  ObjectTable: fffff8a0015835f0  HandleCount: 614.
    Image: explorer.exe

kd> ?? ((nt!_eprocess*)0xfffffa8002f98b30)->Session
void * 0xfffff880`058bb000

正如我们所说,这个地址在系统PTE区域,现在让我们看看它的PTE和它页面的_mmpfn

kd> !pte @@(((nt!_eprocess*)0xfffffa8002f98b30)->Session)
                                           VA fffff880058bb000
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200160    PTE at FFFFF6FC4002C5D8
contains 000000007BE04863  contains 000000007BD83863  contains 000000002A19D863  contains 0000000011ABB963
pfn 7be04     ---DA--KWEV  pfn 7bd83     ---DA--KWEV  pfn 2a19d     ---DA--KWEV  pfn 11abb     -G-DA--KWEV
kd> !pfn 11abb
    PFN 00011ABB at address FFFFFA8000350310
    flink       FFFFF880058BB000  blink / share count 00000001  pteaddress FFFFF6FC80000000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 000000C0  containing page 0120BF  Active     M       
    Modified

_mmpfn的内容显示了一些非典型的值: pteaddress没有被设置成由!pte报告的PTE地址; containing page(实际上是_MMPFN.u4 . PteFrame)没有被设置成PT的PFN,也是由!pte报告的(它是PDE中的PFN);flink被设置成0xFFFFF880058BB000,即_MM_SESSION_SPACE本身的地址,而对于_MM_SESSION_SPACE来说,它的地址是_MM_SESSION_SPACE本身。而对于一个活动页,这个成员通常被设置为WSL索引或0。

现在我们来看看_mmpfn中的pteaddress:它实际上是映射会话空间第一页的PTE的地址,我们可以通过!pte看到。

kd> !pte FFFFF6FC80000000
                                           VA fffff90000000000
PXE at FFFFF6FB7DBEDF90    PPE at FFFFF6FB7DBF2000    PDE at FFFFF6FB7E400000    PTE at FFFFF6FC80000000
contains 0000000000000000
not valid

请记住,当!PTE与PTE地址一起使用时,它报告由给定地址映射的PxEs。因此,我们在上面看到的是0xFFFFF900'000000被PTE在0xFFFFF6FC'80000000处映射,这就是我们在_mmpfn中发现的情况。

因此让我们看看当explorer.exe是当前进程时,会话空间是如何被映射的:

kd> .process /P 0xfffffa8002f98b30
Implicit process is now fffffa80`02f98b30
.cache forcedecodeptes done
kd> !pte fffff90000000000
                                           VA fffff90000000000
PXE at FFFFF6FB7DBEDF90    PPE at FFFFF6FB7DBF2000    PDE at FFFFF6FB7E400000    PTE at FFFFF6FC80000000
contains 000000001267D863  contains 000000001207E863  contains 00000000120BF863  contains 0000000011ABB863
pfn 1267d     ---DA--KWEV  pfn 1207e     ---DA--KWEV  pfn 120bf     ---DA--KWEV  pfn 11abb     ---DA--KWEV

这很有意思:第一个会话空间页是由存储_mm_session_space实例的同一个物理页映射的。

因此概括地说:一个进程属于一个会话;_MM_SESSlON_SPACE的一个实例存储了这个会话的状态信息;当进程地址空间被映射时。
_mm_session_space在第一个会话空间页中是可见的;_mm_session_space在所有内存上下文中也是可见的,其地址在系统PTE区域中,可以在_EPROCESS.Session中找到。

注意系统PTE区域是如何被用来映射需要在任意会话上下文中访问的东西的。从概念上讲,这类似于用MDL来映射用户模式的缓冲区,以使它们在任意的进程上下文中被访问。

还请注意,_MMPFN中包含的页面是0x0120BF,这是上面的!PTE输出中可见的0xFFFFF900'000000的PT的PFN。换句话说,_mmpfn的内容被设置为反映会话空间VA处的映射,而不是系统PTE处的映射。

最后一个注意点是关于会话空间地址的第一个!pte输出,我们在发出.process命令之前获得了这个输出。正如我们所看到的,我们所处的环境中,会话空间的PML4E被设置为0,因此整个会话空间是无效的。事实证明,这就是我们刚刚进入调试器时看到的情况,当前的上下文是系统进程的上下文:这个上下文没有会话空间。

##### 会话工作集列表

会话空间的子范围是可以分页的,所以它们是通过一个工作集列表来管理的,就像我们过去遇到的其他工作集列表一样_mm_session__space有一个类型为_mmsupport的Vm成员。

kd> ?? &( (nt!_mm_session_space*) ((nt!_eprocess*)0xfffffa8002f98b30)->Session)->Vm
struct _MMSUPPORT * 0xfffff880`058bbc00
   +0x000 WorkingSetMutex  : _EX_PUSH_LOCK
   +0x008 ExitGate         : (null) 
   +0x010 AccessLog        : (null) 
   +0x018 WorkingSetExpansionLinks : _LIST_ENTRY [ 0xfffffa80`02fe7410 - 0xfffffa80`03671970 ]
   +0x028 AgeDistribution  : [7] 0
   +0x044 MinimumWorkingSetSize : 0x400
   +0x048 WorkingSetSize   : 0x11f6
   +0x04c WorkingSetPrivateSize : 0xf9e
   +0x050 MaximumWorkingSetSize : 0x408
   +0x054 ChargedWslePages : 0
   +0x058 ActualWslePages  : 0
   +0x05c WorkingSetSizeOverhead : 4
   +0x060 PeakWorkingSetSize : 0x165c
   +0x064 HardFaultCount   : 0
   +0x068 VmWorkingSetList : 0xfffff900`00812000 _MMWSL
   +0x070 NextPageColor    : 0x448e
   +0x072 LastTrimStamp    : 0x1071
   +0x074 PageFaultCount   : 0x4727
   +0x078 RepurposeCount   : 0x442a
   +0x07c Spare            : [2] 0
   +0x084 Flags            : _MMSUPPORT_FLAGS

VmWorkingSetList的成员指向会话空间内。我们也可以看到,_wsle数组也在会话空间中:

kd> ?? ( (nt!_mm_session_space*) ((nt!_eprocess*)0xfffffa8002f98b30)->Session)->Vm.VmWorkingSetList
struct _MMWSL * 0xfffff900`00812000
   +0x000 FirstFree        : 0x67e
   +0x004 FirstDynamic     : 1
   +0x008 LastEntry        : 0x165c
   +0x00c NextSlot         : 1
   +0x010 Wsle             : 0xfffff900`00812488 _MMWSLE
   +0x018 LowestPagableAddress : 0xfffff900`00000000 Void
   +0x020 LastInitializedWsle : 0x176e
   +0x024 NextAgingSlot    : 0
   +0x028 NumberOfCommittedPageTables : 0
   +0x02c VadBitMapHint    : 0
   +0x030 NonDirectCount   : 0
   +0x034 LastVadBit       : 0
   +0x038 MaximumLastVadBit : 0
   +0x03c LastAllocationSizeHint : 0
   +0x040 LastAllocationSize : 0
   +0x048 NonDirectHash    : (null) 
   +0x050 HashTableStart   : 0xfffff900`80001000 _MMWSLE_HASH
   +0x058 HighestPermittedHashAddress : 0xfffff900`a0101008 _MMWSLE_HASH
   +0x060 MaximumUserPageTablePages : 0
   +0x064 MaximumUserPageDirectoryPages : 0
   +0x068 CommittedPageTables : (null) 
   +0x070 NumberOfCommittedPageDirectories : 0
   +0x078 CommittedPageDirectories : [128] 0
   +0x478 NumberOfCommittedPageDirectoryParents : 0
   +0x480 CommittedPageDirectoryParents : [1] 0

这表明数组就在_mmwsl的后面。

内核映像中还有一个名为MiSessionSpaceWs的静态变量,。这个变量存储了会话_mmwsl的地址。

kd> dq nt!MiSessionSpaceWs l1
fffff800`040c2518  fffff900`00812000

这表明这个地址对每个会话都是相同的。

会话空间位图

_mm_session_space.DynamicVaBitMap似乎在会话空间中存储一个位图的地址。

kd> ?? ((nt!_mm_session_space*) ((nt!_eprocess*) 0xfffffa8002f98b30)->Session)->DynamicVaBitMap
struct _RTL_BITMAP
   +0x000 SizeOfBitMap     : 0xfa00
   +0x008 Buffer           : 0xfffff900`00010000  -> 0xf5ffff

这也许是VMM用来跟踪该区域内空闲/使用中的虚拟地址的。

#### 会话空间镜像文件

除了数据结构,这个区域还承载着实现windows GUI的内核模式驱动。我们将在后面更详细地讨论这个问题。现在我们只想说,VMM和映像加载器给每个会话提供了这些映像文件的读/写section的私有拷贝,当然也包括它们的静态变量。在这个区域加载的驱动程序有 "Win32K.sys(窗口管理器),CDD.DLL(Canonical显示驱动程序),TSDDD.dll(帧缓冲显示驱动程序),DXG.sys(DirectX图形驱动程序)",通过在会话空间加载这些驱动程序,VMM在每个会话中给它们一个不同的静态变量副本,因此驱动程序本身不需要维护每个会话状态。

#### 会话分页池

会话空间的一部分被用于每个会话的分页池。这允许其他内核模式的组件对每个会话分配可分页的内存。分页池的限制是由_MM_SESSION_SPACE.PagedPoolStart和_MM_SESSION_SPACE.PagedPoolEnd给出的。

0: kd> ?? ((nt!_mm_session_space*) ((nt!_eprocess*)0xfffffa8032ecf060)->Session)->PagedPoolStart
void * 0xfffff900`c0000000
0: kd> ?? ((nt!_mm_session_space*) ((nt!_eprocess*)0xfffffa8032ecf060)->Session)->PagedPoolEnd
void * 0xfffff920`bfffffff
映射视图

关于这个问题的一些资料提到了会话空间中存在映射的视图。其中指出桌面堆是这些视图之一。这些应该是分配给会话空间实例和其他虚拟区域之间共享section内存的内存section视图。

Win32k.sys使用一个堆,"用于与用户模式共享GDI和用户对象",存储在一个内存区。看起来这个堆很可能是桌面堆,这个内存区的视图被映射到会话空间和用户模式中。

通过对一些数据结构的挖掘,有可能发现一些section同时映射在这个区域和一些进程的用户模式范围内(有只读保护),所以用户模式组件实际上可以读取会话空间的section。

2.10.3 页面错误处理

MmAccessFault处理该区域的页面错误,但有以下特点:

  • 如果当前进程是系统进程,该函数会进行bugchecks,系统的PML4将所有会话空间映射为无效时,
  • 用于跟踪新映射的虚拟地址的WSL当然是会话工作集列表。

2.11 动态内核虚拟地址空间

2.11.1 区域内容

这是一个动态分配的区域,用于系统高速缓存视图、分页特殊池、非分页特殊池。

系统高速缓存视图

这些视图被用来实现文件缓存,后面我们会讲关于缓存管理器的更多细节。

缓存管理器通过创建一个由要缓存的文件支持的内存区对象来完成其工作。我们知道这样的内存区并不能使文件内容在虚拟地址空间中可见,为了实现这一点,我们必须在虚拟空间的某个地方映射出该区的视图。缓存管理器从这个区域分配VA范围来映射这些视图。

非分页和分页特殊池

这些是Driver Verifier使用的分页和非分页池的特殊工具,Driver Verifier是一个集成在系统中的工具,可以用来检查驱动程序的错误。驱动验证器可以被配置为拦截来自驱动的池分配,并从特殊的池区域满足它们,其中分配的内存被特意用未映射的VA包围。这样一来,如果一个有缺陷的驱动程序访问其分配的池块之外的内存,就会发生页面错误,系统崩溃,驱动程序的函数地址被报告为蓝屏数据的一部分。

2.11.2 虚拟地址空间管理

静态的MiSystemAvailableVa包含该区域内可用的2MB范围的数量。

当为内存类型指定MiVaSystemCache、MiVaSpecialPoolPaged或MiVaSpecialPoolNonPaged时,MiObtainSystemVa从该区域进行分配。

用来管理这个区域的位图是MiSystemVaBitmap。

2.11.3 页面错误处理

由于这个区域用于不同类型的内容,必须根据存储在错误地址的内容来选择要使用的工作集列表。对于其他动态分配的区域,要使用的WSL是由区域本身暗示的。然而动态内核VA区域由很多种类组成的,它的物理页可以属于不同的WSL。

MmAccessFault查看包含PTE映射地址的页表的mmpfn,并检查VaType成员。它的值决定了根据下表中选择的WSL:

Untitled 12.png

该表还显示,由_mi_system_va_type定义的符号值与WSL选择的符号值一致。

这给了我们一个提示,即_MMPFN.VaType,并告诉我们,当从这个区域分配时,VMM将其设置为分配中指定的内存类型。VMM使用页表的_MMPFN,它是由映射地址的PDE选择的,所以所有由一个PDE映射的VA将具有相同的类型。这不是一个问题,因为系统VA范围的最小分配单位是一个PDE,即2MB。

2.11 PFN数据库

2.11.1 区域内容

这个特殊用途区域是存储_mmpfn数组的地方,每个物理内存页都有一个元素。我们在本书中一直在讨论_mmpfn实例,所以关于这个区域的内容已经没有什么可说的了。

这个区域是不可分页的,所以_mmpfn实例总是可以在任何IRQL下访问。

2.11.2 PFN数据库和物理地址空间

物理地址空间图

PFN数据库的大小是由系统中的物理内存量和芯片组配置决定的。

系统中的几个设备是内存映射的,这意味着处理器对它们的访问就像对内存一样。有一些为它们配置的物理地址范围,所以当处理器在这些物理地址进行读写时,它实际上是在与这些设备进行交互。

Memlnfo(http://www.winsiderss.com/tools/meminfo/meminfo.htm),由Alex lonescu编写,它是一个能够转储物理地址范围图的工具,这些地址实际上被映射到物理内存中,即没有映射到设备中。这是一个来自虚拟机系统的Memlnfo输出样本,因为MemInfo已经很久没更新了。

Untitled 13.png

这个Map显示,在物理地址空间中有一些没有映射到内存的空缺,例如,第一个空缺在0x9F000和0x100000之间。

物理地址空间大小

Memlnfo也显示了MmHighestPhysicalPage的静态值,尽管它实际上是将其增加了1。如果我们用WinDbg查看同一个变量,就可以看到这一点。

1: kd> ? poi(nt!MmHighestPhysicalPage)
Evaluate expression: 1310719 = 00000000`0013ffff

这意味着该系统的物理地址空间由0x140000或1310720个页面组成,最后一个页面的PFN为0x140000或1310720。

由于每个页面的大小为4kB,0x140000页面给出的字节数如下。

0x140000 * 0x1000 = 1 4000 0000 = 5GB

这个特殊的系统有4GB的RAM,在这个数值之上存在额外地址空间是由于内存映射设备,它将实际的物理内存向更高的地址转移。

将这些数据与静态的 MmNumberOfPhysicalPages 的值相匹配是很有趣的,它给出了 Windows 可用的物理内存的数量。

1: kd> dq nt!MmNumberOfPhysicalPages l1
fffff800`04703080  00000000`000fff7e

这个值小于 MmHighestPhysicalPage + 1,因为它是实际内存页的计数,而另一个是物理地址空间的总页数,包括映射到设备的页数。

为了理解这两个值之间的关系,我们可以从计算 "缺口"的大小开始,即没有映射到内存的范围(来自Memlnfo的输出)。

1: kd> ? 0x100000-0x9F000
Evaluate expression: 397312 = 00000000`00061000
1: kd> ? BFF00000-BFEE0000
Evaluate expression: 131072 = 00000000`00020000
1: kd> ? 1`00000000-0`C0000000
Evaluate expression: 1073741824 = 00000000`40000000

这些范围的总和为0x40081页,必须从MmHighestPhysicalPage中减去,才能找到实际的内存页数。此外Memlnfo显示,映射到物理内存的第一个子范围从0x1000开始,所以在物理地址空间的最开始有一个0x1000字节的额外空缺,即1页。因此实际内存的页数由以下几项给出。

MmHighestPhysicalPage+1-0x40171-1 得出:

1: kd> ? poi(nt!MmHighestPhysicalPage)+1-0x40081-1
Evaluate expression: 1048446 = 00000000`000fff7e

即与MmNumberOfPhysicalPages中的值相同。

不幸的是,这留下了一个无法解释的细节:由于某种原因,VMM没有使用整个5GB的RAM。

PFN数据库大小

PFN数据库是一个由PFN索引的简单数组。由于存在映射到设备的PFN值的范围,与这些范围相对应的数组元素并不代表实际的物理内存,但是它们存在于数据库中,因为VMM计算任何物理地址的_mmpfn的地址为:

0xFFFFFA80'00000000 + sizeof(_mmpfn) * PFN

鉴于此,数据库的结束地址可以很容易地从MmHighestPhysicalPage中计算出来:

0xFFFFFA80'00000000 + sizeof(_MMPFN) * (MmHighestPhysicalPage + 1)

在我们的测试系统上:

1: kd> ?? 0xfffffa80`00000000+sizeof(nt!_MMPFN)*(@@(poi MmHighestPhysicalPage)+1)
unsigned int64 0xfffffa80`03c00000

我们可以看到,虚拟机中PFN数据库后面并不是书上说的紧跟着颜色排列的页面。

1: kd> dq nt!MmFreePagesByColor l1
fffff800`04706100  fffffa80`30c00000
PFN数据库项对应的映射设备

对于没有映射到实际内存的PFN,相应的PFN条目似乎被填上了0。

如果整个PFN db页对应于非内存PFN,则该页没有被映射,即PFN DB区域中对应的VA范围无效。

大页使用

在某些系统上可以观察到一个不同的行为:PFN DB是用大页面映射的,所以即使是跨越整个4k页面的DB区域也可以被映射,如果它们与有效的DB条目共享大页面范围。当这种情况发生时,无效的条目会被填上零。这种行为可以在一个有4GB内存的系统上观察到。

1: kd> !pte fffffa80`00000000
                                           VA fffffa8000000000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00000    PTE at FFFFF6FD40000000
contains 0000000004A45863  contains 0000000004A46863  contains 0000000005C008E3  contains 0000000000000000
pfn 4a45      ---DA--KWEV  pfn 4a46      ---DA--KWEV  pfn 5c00      --LDA--KWEV  LARGE PAGE pfn 5c00

相反在一个拥有1GB内存的虚拟机上使用的是小页:

1: kd> !pte fffffa80`00000000
                                           VA fffffa8000000000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00000    PTE at FFFFF6FD40000000
contains 0000000005C00863  contains 0000000005C01863  contains 0000000005C02863  contains 0000000005C03863
pfn 5c00      ---DA--KWEV  pfn 5c01      ---DA--KWEV  pfn 5c02      ---DA--KWEV  pfn 5c03      ---DA--KWEV

使用大页可以节省TLB项和用于PT的物理页,代价是将物理内存映射到未使用的DB条目。可能当物理内存充足的时候,VMM会选择这种方法。另外更多的物理内存意味着更大的PFN DB范围,如果使用小页,会消耗更多的TLB项和更多的PT,所以最终可能会 "浪费 "映射到无效的DB项的物理内存。

2.13 按颜色划分的Free和Zeroed链表

这个区域是一个特殊用途的区域,专门用来存储列表头的数组。

第一个数组是存储Zeroed页的PFN链表的头。开始的地址是:

0: kd> dq nt!MmFreePagesByColor l1
fffff800`046fa100  fffffa80`30c00000

每个color-NUMA索引值都有一个单独的列表头。在前面几节分析的系统中,颜色指数是7位宽,有一个NUMA节点,所以color-NUMA指数的范围是0-0x7F。每个列表头是_mmpfnlist的一个实例,所以数组的结束地址是

0: kd> ? poi nt!MmFreePagesByColor+80*@@(sizeof(nt!_MMPFNLIST))
Evaluate expression: -6046496058368 = fffffa80`30c01400

我们还知道空闲列表的列表头数组的地址是

0: kd> dq nt!MmFreePagesByColor +8 l1
fffff800`046fa108  fffffa80`30c02800

换句话说,指向Free列表的指针在MmFreePagesByColor+8处,它正好指向Zeroed列表数组的后面。空闲列表也有0x80的列表头,所以它们的终点是:

0: kd> ? poi(nt!MmFreePagesByColor +8) + 80 * @@(sizeof(nt!_MMPFNLIST))
Evaluate expression: -6046496048128 = fffffa80`30c03c00

在这两个数组之后是单链表的对应数组。Zero列表的数组的地址可以被提取如下。

0: kd> dq nt!MiZeroPageSlist l1
fffff800`04694518  fffffa80`30c05000

这个数组为每个color-NUMA值存储一个_SLIST_HEADER,所以它的结束地址是

0: kd> ? poi nt!MiZeroPageSlist + 80 * @@(sizeof(nt!_SLIST_HEADER))
Evaluate expression: -6046496040960 = fffffa80`30c05800

并与指向Free list数组的指针很好地匹配。

0: kd> dq nt!MiFreePageSlist l1
fffff800`04694530  fffffa80`30c06000

这最后一个array结束于

0: kd> ? poi nt!MiFreePageSlist + 80 * @@(sizeof(nt!_SLIST_HEADER))
Evaluate expression: -6046496036864 = fffffa80`30c06800

我们看到非分页池在下两页开始。

1: kd> dq nt!MmNonPagedPoolStart l1
fffff800`046f70d8  fffffa80`30c08000

这个区域似乎是不可分页的,这是有道理的,因为VMM会访问这些列表来解决页面错误。在4GB系统上,它被映射为大页面(因此隐含不可分页)。

1: kd> !pte poi nt!MmFreePagesByColor
                                           VA fffffa8030c00000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00C30    PTE at FFFFF6FD40186000
contains 0000000004A45863  contains 0000000004A46863  contains 0000000008C008E3  contains 0000000000000000
pfn 4a45      ---DA--KWEV  pfn 4a46      ---DA--KWEV  pfn 8c00      --LDA--KWEV  LARGE PAGE pfn 8c00
1: kd> !pte poi nt!MiFreePageSlist + 80 * @@(sizeof(nt!_SLIST_HEADER))
                                           VA fffffa8030c06800
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00C30    PTE at FFFFF6FD40186030
contains 0000000004A45863  contains 0000000004A46863  contains 0000000008C008E3  contains 0000000000000000
pfn 4a45      ---DA--KWEV  pfn 4a46      ---DA--KWEV  pfn 8c00      --LDA--KWEV  LARGE PAGE pfn 8c06

在1GB的系统中,小页面被使用,但它们不属于一个工作集,所以它们也是不能分页的。

1: kd> !pte poi nt!MmFreePagesByColor
                                           VA fffffa800cc00000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00330    PTE at FFFFF6FD40066000
contains 0000000005C00863  contains 0000000005C01863  contains 0000000006807863  contains 0000000006808863
pfn 5c00      ---DA--KWEV  pfn 5c01      ---DA--KWEV  pfn 6807      ---DA--KWEV  pfn 6808      ---DA--KWEV
1: kd> !pfn 6808
    PFN 00006808 at address FFFFFA8000138180
    flink       00000000  blink / share count 00000001  pteaddress FFFFF6FD40066000
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 200000000080  containing page 006807  Active
1: kd> !pte poi nt!MiFreePageSlist t 80 * @@(sizeof(nt!_SLIST_HEADER))
                                           VA fffffa800cc06000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00330    PTE at FFFFF6FD40066030
contains 0000000005C00863  contains 0000000005C01863  contains 0000000006807863  contains 000000000680E863
pfn 5c00      ---DA--KWEV  pfn 5c01      ---DA--KWEV  pfn 6807      ---DA--KWEV  pfn 680e      ---DA--KWEV

1: kd> !pfn 680e
    PFN 0000680E at address FFFFFA80001382A0
    flink       00000000  blink / share count 00000001  pteaddress FFFFF6FD40066030
    reference count 0001    used entry count  0000      Cached    color 0   Priority 0
    restore pte 200000000080  containing page 006807  Active

注意:在1GB系统的计算中,我们使用的颜色范围大小等于0x80,因为这个系统是在8GB的物理机器上运行的虚拟机。在VmWare Player中,虚拟处理器的缓存大小与物理处理器相同,所以VMM在虚拟机中也使用7位的颜色索引。

由于我的物理cpu和书上的可能不一样,所以我不清楚80是否正确。

2.14 非分页池

2.14.1 区域内容

这是一个动态分配的区域,专门用于存储非分页池。静态的 MmNonPagedPoolStart 被设置为其起始地址。

VMM使用大页来映射这个区域,以节省TLB项。例如,在一个拥有1GB内存的测试系统上,我们发现:

1: kd> dq nt!MmNonPagedPoolStart l1
fffff800`046b00d8  fffffa80`0cc08000

一个大页面映射了一个2MB大小的范围,并在2MB的边界上对齐(即0x200000的倍数)。这个起始地址位于0xFFFFFA80'0CC00000范围内,它也存储了页面列表头。这个范围被映射为小页,可能是因为它没有完全用于非分页池。

1: kd> !pte fffffa80`0cc08000
                                           VA fffffa800cc08000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00330    PTE at FFFFF6FD40066040
contains 0000000005C00863  contains 0000000005C01863  contains 0000000006807863  contains 000000003FF01863
pfn 5c00      ---DA--KWEV  pfn 5c01      ---DA--KWEV  pfn 6807      ---DA--KWEV  pfn 3ff01     ---DA--KWEV

然而一旦我们越过2MB的边界,我们就会看到大页面被使用。在我们的起始地址之后,0x200000的第一个倍数是0xFFFFFA80'0CE00000;在下面的输出中,注意PDE地址如何增加了8,确认我们已经转移到下一个PDE,这个PDE被设置为映射大页面。

1: kd> !pte 0xFFFFFA80`0CE00000
                                           VA fffffa800ce00000
PXE at FFFFF6FB7DBEDFA8    PPE at FFFFF6FB7DBF5000    PDE at FFFFF6FB7EA00338    PTE at FFFFF6FD40067000
contains 0000000005C00863  contains 0000000005C01863  contains 000000003FC008E3  contains 0000000000000000
pfn 5c00      ---DA--KWEV  pfn 5c01      ---DA--KWEV  pfn 3fc00     --LDA--KWEV  LARGE PAGE pfn 3fc00

其他内核模式组件可以通过调用像ExAllocatePool这样的DDI并指定所需的字节数来分配池块。这些分配可以是任何大小的。在内部,VMM以类似于堆的方式管理非分页池,当它需要增加其大小时,它会调用MiObtainSystemVa,并最终将物理内存映射到新的VA地址。

2.14.2 虚拟地址空间管理

通过调用MiObtainSystemVa指定MiVaNonPagedPool,从该区域分配虚拟地址空间。用于管理该区域的位图是MiNonPagedPoolVaBitmap。

2.15 HAL和Loader 映射

这个区域用于系统启动期间。

免费评分

参与人数 8威望 +1 吾爱币 +26 热心值 +8 收起 理由
ZhangLangShui + 1 + 1 大佬,支持分享
oldn123 + 1 + 1 支持分享
云天灬 + 1 + 1 我很赞同!
HongHu106 + 1 + 1 谢谢@Thanks!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
明月照何处 + 1 + 1 大佬
杨辣子 + 1 + 1 用心讨论,共获提升!
hui-shao + 1 太详细了

查看全部评分

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

sam喵喵 发表于 2022-8-26 11:09
感谢大佬分享!
iawyxkdn8 发表于 2022-8-26 11:30
laomogu 发表于 2022-8-28 09:11
MrRight929 发表于 2022-8-28 09:48
感谢楼主无私分享,万分感谢~~~
MrRight929 发表于 2022-8-28 09:48
刚好马上就要用,谢谢楼主!!
zflx 发表于 2022-8-28 10:45
超级使用的东东,楼主是好人!
hnwcw1986 发表于 2022-12-8 17:30
感谢楼主,
虽然对我来说是天书一样,什么也看不懂
tianshangfly 发表于 2022-12-9 18:44
大佬,学习了,
david33 发表于 2023-1-6 23:22
感谢大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

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

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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