[翻译]深入解析Windows操作系统(下)之第十章 内存管理

参考:
[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第一部分)
[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第二部分)
[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分)
[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第四部分)

文章目录

前言

在本章中,您将了解Windows如何实现虚拟内存,以及它如何管理保存在物理内存中的虚拟内存的子集。我们还将描述其内部结构和组件它们组成了内存管理器,包括关键的数据结构和算法。在检查这些机制之前,我们将回顾内存管理器提供的基本服务和关键概念作为保留内存与提交内存和共享内存。

内存管理 简介

默认情况下,32位Windows上的进程的虚拟大小为2GB。如果映像被专门标记为大地址空间感知,并且系统通过一个特殊选项启动(稍后描述 在本章中),32位进程在32位Windows上可以增长到3GB,在64位Windows上可以增长到4GB。64位Windows上的进程虚拟地址空间大小为7,152 GB,IA64系统上的为8152GB 在x64系统上的2GB。(此值可以在未来的版本中增加。)

正如您在第1部分的第2章“系统架构”中所看到的(特别是在表2-2中),目前Windows所支持的最大物理内存量从2GB到2,048GB,具体取决于 您正在运行的是哪个版本和版本(32位/64位)。由于虚拟地址空间可能大于或小于机器上的物理内存,所以内存管理器有两个主要任务:

  • 将进程的虚拟地址空间转换或映射为物理内存,以便当在该进程的上下文中运行的线程读取或写到虚拟地址空间时,将引用正确的物理地址。(物理上驻留的进程的虚拟地址空间的子集称为工作集。我们将在本章后面更详细地描述工作集。)
  • 当内存被过度提交时,将一些内存内容分页提交到磁盘上——也就是说,当运行线程或系统代码试图使用比当前可用的更多的物理内存时——会需要的时候将内容带回物理内存。

除了提供虚拟内存管理之外,内存管理器还提供了一组核心服务,在这些服务上构建了各种Windows环境子系统。这些服务包括内存映射文件(内部称为节对象)、写入时复制内存,以及支持使用大的稀疏地址空间的应用程序。此外,内存管理器还提供了一种方法进程分配和使用的物理内存数量大于可以同时映射到进程虚拟地址空间的物理内存数量(例如,在32位系统上,物理内存超过3gb内存)。这将在本章后面的“地址窗口扩展”一节中解释。

注:
有一个控制面板小程序提供了对分页文件的大小、数量和位置的控制,它的系统命名法表明“虚拟内存”与分页文件相同。但事实并非如此。分页文件只是虚拟内存的一个方面。事实上,即使您根本不运行页面文件,Windows仍将使用虚拟内存。这种区别我在本章的后面会有更详细的解释。

内存管理组件

内存管理器是Windows执行程序的一部分,因此存在于文件Ntoskrnl.exe中。HAL中不存在内存管理器的任何部分。内存管理器包括以下内容:

  • 一组用于分配、解分配和管理虚拟内存的执行系统服务,其中大部分是通过WindowsAPI或内核模式的设备驱动程序接口公开的
  • 一个转换无效和访问故障陷阱处理程序,用于解决硬件检测到的内存管理异常,并使虚拟页面驻留在进程上
  • 六个关键的顶级例程,每个例程运行在系统进程中的六个不同的内核模式线程中的一个(参见实验“将系统线程映射到设备驱动程序”,它展示了如何识别 fy系统线程,见第1部分的第2章):
    • 平衡集管理器(平衡值设置管理器,优先级16)。它调用一个内部例程,工作集管理器(管理工作集管理器),每秒一次,以及当空闲内存低于certa时 在阈值。工作集管理器驱动整个内存管理策略,如工作集修剪、老化和修改后的页面写入。
    • 进程/堆栈交换器(关键交换进程或堆栈,优先级23)同时执行进程和内核线程堆栈内交换和外交换。属性中的平衡集管理器和线程调度代码 当需要发生内交换或外交换操作时,内核会唤醒这个线程。
    • 修改的页面写入器(错误修改页面写入器,优先级17)将修改列表中的脏页写回相应的分页文件。当修改列表的大小时,此线程被唤醒 需要减少。
    • 映射页面写入器(MiMappedPageWi写入器,优先级17) 将映射文件中的脏页面写入磁盘(或远程存储)。当需要将修改后的列表的大小减小,或者如果 映射文件的页面已在修改列表中超过5分钟。这第二个修改后的页面写入器线程是必要的,因为它可以生成导致fr请求的页面错误 ee页面。如果没有免费页面,而只有一个修改过的页面写入器线程,系统可能会死锁,等待免费页面。
    • 段取消引用线程(错误引用段线程,优先级18)负责减少高速缓存,以及页面文件的增长和收缩。(例如,如果没有虚拟地址空间对于分页池的增长,这个线程会修剪页面缓存,以便用于固定它的分页池可以被释放以供重用。)
    • 零页线程(MmZeroPage线程,基本优先级0)排除空闲列表中的页面,以便零页面的缓存可以满足未来的需求-零页面故障。不像其他的例程 这里描述的缺点是,这个例程不是一个顶级线程函数,而是由顶级线程例程 Phase1Initialization. MmZeroPageThread调用的。该线程永远不会返回给它的调用者,所以实际上是t 阶段1初始化线程通过调用这个例程成为零页线程。在某些情况下,内存归零是由一个称为MiZero-并行的更快的函数来完成的。参见本部分的说明” 页面列表动态”将在本章的后面介绍。

这些组件将在本章后面详细介绍。

内部同步

与Windows执行器的所有其他组件一样,内存管理器是完全可复用(fully reentrant)的,并支持在多处理器系统上同时执行——也就是说,它允许两个线程以不损坏彼此数据的方式获取资源。为了实现完全可复用的目标,内存管理器使用了几种不同的内部同步机制,例如 旋锁,控制对其内部数据结构的访问。(同步对象将在第1部分的第3章“系统机制”中进行讨论。)

内存管理器必须同步访问到的一些系统范围内的资源,包括:

  • 系统虚拟地址空间的动态分配部分
  • 系统工作集
  • 内核内存池
  • 已加载的驱动程序的列表
  • 分页文件的列表
  • 物理内存列表
  • 图像基随机化(ASLR)结构
  • 页帧编号(PFN)数据库中的每个单独的条目

需要同步的每个进程内存管理数据结构包括工作集锁(在对工作集列表进行更改时保持)和地址空间锁(保持 每当地址空间被更改时)。这两个锁都是使用推锁(pushlocks)来实现的。

检查内存使用情况

内存和进程性能计数器对象提供了对有关系统和进程内存利用率的大部分详细信息的访问。在这一章中,我们将包括对特定pe的引用 包含与要描述的组件相关的信息的性能计数器。我们在这一章中都包含了相关的例子和实验。然而,有一句警告是:不同的 在显示内存信息时,实用程序使用不同的、有时是不一致的或令人混淆的名称。下面的实验说明了这一点。(我们将解释本例中使用的术语 在随后的章节中。)

以下屏幕截图中显示的窗口任务管理器中的“性能”选项卡将显示基本的系统内存信息。此信息是可用的详细内存信息的一个子集 您可以通过性能计数器。它包括关于物理内存和虚拟内存使用情况的数据。
在这里插入图片描述
下表显示了与内存相关的值的含义。

任务管理器值定义
内存直方图 (Memory)条形/图表线高度显示Windows正在使用的物理内存(不能用作性能计数器)。图的剩余高度等于物理内存se中的可用计数器 连接,后面在表中描述。该图的总高度等于该部分中的总计数器。这表示操作系统可用的总RAM,并且不包括 BIOS影子页面、设备内存等。
物理内存(MB):总共(Physical Memory (MB): Total)可使用的物理内存
物理内存(MB):缓存(Physical Memory (MB): Cached)内存对象中下列性能计数器的和:高速缓存字节、修改的页列表字节、备用高速缓存核心字节、备用高速缓存正常优先级字节和备用高速缓存保留字节 (所有在内存对象中)
物理内存(MB):可用 (Physical Memory (MB): Available)操作系统、进程和驱动程序可立即使用的内存量。等于备用、空闲和零页列表的合并大小。
物理内存(MB):空闲(Physical Memory (MB): Free)空闲和零页面列表字节
内核内存(MB):分页 (Kernel Memory (MB): Paged)池分页字节。这是池的总大小,包括自由区域和已分配区域
内核内存(MB):非分页 (Kernel Memory (MB):
Nonpaged)池非分页字节。这是池的总大小,包括自由区域和已分配区域
系统:提交(显示两个数字)(System: Commit (two numbers shown))分别等于性能计数器提交字节和提交限制

要查看分页池和非分页池的具体用法,请使用“监视池使用情况”部分中描述的Poolmon实用程序。
来自窗口系统内部(http://www.microsoft.com/technet/sysinternals)的进程资源管理器工具可以显示更多关于物理内存和虚拟内存的数据。在其主屏幕上,单击“查看” 然后是系统信息,然后选择内存选项卡。

下面是一个来自32位Windows系统的显示示例:
在这里插入图片描述
我们将在本章后面的相关部分中解释大多数这些额外的计数器。

另外两个系统内部工具显示扩展内存信息:

  • VMMap以非常精细的细节级别显示了进程中虚拟内存的使用情况。
  • RAMMap显示了详细的物理内存使用情况。
    这些工具将在本章后面的实验中进行介绍。

最后,内核调试器中的!vm命令显示了通过与内存相关的性能计数器可用的基本内存管理信息。如果你正在寻找,这个命令可能会很有用 在崩溃转储或挂起系统。下面是一个从4gb的Windows客户端系统输出的示例:
在这里插入图片描述
我们将在本章的后面描述这个命令的输出的许多细节。

由内存管理器提供的服务

内存管理器提供了一组系统服务来分配和释放虚拟内存,在进程之间共享内存,将文件映射到内存,将虚拟页面刷新到磁盘,检索有关虚拟页面范围的信息,更改对虚拟页面的保护,并将虚拟页面锁定到内存中。
与其他Windows执行服务一样,内存管理服务允许其调用者提供一个进程句柄,指向要操作其虚拟内存的特定进程。这个因此,调用者可以操作它自己的内存或(具有适当的权限)的另一个进程的内存。例如,如果一个进程创建了一个子进程,默认情况下它有权限 操作子进程的虚拟内存。此后,父进程可以通过调用虚拟内存服务,并将一个句柄作为参数传递给子进程,从而代表子进程来分配、解除分配、读取和写内存。此功能被子系统用于管理其客户机进程的内存。它对于实现调试器也很重要,因为调试器 必须能够读写被调试进程的内存。

这些服务大多是通过WindowsAPI公开的。WindowsAPI有三组用于管理应用程序中的内存的函数:堆函数(Heapxxx和旧的本地接口 xxx和Globalxxx,内部使用Heapxxxapi),可用于分配小于页面的位置;虚拟内存功能,使用页面粒度(Virtualxxx);一个 d内存映射文件函数(创建文件映射,创建文件映射,文件视图,文件视图和文件视图)。(我们将在本章的后面描述堆管理器。)
内存管理器还将许多服务(例如分配和释放物理内存以及锁定物理内存中的页面以便直接内存访问[DMA]传输)到执行器内部的其他内核模式组件以及设备驱动程序。这些函数以前缀Mm开头。此外,虽然不是严格的内存管理器的一部分,但一些以Ex开始的执行支持例程被用于分配和释放从 系统堆(分页和非分页池)以及操作备用列表。我们将在本章后面的“内核模式堆(系统内存池)”一节中讨论这些主题。

大页面和小页面

虚拟地址空间划分单位被称为页面。这是因为硬件内存管理单元将页面粒度上的虚拟地址转换为物理地址。

因此,一个页面是在硬件级别上最小的保护单元。(本章后面的“保护内存”部分介绍了各种页面保护选项。)在Windows运行的处理器支持两种页面尺寸,称为小页面尺寸(Small Page Size)和大页面尺寸(Large Page Size)。实际支持的尺寸根据处理器架构的不同而不同,它们列在表10-1中。
在这里插入图片描述

注意:IA64处理器支持各种动态可配置的页面大小,从4KB到256MB。
安腾腾上的Windows分别对小页面和大页面使用8kb和16mb,因为确认这些值为最优值的性能测试。此外,最近的x64处理器支持1GB大小的大页面,但Windows不使用此功能。

大页面的主要优点是快速引用大页面内其他数据的地址转换速度。这种优势的存在是因为第一次引用了一个大范围内的任何字节 页面将导致硬件的转换查看备用缓冲区(TLB,在后面的部分中描述) 在其缓存中包含将引用转换到l中的任何其他字节所需的信息 arge页面。如果使用小页面,对于相同范围的虚拟地址需要更多的TLB条目,从而增加条目的回收,因为新的虚拟地址需要转换。这一点,反之,是指当引用已缓存翻译的小页面范围之外的虚拟地址时,必须返回到页面表结构。TLB是一个非常小的c 因此,大的页面可以更好地利用这种有限的资源。

为了利用具有超过2GB内存的系统上的大页面,Windows用大页面映射核心操作系统映像(Ntoskrnl.exe和Hal.dll)以及核心操作系统数据(例如非分页池的初始部分和描述每个物理内存页面状态的数据结构)。Windows还可以自动映射I/O空间请求(由设备驱动程序调用 如果请求是令人满意的大页面长度和对齐。此外,Windows允许应用程序映射它们的图像、私有内存和页面文件-bac 带有大页的可变部分。(请参阅虚拟代理、虚拟代理代理和虚拟代理函数上的MEM_LARGE_PAGE标志。)您还可以指定要映射的其他设备驱动程序 页面,通过将一个多字符串注册表值添加到HKLM\SYSTEM\CurrentControlSet\ Control\Session Manager\Memory Management\LargePageDrivers,并指定将驱动程序的名称指定为以空字符结尾的字符串。

在操作系统长期运行一段时间后,尝试分配大页面可能会失败,因为每个大页面的物理内存必须占用相当的数量(参见T 能够10-1)物理上连续的小页面,并且这个范围的物理页面必须进一步从一个大的页面边界开始。(例如,物理页面0到511可以被用作一个大的 x64系统上的页面,物理页面512到1023页可以,但10到521页不能。) 随着系统的运行,可用的物理内存确实会变得支离破碎。这对于使用小页面的分配不是问题,但可能会导致大页面分配失败。(内存碎片问题)

除了对大页面的读/写访问外,不能指定任何东西。内存也总是不可分页的,因为页面文件系统不支持大页面。而且,由于内存是不可分页的,因此它不被认为是进程工作集(descr)的一部分 我在ibed后)。大型页面分配也不受作业范围内虚拟内存使用的限制。

大尺寸页面有一个不幸的副作用。
每个页面(无论大或小)都必须具有适用于整个页面的单一保护(因为硬件内存保护是以每页为基础的内容)。例如,如果一个大页面包含只读代码和读/写数据,该页面必须标记为读/写,这意味着代码是可写的。这意味着它 在设备驱动程序或其他内核模式代码中,由于bug,可以修改应该是只读的操作系统或驱动程序代码,而不会导致内存访问冲突。如果小尺寸页面用于映射操作系统的内核模式代码,Ntoskrnl.exe和Hal.dll的只读部分可以映射为只读页面。使用较小的页面确实会降低地址的效率 但是,如果设备驱动程序(或其他内核模式代码)试图修改操作系统的只读部分,系统将立即崩溃,异常信息指向驱动的违规指令。如果允许发生写入,那么当其他组件试图使用损坏的数据时,系统可能会崩溃(以一种更难诊断的方式)。

如果您怀疑您遇到了内核代码损坏,请启用驱动程序验证器(本章后面将进行描述),这将禁用大页面的使用。

预订(Reserving)和提交(Committing)页面

进程虚拟地址空间中的页面分为空闲的(free)、保留的(reserved)、已提交的(committed)或可共享的 这几种状态。

Committed和Shareable的页面是指这些页面,当访问时,最终转化为有效的物理页面内存。

已提交的页面(Committed Page)也被称为 私有页面(Private Pages)。这反映了这样一个事实,即已提交的页面不能与其他进程共享,而可共享的页面可以被共享(但是,当然,可能是在 只有一个进程使用)。

提交限制

在任务管理器的性能选项卡,“提交(GB)”后面跟着2个数值。内存管理器在全局基础上,跟踪记录私有提交内存的使用情况,这被称为commitment 或 commit charge,即第一个数值,它代表系统中所有提交的虚拟内存总合。还有一个系统级限制,叫做system commit limit 或者简称 commit limit,这个限制对应当前的所有分页文件总大小,增加可被操作系统使用的物理内存总量,即第二个数值。内存管理器可以通过扩展一个或多个分页文件,自动增加commit limit上限。(如果它们尚未配置最大上限)。

本章后面部分将详细解释Commit charge 与 system commit limit。

锁定内存

通常,最好是让内存管理器来决定哪些页面仍保留在物理内存中。但是,在某些特殊情况下,应用程序或设备驱动程序可能需要将页面锁定在物理内存中。

页面可以通过两种方式锁定在内存中:

  • Windows应用程序可以调用“VirtualLock”函数来锁定其进程工作集中的页面。使用此机制锁定的页面将保留在内存中,直到显式解锁或锁定它们的进程终止。进程可以锁定的页面数不能超过其最小工作集大小减去8页。因此,如果一个进程需要锁定更多的页面,它可以使用SetProcessWorkingSetSizeEx函数(在“工作集管理”一节中提到)来增加其最小工作集。
  • 设备驱动程序可以调用内核模式函数、、代码段、数据段或MmLockPagableSectionByHandle.使用此机制锁定的页面在显式解锁。最后三个api对可以锁定在内存中的页面数量没有配额,因为在驱动程序第一次加载时获得了居民可用的页面费用;这确保了它永远不会由于过锁而导致系统崩溃。对于第一个API,必须获得配额费用,否则API将返回一个失败状态。

分配粒度

Windows将每个保留的进程地址空间区域,按照一个完整的边界为起始进行对齐,这由系统分配粒度值定义,该值可以通过Windows函数GetSystemInfo 或 GetNativeSystemInfo 检索。
这个值的大小为64KB,是被内存管理器使用的一个粒度,用于高效分配元数据(例如VADs,位图等等)来支持各种进程操作。此外,如果往后的处理器增加了对大页尺寸的支持(例如,最多到64KB),或者增加虚拟索引缓存(要求全系统的物理页对齐虚拟页),将减少变成要求应用程序来假设分配对齐(粒度)的风险。

注意:Windows内核模式代码不受相同的限制;它可以保留单页粒度保留内存(尽管由于前面详细介绍的原因,这没有暴露给设备驱动程序)。这种粒度级别主要用于更密集地打包TEB分配,而且由于这种机制只是内部的,因此如果未来的平台需要不同的值,则可以很容易地更改此代码。此外,为了仅在x86系统上支持16位和MS-DOS应用程序,内存管理器为MapViewfFileExAPI提供了MEM_DOS_LIM标志,该API用于强制使用单页粒度。

最后,当保留一个地址空间区域时,Windows确保该区域的大小和基数是系统页面大小的倍数,无论它是多少。例如,由于x86系统使用4-KB的页面,如果您试图保留一个大小为18 KB的内存区域,那么在x86系统上实际保留的量将是20KB。如果为18kb区域指定了3 KB的基本地址,实际保留量将为24KB。请注意,分配的VAD也将被四舍五入到64kb的对齐/长度,从而使它的其余部分无法访问。(本章后面将介绍VADs。)

共享内存和映射文件

与多数现代操作系统一样,Windows 提供一种机制用于在进程和操作系统之间共享内存。共享内存可以被定义为:多于一个进程可见的,或者存在于多个进程虚拟地址空间中的内存。举例来说,如果 2 个进程使用相同的 DLL,合理的做法是,只需要将该 DLL 中被引用的代码页加载进物理内存一次,并且在所有映射该 DLL 的进程间共享那些页面,
如图 10-1 所示:
在这里插入图片描述
每个进程将仍旧维护自己的私有内存区域,在其中存储私有数据,但是 DLL 代码页面和未修改数据页面(意味着可写)可以被无损害地共享。正如我们稍后将解释的,这种形式的共享会自动发生,因为可执行映像(.exe,.dll文件,以及几种其它类型文件,例如屏幕保护文件[.scr],包含其它必需的DLLs名称)中的代码页被映射为仅执行;可写页被映射为写时复制。(更多内容参见“写时复制”小节)
内存管理器中用来实现共享内存的底层原语叫做 section objects(暂译为"section 对象" ),它作为(或通过)Windows API 中的文件映射对象对外暴露。
本章后面的“Section Objects”小节将解释 section 对象的内部结构与实现。
内存管理器中的这个基本原语用于映射虚拟地址,无论是在主存中,分页文件中,或者在应用程序要访问的一些其它文件中,通过 section 对象,在进程看来就好像在内存中一样。一个 section 可以被一个或多个进程开启;换句话说,section 对象不一定等同于共享内存。
一个 section 对象可以被连接到一个打开的磁盘文件(叫做映射文件)或连接到提交的内存(用于提供共享内存)。后者叫做 page-file-backed sections(页面文件备份 sections),这是由于,如果有对物理内存的需求,该提交页将被写入页面文件(而不是映射文件)。(由于 Windows 可以完全不使用页面文件,页面文件备份 sections 实际上可能仅“备份”在物理内存中)正如任何其它用户模式可见的空页面(例如私有提交页面),当共享提交页面被首次访问时,总是用零填充(以前的页面内容),确保没有敏感数据被泄漏。

使用 Windows 函数 CreateFileMapping 或 CreateFileMappingNuma 创建 section 对象,指定要映射到的文件句柄(或者,对于页面文件备份 sections ,指定 INVALID_HANDLE_VALUE 参数),以及一个可选的名称,一个安全描述符。如果有节名称,其它进程可以通过 OpenFileMapping 函数将其打开。或者,你可以使用句柄继承(通过在打开或创建句柄时,指定要被继承的句柄)和句柄复制(通过使用 DuplicateHandle 函数)授予对该节对象的访问。设备驱动程序也可以使用 ZwOpenSection,ZwMapViewOfSection,以及 ZwUnmapViewOfSection 等函数操纵 section 对象。

HANDLE CreateFileMapping(
  HANDLE hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD flProtect,
  DWORD dwMaximumSizeHigh,
  DWORD dwMaximumSizeLow,
  LPCTSTR lpName
);

第一个参数就是要映射到的文件句柄,其值为 INVALID_HANDLE_VALUE,即无效的句柄值时,实际上是创建了实现共享内存的内核对象(section 对象),因为没有任何文件句柄与一个打开的磁盘文件关联;第二个参数是一个 SECURITY_ATTRIBUTES 结构类型的变量;
该结构定义如下:

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;      //只有该成员与安全性有关
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

其中,第二个成员就是原文提及的“安全描述符”;如果我们想对创建的内核对象施加访问控制,就必须创建一个 lpSecutrtyDescripter,分配并初始化 SECURITY_ATTRIBUTES 结构:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecutrtyDescripter = pSD;
sa.bInheritHandle = FALSE;

调用创建内核对象的函数时,将 SECURITY_ATTRIBUTES 结构变量的地址作为第二个实参传入:

HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 0, 1024, TEXT("MyFileMapping"));

最后一个参数就是原文提及的可选的 section 名称,使用 TEXT 宏的目的在于根据编译器的UNICODE 设置情况,自动转换名称为 ASCII/ANSI 或 unicode 字符串。原文提到,如果有section 名称,其它进程可以通过 OpenFileMapping 函数将其打开,也就是如下调用:

HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, TEXT("MyFileMapping"));

将 FILE_MAP_READ 作为第一个参数传给 OpenFileMapping,表明在获得对这个内核对象的访问权后,要从中读取数据。OpenFileMapping 函数在返回一个有效的句柄值前,会先执行一次安全检查。如果当前登录的用户或以特定用户帐户身份执行的进程被允许访问该文件映射内核对象(section 对象),OpenFileMapping 返回一个有效的句柄值;如果访问被拒绝,则返回 NULL;此时调用 GetLastError,返回值为 5(ERROR_ACCESS_DENIED)。同样的,如果利用返回的有效句柄调用其它 Windows API,但被调函数需要的权限不是 FILE_MAP_READ,也会发生拒绝访问错误。

下面的例子中,父进程在创建一个互斥量内核对象时,使用了句柄继承:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecutrtyDescripter = NULL;
sa.bInheritHandle = TRUE;
 
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

根据 MSDN 上相关内容的解释,在调用需要 SECURITY_ATTRIBUTES 结构实例作为参数的函数时(多数创建内核对象的函数都要求此参数),如果将其设置为 NULL,或者将其 lpSecutrtyDescripter 成员设置为 NULL(如上述代码所示),那么创建的对象具有默认安全性——来自于调用或创建者的安全令牌(例如 ACLs ,访问控制列表)。
其次,如果将 SECURITY_ATTRIBUTES 结构的 bInheritHandle 成员设置为 TRUE,然后向 CreateMutex() 传入这个结构实例的地址,那么该函数返回的互斥量句柄值是可继承的。如此一来,在父进程的内核对象句柄表中将添加一项互斥量句柄值,其“可继承的标志位” = 1。
接着,父进程调用 CreateProcess() 创建子进程时,将前面的句柄值作为 CreateProcess() 的第2个参数 pszCommandLine 传递,并且将第5个参数 bInheritHandles 的值设为 TURE(表明“主动”请求继承;反之如果 bInheritHandles = FALSE ,则无论 SECURITY_ATTRIBUTES 结构的 bInheritHandle 成员值为何,都不会继承 ),这样子进程就会继承父进程句柄表中,可继承的标志位 = 1 的句柄。然后父子进程可以使用相同的句柄值访问同一个互斥量对象。CreateProcess() 函数原型如下:

BOOL WINAPI CreateProcess(
  _In_opt_       LPCTSTR                                     lpApplicationName,
  _Inout_opt_  LPTSTR                                       lpCommandLine,
  _In_opt_       LPSECURITY_ATTRIBUTES           lpProcessAttributes,
  _In_opt_       LPSECURITY_ATTRIBUTES           lpThreadAttributes,
  _In_              BOOL                                          bInheritHandles,
  _In_              DWORD                                      dwCreationFlags,
  _In_opt_       LPVOID                                       lpEnvironment,
  _In_opt_       LPCTSTR                                     lpCurrentDirectory,
  _In_              LPSTARTUPINFO                         lpStartupInfo,
  _Out_            LPPROCESS_INFORMATION        lpProcessInformation
);

CreateProcess() 是一个复杂的函数,它拥有 10 个参数,每个参数都足够复杂,因此想要用好 CreateProcess() 并不容易。详细用法可以参考 MSDN 网站上的原文;日后有机会再发一帖该原文的原创翻译。
句柄继承导致增加内核对象的使用计数,因为父子进程引用相同的对象;仅当父子进程都调用 CloseHandle() 关闭引用该对象的句柄,或者父子进程都退出,让使用计数 = 0,该对象才会实际在内核空间中被销毁。强调一下,如果此后父进程又创建新的内核对象并将其设置为可继承的,现存子进程并不会自动继承新的对象,句柄继承仅发生在 CreateProcess() 调用并传入了相关参数时。

一个 section 对象可以引用远大于一个进程地址空间能够容纳的文件。(如果在分页文件中备份一个 section 对象,分页文件以及/或者物理内存中必须预留足够的空间包含它,如前所述,这是由于取决于系统的内存使用情况,section 对象需要在两者间进行读写操作)如果进程要访问一个非常大的 section 对象,可以仅映射实际需要的一部分(称为 section 视图),这可以通过调用 MapViewOfFile,MapViewOfFileEx,或者MapViewOfFileExNuma 函数,并且指定要映射的范围。这种机制允许进程节约地址空间,因为只有在需要该 section 对象的部分内容时,才被映射进内存。

Windows 应用程序可以使用映射文件,通过简单地让它们出现在调用进程地址空间中,方便对其执行 I/O 操作。用户应用程序并不是唯一的 section 对象“消费者”:映像加载器也使用 section 对象来将可执行映像,DLLs,以及设备驱动程序映射进内存,还有缓存管理器也使用 section 对象访问缓存文件中的数据。(更多有关缓存管理器如何与内存管理器集成在一起的信息,参见第11章“缓存管理”)
本章后面会解释共享内存部分的实现,包括地址翻译和内部数据结构两方面。

实验:查看内存映射文件

你可以通过使用来自 Sysinternals 的进程浏览器,列出一个进程中的内存映射文件。要这么做,将进程浏览器的下窗格配置成显示“DLL view”(点击主菜单中的”View“->“Lower Pane View”->“DLLs”。)注意,DLL view 不仅仅是一份 DLLs 列表——它代表该进程地址空间中的所有内存映射文件。其中一些是 DLLs,有一个是正在运行进程的对应磁盘映像文件(EXE),其它项目可能代表着内存映射数据文件。举例来说,下面的进程浏览器截图显示出一个 WinDbg 进程使用几种不同的内存映射来访问被审查的内存转储文件。如同多数 Windows 程序,该进程(或者它正使用的其中一个 Windows DLL)亦使用内存映射来访问一个叫做 Locale.nls 的 Windows 数据文件;Locale.nls 是 Windows 中支持国际化的组成部分。你也可以通过点击主菜单“Find”->“DLL”,来搜索内存映射文件。在试图判断那个或哪些进程正使用一个 DLL,或者尝试替换一个内存映射文件时,这可能会有所帮助。

在这里插入图片描述

保护内存

如同在本书上册第一章“概念和工具”中解释的,Windows 提供内存保护机制, 以至于没有用户进程可以无意或刻意损坏另一个进程或操作系统的地址空间。

Windows 通过4种主要方式提供内存保护:

  • 第一,所有内核模式系统组件使用的系统级别数据结构和内存池,只能够在内核模式下被访问——用户模式线程不能够访问这些页面。如果试图这么做,硬件生成一个错误,从而内存管理器反过来向该线程报告一个非法访问。
  • 第二,每个进程有一个独立,私用的地址空间,防止被属于另一进程的任何线程访问。甚至共享内存也不例外,因为每个进程使用其自身虚拟地址空间的一部分地址,访问该共享区域。唯一的例外是,如果另一个进程有该进程对象的虚拟内存读或写访问需求(或者持有SeDebugPrivilege权限),可以使用ReadProcessMemory 或 WriteProcessMemory函数。每当一个线程引用一个地址,虚拟内存硬件(译注:通常是处理器内置的MMU,即内存管理单元)与操作系统内存管理器配合,介入其中并且将虚拟地址翻译物理地址。Windows通过控制虚拟地址被翻译的方式,可以确保一个进程中运行的线程不会有对属于另一个进程页面的不当访问。
  • 第三,除了提供虚拟到物理地址翻译的隐式保护之外,所有Windows支持的处理器提供某种形式的“硬件控制内存保护机制”(例如读/写,只读等等);这种保护机制的具体细节因处理器而异。
    例如,一个进程地址空间中被标记为只读的代码页因而能够防止被用户线程修改。
    表10-2列出了Windows API 中定义的内存保护选项(参见 VirtualProtect,VirtualProtectEx,VirtualQuery,以及VirtualQueryEx函数的说明文档)
  • 最后,共享内存节对象有标准的Windows访问控制列表(ACLs),当进程尝试打开它们时,将检查相应的ACL,从而给予那些进程适当的权限来限制对共享内存的访问。当一个线程创建一个节对象来包含一个映射文件时,访问控制也能够发挥作用。该线程必须至少有对底层文件对象的读访问权限,否则操作将失败。

一旦线程成功的打开到一个节的句柄,其行为仍旧受到内存管理器与基于硬件的页面保护限制。一个线程可以更改节对象中虚拟页的页级保护属性,前提是这个更改没有违反该节对象ACL中的权限。举例来说,内存管理器允许一个线程将一个只读节中的页面改变为写时复制访问,但是不能改变为可读写访问。允许改变为写时复制访问是因为这不会影响其它进程共享只读的数据。(译注:即前文讲过的,在线程请求写入时,创建一个只有该线程可见的私有副本页,其对进行写操作不会改变原始的只读共享页)
在这里插入图片描述
在这里插入图片描述

不可执行页面保护 (No Execute Page Protection)

不可执行页保护(也被称为数据执行保护,或DEP)导致试图转移(对CPU的)控制到一个标记为“不可执行”页中的指令时,生成一个访问错误。这可以防止某些类型的恶意软件通过将可执行代码放置在比如栈这种数据页中,从而利用系统中的缺陷或漏洞。DEP也能够捕捉到那些编写不当的程序,这些程序没有为它们打算从中执行代码的页面设置正确的权限。

假设在内核模式下尝试在一个标记为不可执行的页中执行代码,系统会崩溃,并且错误检查码为ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY。(参见第14章,“崩溃转储分析”,其中有对这些代码的解释)

如果上述情况发生在用户模式,会传递一个STATUS_ACCESS_VIOLATION(0xc0000005)异常到尝试非法引用的线程。如果一个进程申请分配的内存需要能够被执行,进程必须通过在页面粒度内存分配函数中指定PAGE_EXECUTE,PAGE_EXECUTE_READPAGE_EXECUTE_READWRITE,或者PAGE_EXECUTE_WRITECOPY标志,来显式标记此类页面。

在支持DEP的32位x86系统上,页表条目(PTE)中的比特位63(以及页目录项PDE中的比特位63),用于标记一个页(对于PDE而言,则是该PDE引用的页表中的所有页)是否为不可执行。故而仅当处理器以物理地址扩展(PAE)模式运行时,DEP功能才可用;对于32位宽的页表条目,则不支持DEP。(参见本章后面的“物理地址扩展”部分。)于是,在32位系统上支持硬件DEP,需要加载PAE内核(即 %SystemRoot%\System32\Ntkrnlpa.exe),即使该系统并不需要扩展物理寻址(例如大于4GB的物理地址)。在支持硬件DEP的32位系统上,操作系统加载器会自动载入PAE内核。要在支持硬件DEP的系统上强制加载非PAE内核,BCD 选项 nx 必须设置成 AlwaysOff,并且 pae 选项必须设置成 ForceDisable。( 译注:在CMD命令行提示符下切换到 C:\Windows\System32,执行 bcdedit 命令,即可显示当前的BCD 选项 nx 的值,并对其进行更改;另外,该命令也可以强制启用或禁用OS的PAE功能,如下图所示)
在这里插入图片描述
此外,还需要检查注册表项HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management,其右侧名称为 PhysicalAddressExtension 的 REG_DWORD 类型值是否为16进制的1,1表示启用PAE,0表示禁用PAE。最后,当IA32_EFER 寄存器的 NXE 标志置1,并且PDE/PTE 的比特位63(XD,第64位)置1时,就无法从被引用的页中取指令,这些页可能用于栈,数据段,以及堆。
在这里插入图片描述
在64位版本的Windows上,所有的64位进程和设备驱动程序总是应用了执行保护机制,并且只能通过将 nx BCD 选项设置成 AlwaysOff 来禁用。而对于32位程序的执行保护则取决于系统配置的设置,这将在稍后描述。在64位Windows上,执行保护被应用于线程栈(包括用户与内核模式),没有明确标记为可执行的用户模式页面(译注:请参考表10-2),内核可分页池,以及内核会话池(参见本章后面的“内核模式堆[系统内存池]”小节。)然而,在32位Windows上,执行保护仅被应用于线程栈与用户模式页面,对于内核可分页池以及会话池,则没有应用执行保护。

对于32位进程的应用程序执行保护,取决于 BCD nx 选项的值。
此设置可以通过如下操作来更改:

在桌面上右击“计算机”图标->“属性”->“高级系统设置”->点击“高级”选项卡的“性能”栏目的“设置”->“数据执行保护”选项卡。(参见下图)

当您在性能选项对话框中配置不可执行保护时,BCD nx 选项会被设置成相应的值。表10-3列出了它的取值,以及相应的DEP设置。在注册表键 HKLM\SOFTWARE\Microsoft\Windows NT \CurrentVersion\AppCompatFlags\Layers 下,列出了排除在执行保护外的32位应用程序,其键值包含被排除在外的可执行文件完整路径,并且其“数值数据”被设置为“DisableNXShowUI”。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对于Windows的客户机版本(包括64位与32位),默认情况下,32位进程的执行保护被配置为,仅应用于核心的Windows操作系统可执行文件(nx BCD 选项被设置为 OptIn),以至于不会打断一些32位应用程序的执行,它们可能需要在没有明确标记为可执行的页中执行代码,例如自解压的,以及加壳的应用程序。(译注:查看其PE文件结构即可知道,某些加壳程序需要运行的脱壳代码可能就没有显式标记为可执行,而默认配置在方便一些正常自解压程序的同时,也会增加恶意代码被成功执行的风险);对于Windows的服务器版本,默认情况下,32位应用程序的执行保护被配置为应用于所有32位程序(nx BCD 选项被设置为 OptOut)。

注意 要获得一份受保护程序的完整列表,请安装从www.microsoft.com下载的Windows Application Compatibility Toolkit,然后运行Compatibility Administrator Tool,点击 “System Database”->“Applications”->“Windows Components”,右侧面板显示出受保护的可执行文件列表。

即便你强制启用DEP,应用程序仍然可以通过其它办法为其自身映像禁用DEP。举例来说,无论是否启用执行保护选项,映像加载器(参见本书上册第3章)都将针对已知的防拷贝机制(例如SafeDisc 和 SecuROM)验证可执行文件的签名,并且禁用执行保护,提供对较早的防拷贝软件,如计算机游戏等的兼容性。(译注:一个广为人知的例子是即时战略游戏“星际争霸”的硬盘版,如果没有破解补丁去除它的防拷贝机制,运行时会提示插入光盘,也就不是完美的硬盘版)

几种设置DEP方法:

  • 性能选项设置DEP/注册表设置
  • vs 编译选项设置 DEP
  • API设置 SetProcessDEPPolicy

几种禁用DEP方法:

  • API设置 SetProcessDEPPolicy

实验 查看进程的DEP信息

进程浏览器可以显示您系统上所有进程的当前DEP状态,包括进程是否已选择加入,或受益于永久性DEP。要查看进程的DEP状态,在进程浏览器主界面进程数列表的任意列上右击,“选择列”,在打开的对话框中切换到“Process Image”选项卡,勾选其中的“DEP Status”复选框即可。可能的取值有3种:
■ DEP(永久性) 表示该进程启用了DEP,因为它是一个“必要的”Windows程序或服务。
■ DEP 表示该进程“选择加入”DEP,这可能是由于选择了所有32位进程的系统级策略导致的,例如对SetProcessDEPPolicy API函数的调用(动态DEP配置),或者在构建该进程对应的映像文件时,设置了/NXCOMPAT 链接器标志(静态DEP配置)。(译注:设置方法参考下图)
在这里插入图片描述

■ Nothing 如果进程的DEP状态列没有显示任何信息,表示禁用了DEP,这要么是因为一个系统级的策略,要么是一个显式的 API 调用导致的。(译注:最后的单词shim不知作何解释)
在这里插入图片描述
此外,为提供对较早版本的活动模板库(ATL)框架的兼容性(7.1或更早版本),Windows内核提供了一个ATL thunk 模拟环境。该环境检测导致DEP异常的ATL thunk代码序列,并且模拟预期的操作。应用程序开发人员可以通过最新的Microsoft C++ 编译器,指定 /NXCOMPAT 标志(前文提到过,并且这将在PE文件头中设置 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 标志),用于告知系统这个可执行文件完全支持DEP;如此就能够向内核请求不对该程序应用ATL thunk模拟环境。需要注意,如果BCD nx 值被设置为AlwaysOn,将永久禁用ATL thunk模拟。

最后,如果系统处于OptIn 或 OptOut 模式(也就是启用动态DEP配置),并且正执行一个32位进程,函数SetProcessDEPPolicy允许该进程动态禁用DEP,或者永久启用DEP。(译注:请参考表10-3)(一旦通过此API启用,在该进程生命周期内,DEP无法通过编程方式禁用。)此函数也可用于动态禁用ATL thunk 模拟,防止没有使用 /NXCOMPAT 标志来编译生成映像文件。SetProcessDEPPolicy() 的原型声明在 Winbase.h 头文件中:

BOOL WINAPI SetProcessDEPPolicy(__in  DWORD dwFlags);

如果 dwFlags 的值为1,则在进程生命周期内永久启用DEP。
对于64位进程,或者系统以AlwaysOff/AlwaysOn 选项启动,则该函数总是返回失败。函数GetProcessDEPPolicy返回每个32位进程的DEP策略(在64位系统上调用此函数会失败,因为此时DEP策略总是“启用”),而函数GetSystemDEPPolicy可用于返回(查询)与表10-3中列出策略相对应的值。

软件数据执行保护

对于不支持硬件不可执行保护的较早处理器,Windows也提供了有限的软件数据执行保护(DEP)。软件DEP的一个方面是减少对Windows中异常处理机制的漏洞利用可能性。(参见本书上册第3章对结构化异常处理的介绍。)如果程序的二进制映像文件通过安全的结构化异常处理(Microsoft Visual C++ 编译器中的一个功能,通过 /SAFESEH 标志启用)构建,在分发一个异常前,系统会验证该异常处理程序是否在位于映像文件内的函数表(由编译器构建)中注册。
(译注:下图给出在 Visual Studio 2010 中,对欲构建的PE文件,配置启用/SAFESEH 标志的方法。)

在这里插入图片描述
在 VS 2015 中,改选项的中文解释大致相同:
在这里插入图片描述
前述的机制依赖于程序的映像文件是否通过安全的结构化异常处理来构建。如果不是,软件DEP保护针对x86进程栈上的结构化异常处理链,阻止其被覆写,这是通过一个被称为Structured Exception Handler Overwrite Protection(结构化异常处理程序覆写保护,SEHOP)的机制完成的。
当一个线程首次开始在用户模式执行时,一个新的符号异常注册记录被加入到栈上。普通的异常注册链将指向(lead to)此记录。当发生一个异常时,异常分发器将首先遍历(walk)异常处理程序注册记录列表,确保该链连接到此符号记录。如果不是这样,该异常链必定已损坏(不是无意就是有意),此时异常分发器将仅仅终止进程,不会调用任何栈上描述的异常处理程序。地址空间布局随机化(ASLR)技术,通过符号异常注册记录让攻击代码更难于得知函数指向的位置,以及构造一个自身的假符号记录。这有助于此方法(译注:即异常分发器检查符号记录决定是否调用异常处理程序)的健壮性。

写入时复制

地址窗口扩展

内核模式堆(系统内存池)

池大小

监控池使用情况

旁观列表

堆管理器

堆的类型

堆管理器结构

堆同步

低碎片堆

堆安全功能

堆调试功能

页面堆

容错堆

虚拟地址空间布局

x86地址空间布局

x86系统地址空间布局

x86会话空间

系统页面表条目

系统页面表条目(PTEs)用于动态映射系统页面,如I/O空间、内核堆栈和内存描述符列表的映射。系统pte并不是一个无限的资源。在32位Windows,可用系统pte的数量,系统理论上可以描述2gb的连续系统虚拟地址空间。在64位Windows上,系统pte可以描述向上 到128gb的连续虚拟地址空间。

实验: 查看系统PTE信息
通过检查性能监视器中的内存:空闲系统页面表条目计数器,或使用debu中的!sysptes或!vm命令,您可以看到有多少个系统pte可用 gger.您还可以转储与MiSystemPteInfo全局变量关联的_MI_SYSTEM_PTE_TYPE结构。这还将显示您在系统上发生了多少次PTE分配故障一个很高的 计数表示有问题,也可能是系统PTE泄漏。

64位地址空间布局

Windowsx64 16TB限制

动态系统虚拟地址空间管理

系统虚拟地址空间配额

用户地址空间布局

地址转换

x86虚拟地址转换

转换旁观缓冲区

物理地址扩展(PAE)

x64虚拟地址转换

IA64虚拟地址转换

页面故障处理

之前,你看到了在PTE有效时如何解析地址转换。当PTE有效位被清除时,这表示由于某种原因当前无法访问 过程本节描述无效pte的类型以及如何解析对它们的引用。

注意:本节中只详细介绍了32位x86PTE格式。64位系统的pte包含类似的信息,但没有提供它们的详细布局。

对无效页面的引用称为页面错误
内核陷阱处理程序(在第1部分第3章的“陷阱调度”一节中介绍)将这种故障分派到内存管理器中 另一个故障处理程序(MmAccess故障)来解决。

此例程在导致故障的线程的上下文中运行,并负责尝试解决故障(如果可能的话)或引发适当的异常。
这些故障可由多种条件引起,如表10-13所示。

表10-13出现访问故障的原因

错误原因结果
访问不驻留在内存中但位于页面文件或映射文件中的磁盘上的页面分配物理页面,并从磁盘读取和相关工作集中读取所需页面
访问备用或已修改列表中的页面将页面转换到相关的流程、会话或系统工作集
访问未提交的页面(例如,保留的地址空间或未分配的地址空间)访问违规
从只能在内核模式中访问的用户模式中访问页面访问违规
写入一个只读的页面访问违规
访问一个零需求(demand-zero)页面向相关的工作集中添加一个零填充的页面
写入到一个保护(guard)页面制作进程私有(或会话私有)页面副本,并替换原始的进程中、会话或系统工作集
写到一个写时复制(copy-on-write)的页面上在PTE中设置脏位
写入有效但尚未写入当前备份存储副本的页面访问冲突(仅在不支持执行保护的硬件平台上受支持)

以下部分介绍了访问故障处理程序处理的四种基本无效pte。
下面是对无效pte的一种特殊情况的解释,即原型pte ,它被用于实现可共享的页面。

无效的PTEs

如果在地址转换过程中遇到的PTE的有效位为零,则PTE表示一个无效的页面——在引用时将引发内存管理异常或页面错误。MMU(Memory Management Unit)忽略PTE的其余位,因此操作系统可以使用这些位来存储有关页面的信息,以帮助解决页面故障。
下面详细介绍了四种无效 PTEs及其结构。这些通常被称为软件PTEs (Software PTEs),因为它们是由内存管理器而不是MMU来解释的。S 其中一些标志与表10-11中描述的硬件PTE的标志相同,并且一些位字段与硬件PTE中的相应字段具有相同或相似的含义。

  • 页面文件(Page file) 所需的页面保存在一个分页文件中。如图10-26所示,PTE中的4位表示页面位于其中的16个可能的页面文件中,以及20位(在x86个非PAE中;更多的是其他版本 er模式)在文件中提供页码。寻呼机启动一个页面操作,将页面带到内存中并使其有效。页面文件偏移量总是是非零的,永远都是1s( 首先,页面文件中的第一个页面和最后一页都不用于分页),以允许使用其他格式,下面将进行描述。
    在这里插入图片描述
  • 零需求(Demand zero) 此PTE格式与前一条目中显示的页面文件PTE相同,但是页面文件的偏移量为零。所需的页面必须满足一个大小为0的页面。寻呼机将查看零 页面列表。如果列表为空,寻呼机将从空闲列表中获取一个页面并将其归零。如果空闲列表也为空,则它从一个备用列表中获取一个页面并将其归零。
  • 虚拟地址描述符(Virtual address descriptor) 这种PTE格式与之前显示的页面文件PTE相同,但在这种情况下,页面文件偏移字段都为1。这表示可以使用其定义和备份存储的页面 在进程的虚拟地址描述符(VAD)树中。此格式用于由映射文件中的部分支持的页面。寻呼机会找到定义虚拟地址范围e的VAD 不包含虚拟页面,并从VAD引用的映射文件启动页内操作。(虚拟辅助系统将在后一节中进行更详细的描述。)
  • 转化(Transition) 所需页面在待机、修改或修改的列表中,或者不在任何列表中。如图10-27所示,PTE中包含了该页面的页帧号。寻呼机wi 将从列表中删除该页面(如果它在一个页面上),并将其添加到进程工作集中。 在这里插入图片描述

原型的PTEs

分页 I/O

对齐的页面故障

群集页面故障

分页文件

提交代价和系统提交限制

提交代价和页面文件大小

堆栈

用户堆栈

内核堆栈

DPC堆栈

虚拟地址描述符

内存管理器使用需求分页算法来知道何时将页面加载到内存中,直到线程引用地址并导致页面故障,然后再从磁盘中检索页面。 与写时复制一样,需求分页是一种惰性评估的形式——等待直到需要任务时才执行任务。内存管理器使用惰性计算不仅可以将页面带入内存,还可以构造描述新页面所需的页面表。例如,当一个线程使用VirtualAlloc或VirtualAllocExNuma提交大区域的虚拟内存时,内存管理器可以立即构建访问所需的页面表 已分配内存的整个范围。但如果其中一些范围从未被访问过呢?为整个范围创建页面表将是一个浪费时间的工作。相反,内存管理器会等待创建一个页表,直到一个线程出现 一个页面错误,然后它为该页面创建一个页面表。这种方法显著提高了保留和/或提交大量内存但访问量稀疏的进程的性能。

将被诸如尚不存在的页面表所占用的虚拟地址空间将被记入进程页面文件配额和系统提交负担。这确保了空间将可用对于他们被实际创建的情况。使用惰性评估算法, 即使是分配很大的内存块也是一个快速的操作。当一个线程分配内存时,内存管理器必须响应该线程要使用的地址范围。为了做到这一点,内存管理器维护另一组数据结构,以跟踪哪些虚拟地址已经在进程的地址空间中保留,而哪些没有保留。这些数据结构被称为虚拟地址描述符 (Virtual Address Descriptors (VADs)。VADs被分配在非分页池中。

进程VADs

对于每个进程,内存管理器都会维护一组描述进程地址空间状态的VAD。VAD被组织成一个自平衡的AVL树(以其发明者的名字命名, 它们最优地平衡了这棵树。这导致,在搜索与虚拟地址对应的VAD时,比较次数最少。有一个 虚拟地址描述符,用于每个几乎连续的非空闲虚拟地址范围,它们都具有相同的特征(保留与提交与映射,内存访问保护 N,以此类推)。VAD树的示图如图10-32所示。
在这里插入图片描述
当进程保留地址空间或映射一个区段的视图时,内存管理器创建一个VAD来存储由分配请求提供的任何信息,例如地址的范围 保留,该范围是共享的还是私有的,子进程是否可以继承该范围的内容,以及应用于该范围内的页面的页面保护。

当一个线程第一次访问一个地址时,内存管理器必须为包含该地址的页面创建一个PTE。为此,它将找到其地址范围内包含被访问的地址的VAD,并使用它所找到的信息来填充PTE。如果地址超出VAD覆盖的范围,或者属于保留(Reserve)但未提交(Commit)的地址范围,内存管理器知道该线程在尝试使用内存之前没有分配内存,因此产生了访问冲突。
在这里插入图片描述

旋转VADs

视频卡驱动程序通常必须将数据从用户模式的图形应用程序复制到各种其他系统内存,包括显卡内存和AGP端口的内存,这两者都有不同的缓存属性和地址。为了快速地允许将这些不同的内存视图映射到一个进程中,并支持不同的缓存属性,内存管理器实现了旋转VAD,它允许视频驱动程序通过使用GPU直接传输数据,并在进程视图页面中旋转不需要的内存。
图10-33显示了一个例子,相同的虚拟地址如何在视频RAM和虚拟内存之间旋转。
在这里插入图片描述

非均匀存储器存取NUMA

Windows的每个新版本都为内存管理器提供了新的增强,以更好地利用非统一内存架构(NUMA)机器,如大型服务器系统(但也包括英特尔i7和AMDOpteronSMP工作站)。内存管理器中的NUMA支持增加了对节点信息的智能知识,如位置、拓扑结构和访问成本,以允许应用程序和驱动程序实现 利用NUMA功能,同时抽象底层硬件细节。

当内存管理器初始化时,它调用小型计算机numacost函数,在不同的节点上执行各种页面和缓存操作,然后计算这些操作所花费的时间 离子完成。基于此信息,它构建了一个访问成本的节点图(一个节点和系统上任何其他节点之间的距离)。当系统需要一个给定的操作数的页面时 打开时,它会查阅图来选择最优的节点(即最接近的节点)。如果在该节点上没有可用的内存,那么它将选择下一个最近的节点,以此类推。

尽管内存管理器尽可能确保内存分配来自于进行分配的线程的理想处理器节点(理想节点),但它也提供了函数 允许应用程序选择自己的节点,如虚拟本地节点、创建文件节点、映射文件节点和AllocateUserPhysicalPagesNuma api。

理想节点不仅在应用程序分配内存时使用,也在内核操作和页面故障时使用。例如,当一个线程在非理想处理器上运行并产生一个页面错误时,内存管理器不会使用当前节点,而是从线程的理想节点分配内存。尽管这可能会导致在线程仍在其上运行时访问时间变慢 当线程迁移回其理想节点时,这个CPU、整体内存访问将得到优化。在任何情况下,如果理想节点的资源不足,则选择与理想节点最近的节点,而不是随机的其他节点。然而,就像用户模式的应用程序一样,驱动程序可以在使用api时指定它们自己的节点,如MdlEx或MmAllocateContiguousMemorySpecifyCacheNode.的内存定位页面.

各种内存管理器池和数据结构也被优化,以利用NUMA节点。内存管理器试图均匀地使用来自系统上所有节点的物理内存 非分页池。当进行非分页池分配时,内存管理器会查看理想的节点,并将其作为索引来选择所关联的非分页池内的虚拟内存地址范围 响应到属于此节点的物理内存。此外,创建每个numa节点池空闲列表可以有效地利用这些类型的内存配置。除了非分页池外,系统缓存和系统pte也类似地分配到所有节点上,以及内存管理器的查看备用列表。

最后,当系统需要零页时,它通过创建与物理内存所在的节点相对应的具有NUMA亲和力的线程,在不同的NUMA节点上并行地实现这一点。逻辑预取和超取(稍后描述)在预取时也使用目标进程的理想节点,而软页错误会导致页面迁移到t的理想节点 他错线。

节对象

从本章前面关于共享内存的部分中可以看出,Windows子系统称之为文件映射对象,表示一个或两个进程的内存块 重新处理可以共享。可以将节对象映射到分页文件或磁盘上的另一个文件。

执行人员使用部分将可执行图像加载到内存中,而缓存管理器则使用它们来访问缓存文件中的数据。(有关缓存管理器如何使用的更多信息,请参见第11章 节对象。)您还可以使用截面对象将文件映射到进程地址空间中。然后,通过映射部分对象和r的不同视图,可以作为大数组访问 读取或写入内存而不是文件(称为映射文件I/O的活动)。当程序访问一个无效的页面(一个不在物理内存中的),出现页面故障,内存管理器会自动将页面从映射的文件(或页面文件)带入内存中。如果应用程序修改了页面,内存管理器将在其正常的分页操作期间将更改写回文件(或者应用程序可以使用WindowsFlush视图刷新视图 文件函数)。

与其他对象一样,分段对象也由对象管理器分配和释放。对象管理器创建并初始化对象头,使用它来管理对象 y管理器定义了横断面对象的主体。内存管理器还实现了用户模式线程可以调用的服务,以检索和更改存储在节对象主体中的属性。显示了一个截面对象的结构 在图10-34中。
在这里插入图片描述
表10-15总结了存储在节对象中的唯一属性。

驱动程序验证器

正如在第8章“I/O系统”中所介绍的,驱动程序验证器是一种可用于帮助发现和隔离设备驱动程序或其他内核模式系统代码中常见的错误的机制。本节 描述驱动验证器提供的内存管理相关验证选项(与设备驱动相关的选项)。

验证设置存储在注册表下的HKLM系统货币控制设置控制会话管理器内存管理。该值包含一个表示th的位掩码 e已启用验证类型。验证驱动程序值包含要验证的驱动程序的名称。(这些值在驱动程序验证中选择要验证的驱动程序中 r经理。)如果选择验证所有驱动程序,则验证驱动程序将设置为星号(*)字符。根据您所做的设置,您可能需要为所选的veri重新启动系统 确认发生。

在引导过程的早期,内存管理器读取驱动程序验证器注册表值,以确定要验证哪些驱动程序以及启用了哪些驱动程序验证器选项。(请注意,如果你启动在 在安全模式下,任何驱动程序验证器的设置都会被忽略。)随后,如果您选择了至少一个驱动程序进行验证,那么内核将根据您为验证所选择的驱动程序列表来检查它加载到内存中的每个设备驱动程序的名称 阳离子。对于出现在这两个地方的每个设备驱动程序,内核都会调用VfLoadDriver函数,该函数会调用其他内部Vf*函数来替换驱动程序对许多内核的引用 函数引用了驱动程序验证器-这些函数的等效版本。例如,扩展池被替换为对验证定位池的调用。窗口系统驱动程序(Win32k.s ys)也做了类似的改变,使用驱动验证等效的功能。

现在我们已经回顾了驱动程序验证器是如何设置的,我们将检查6个可以应用于设备驱动程序的与内存相关的验证选项:特殊池、池跟踪、强制IRQL检查、低资源模拟、杂项检查和自动检查.

特殊池 特殊池选项会导致池分配例程包含一个无效页面的池分配,以便在分配之前或之后的引用将导致内核模式 取消访问冲突,从而用手指指向童车司机,使系统崩溃。特殊池还会导致在驱动程序分配或释放时执行一些额外的验证检查 记忆力.

当启用特殊池时,池分配例程将为内核内存区域分配给驱动程序验证器使用。驱动程序验证器重定向内存分配请求,驱动程序在veri下 生成特殊的池区域,而不是生成标准的内核模式内存池。当设备驱动程序从特殊池分配内存时,驱动程序验证器将分配汇总到一个覆盖边界。因为驱动程序验证器将分配的页面括上了无效的页面,如果设备驱动程序试图读取或写入缓冲区的末端,驱动程序将访问无效的页面,并且内存管理器将引发内核模式访问冲突。

图10-36显示了当驱动程序验证器检查溢出错误时,驱动程序验证器分配给设备驱动程序的特殊池缓冲区的一个示例。
在这里插入图片描述
默认情况下,驱动程序验证器将执行溢出检测。它通过将设备驱动程序使用的缓冲区放置在已分配的页面的末尾,并以一个已运行的方式填充页面的开头 dom模式。虽然驱动程序验证器管理器不允许您指定运行不足检测,但您可以通过添加DWORD注册表值HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PoolTagOverruns标记溢出并将其设置为0(或通过运行Gflags实用程序并选择验证开始选项而不是默认选项,验证结束)。 当Windows强制执行运行不足检测时,驱动程序验证器在页面的开始而不是最后分配驱动程序的缓冲区而不是结束。

溢出检测配置还包括一些不足检测的度量。当驱动器释放其缓冲区将内存返回给驱动器验证器时,驱动程序验证器确保缓冲区之前的模式没有改变。如果模式被修改,设备驱动程序已经运行不足的缓冲区,并写入缓冲区外部的内存。

特殊池分配还需要检查,以确保在分配和交易时的处理器IRQL是合法的。此检查捕获了一些设备驱动程序所犯的错误:在DPC/调度级别或以上级别从IRQL分配可分页内存。

您还可以通过添加DWORD注册表值HKLM\SYSTEM\CurrentControlSet\Control\Session\Manager\Memory\Management\PoolTag,来手动配置特殊池,它表示分配标记 该系统用于特殊的池。因此,即使驱动程序验证器没有配置为验证特定的设备驱动程序,如果驱动程序与它分配的内存关联的标记与之匹配 在PoolTag注册表值中指定时,池分配例程将从特殊池中分配内存。如果您将PoolTag的值设置为0x0000002a或通配符(*),则所有内存均为该内存 驱动程序的分配来自特殊的池,只要有足够的虚拟和物理内存。(如果没有足够的空闲页面,驱动程序将恢复到常规池的分配 ,但每个分配都使用两个页面。)

池跟踪 如果启用了池跟踪,则内存管理器将在驱动程序卸载时检查驱动程序是否释放了它所做的所有内存分配。如果没有,它就会破坏系统 把马车的司机。驱动程序验证器还可以在驱动程序验证器管理器的“池跟踪”选项卡上显示一般的池统计信息。您还可以使用!验证器内核调试器命令。此命令显示 比驱动程序验证者提供更多的信息,并且对驱动程序编写者很有用。

池跟踪和特殊池覆盖的不仅是显式的分配调用,不仅 ExAllocatePoolWithTag,而且还可以调用其他隐式分配池的内核api,IoAllocateMdl, IoAllocateIrp, 还有其他 IRP 分配调用;各种Rtl字符串api;和IoSetCompletionRoutineEx函数.

由池跟踪选项启用的另一个驱动程序验证功能与池配额费用有关。呼叫ExAllocatePoolWithQuotaTag向当前进程的池配额收费 字节分配。如果这样的调用是由延迟过程调用(DPC)例程调出的,则收费的进程是不可预测的,因为DPC例程可以在任何进程的上下文中执行。这个 池跟踪选项会检查从DPC例程上下文对此例程的调用。

驱动程序验证器还可以执行锁定的内存页面跟踪,它可以另外检查在I/O操作后被锁定的页面,并生成DRIVER_LEFT_LOCKED_PAGES_IN_PRO CESS而不是PROCESS_HAS_LOCKED_PAGES崩溃代码——前者表示负责该错误的驱动程序以及负责锁定页面的函数。

强制IRQL检查当设备驱动程序执行的处理器处于高IRQL时,驱动程序访问可分页数据或代码时,会发生最常见的设备驱动程序错误之一。 如第1部分中的第3章所述,当IRQL为DPC/调度级别或以上时,内存管理器无法服务于页面故障。系统通常不会检测到设备驱动程序访问的实例 g当处理器在高IRQL级别上执行时的可分页数据,因为被访问的可分页数据在当时恰好是物理上驻留的。然而,在其他时候,数据可能是b 这将导致停止代码IRQL_NOT_LESS_OR_EQUAL(即IRQL不小于或等于尝试操作所需的级别——在这种情况下,ac 打乱可分页的内存)。

虽然测试这种错误的设备驱动程序通常是困难的,但驱动程序验证器使它变得容易。如果您选择强制IRQL检查选项,驱动程序验证力 当被验证的设备驱动程序引发IRQL时,系统工作集中的所有内核模式的可分页代码和数据。这样做的内部函数是MiTrimAllSystemPagableMemo ry.启用此设置后,当被验证的设备驱动程序在IRQL升高时访问可分页内存时,系统就会立即检测到违规行为,并检测到生成的系统cr Ash识别故障驱动程序。

当同步对象是数据结构的一部分时,这是由不正确的IRQL使用导致的另一个常见驱动程序崩溃。同步对象应该 永远不要分页,因为调度程序需要在高IRQL访问它们,这将导致崩溃。驱动程序验证器检查可分页备忘录中是否存在以下任何结构:KTIMER, KMUTEX, KSPIN_LOCK, KEVENT, KSEMAPHORE, ERESOURCE, FAST_MUTEX.。

低资源模拟 启用低资源模拟导致驱动程序验证器随机故障验证设备驱动程序执行的内存分配。在过去,开发者编写了许多设备 假设内核内存始终可用,如果内存耗尽,设备驱动程序不必担心它,因为系统无论如何都会崩溃。然而 因为低内存条件可能会暂时发生,因此设备驱动程序要正确地处理表明内核内存已耗尽的分配故障是很重要的。驱动程序调用的将是inj 随机失败的结果包括ExAllocatePool*,MmProbeAndLockPages,MmMapLockedPagesSpecifyCache,MmMapIoSpace,MmAllocateContiguousMemory, MmAllocatePagesForMdl, IoAllocateIrp, IoAllocateMdl, IoAllocateWorkItem, IoAllocateErrorLogEntry, IOSetCompletionRoutineEx,以及分配池的各种Rtl字符串api。

此外,您还可以指定分配失败的概率(6p 默认情况下的百分比),哪些应用程序应该服从模拟(默认情况下都是),哪些池标签应该受到影响(默认情况下都是),以及在故障i之前应该使用什么延迟 n注入启动(默认值是在系统启动后7分钟,这是足够的时间来通过关键的初始化期间,在此期间,低内存条件可能会阻止设备驱动程序fr om加载)。在延迟周期之后,驱动程序验证器启动对它正在验证的设备驱动程序的随机失败分配调用。如果一个驱动程序不能正确地处理分配失败,这将是l 主要显示为一个系统崩溃。

在延迟周期之后,驱动程序验证器启动对它正在验证的设备驱动程序的随机失败分配调用。如果一个驱动程序没有正确地处理分配失败,这很可能会作为一个系统崩溃显示。

杂项检查 驱动程序验证器称之为“杂项”的一些检查允许驱动程序验证器检测池中仍然处于活动的某些系统结构的释放。用于exa mple,驱动程序验证器将检查:释放内存中的

  • 活动工作项(驱动程序调用扩展自由池来释放一个池块,其中一个或多个工作项排队的工作项存在)。
  • 释放内存中的活动资源(驱动程序在调用外部删除资源之前调用ExFreePoel以销毁电子资源对象)。
  • 释放内存中的活动查看旁列表(驱动程序调用前自由池 e调用ExDeleteNPagedLookasideList或ExDeletePagedLookasideList来删除查看旁端列表)。

最后,当验证被启用时,驱动程序验证器也会执行某些自动检查 不能单独启用或禁用。

这些代码包括:

  • 调用内存描述符列表(MDL)上的MmProbeAndLockProcessPages。例如,它是在 正确地,可以通过调用MmbuildMdlFor非MDL池来进行MDL设置。
  • 调用具有错误标志的MDL上的内存锁定页面。例如,调用MmMapLock是不正确的 已映射到系统地址的MDL的页面。另一个错误驱动程序行为的例子是为未锁定的MDL调用MmMapLocked页面。
  • 调用部分部分(使用MDL创建)上的锁定页面。
  • 在未映射到系统地址的MDL上调用内存锁定页面。
  • 分配同步对象,如偶数 来自非页面池话内存的Ts或互作文本。驱动程序验证器是设备驱动程序编写器可用的验证和调试工具库中的一个有价值的补充。许多设备驱动程序 第一次运行与驱动程序验证器有一个驱动程序验证器能够暴露的错误。

因此,驱动程序验证器已经导致了在Windows中运行的所有内核模式代码的质量的整体提高 .

驱动程序验证器是设备驱动程序编写器可用的验证和调试工具库中的一个有价值的补充。许多设备驱动程序第一次运行与驱动程序验证有错误 我验证者能够曝光。因此,驱动程序验证器已经导致了在Windows中运行的所有内核模式代码的质量的整体提高。

页面帧号数据库

页面列表动态化

页面优先级

修改页面写入器

PFN数据结构

物理内存限制

Windows客户端内存限制

工作集

需求分页

逻辑预取器

放置政策

工作集管理

均衡集管理器和交换器

系统工作集

内存通知事件

主动内存管理(超级获取)

组件

跟踪和日志

场景

页面优先级和重新平衡

稳健的性能

ReadyBoost

ReadyDrive

统一缓存

进程反射

结论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值