x86 内存管理(三)无效/原型 PTE 与页面错误

😄

1 PTE

说明:

  1. 《Windows 内核原理与实现》中关于无效 PTE、原型 PTE 适用于 Windows XP 非 PAE。
  2. 《深入解析 Windows 操作系统第六版》中关于无效 PTE、原型 PTE 适用于 Windows 7 非 PAE。
  3. 《深入解析 Windows 操作系统第七版》中关于无效 PTE、原型 PTE 适用于 Windows 10 10240(1507)-17134(1803) PAE。
  4. 本文研究中关于无效 PTE、原型 PTE 适用于 Windows 10 17763(1809)-19044(21H2) PAE。

测试的 x86 系统版本信息

64.png

本节内容可参考:揭秘Icebox虚拟机自检解决方案实现Windows内存自省的原理,原文为Windows Memory Introspection with IceBox

1.1 硬件PTE

页表项的 PTE 叫做硬件 PTE,结构为 _MMPTE_HARDWARE,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//0x8 bytes (sizeof)
kd> dt _MMPTE_HARDWARE
nt!_MMPTE_HARDWARE
+0x000 Valid : Pos 0, 1 Bit
+0x000 Dirty1 : Pos 1, 1 Bit
+0x000 Owner : Pos 2, 1 Bit
+0x000 WriteThrough : Pos 3, 1 Bit
+0x000 CacheDisable : Pos 4, 1 Bit
+0x000 Accessed : Pos 5, 1 Bit
+0x000 Dirty : Pos 6, 1 Bit
+0x000 LargePage : Pos 7, 1 Bit
+0x000 Global : Pos 8, 1 Bit
+0x000 CopyOnWrite : Pos 9, 1 Bit
+0x000 Unused : Pos 10, 1 Bit
+0x000 Write : Pos 11, 1 Bit
+0x000 PageFrameNumber : Pos 12, 25 Bits
+0x000 reserved1 : Pos 37, 26 Bits
+0x000 NoExecute : Pos 63, 1 Bit

成员 PageFrameNumber 表示物理内存中的页面索引。当页面有效时,就可以很容易地计算出物理地址:

$$PhysicalAddress = \_MMPTE\_HARDWARE.PageFrameNumber * 0x1000 + PageOffset$$

1.2 无效PTE

当处理器在翻译一个虚拟地址引用时,如果该地址的页表项 PTE 中的有效位 P 为0,则处理器会引发一个异常(#PF),也称为页面错误(page fault)。Windows 内核的陷阱处理器(trap handler )会把这一异常交给内存管理器的页面错误处理例程(page fault handler),对应于 IDT 0xE 号中断的 ISR 处理函数 _KiTrap0E,由它来解决页面无效的问题。

参考《Windows XP 异常处理(一)异常采集》-1.3 中断和异常列表,该异常由硬件触发,属于错误类异常。也就是说异常处理后,会继续到引发异常的地址继续执行。

PTE 中的有效位 P 为 0 时,称为无效 PTE,或软件 PTE(SoftWare PTE)。在Windows中,PTE 的 P 位有效时,页面位于工作集(Working Set,WS)中,此时的 PTE 属于页表项中的,叫做硬件 PTE。它基本上对应于在不引起页面错误的情况下可以访问的页面集。实际上,存在三种类型的工作集:进程、系统和会话,每一种都有其自己的限制。

当有效位为 0 时,访问这样的页面将会产生页面错误。这时的 PTE 为软件 PTE。

无论是硬件 PTE 还是软件 PTE,Windows 把所有这些类型的 PTE 组织在了一个 C 语言联合(union)中,因而在代码中可以方便地取用任何一种类型的 PTE。

Windows 通过名为 _MMPTE 的特定联合体为页面定义了几种内部状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
kd> dt _MMPTE -r1
nt!_MMPTE
+0x000 u : <anonymous-tag>
+0x000 Long : Uint8B
+0x000 VolatileLong : Uint8B
+0x000 HighLow : _MMPTE_HIGHLOW
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Proto : _MMPTE_PROTOTYPE // 原型 PTE
+0x000 Soft : _MMPTE_SOFTWARE // 软件 PTE
+0x000 TimeStamp : _MMPTE_TIMESTAMP
+0x000 Trans : _MMPTE_TRANSITION // 转移状态 PTE
+0x000 Subsect : _MMPTE_SUBSECTION
+0x000 List : _MMPTE_LIST

在 PAE 分页下,无效PTE(软件 PTE)结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kd> dt _MMPTE_SOFTWARE
nt!_MMPTE_SOFTWARE
+0x000 Valid : Pos 0, 1 Bit
+0x000 PageFileReserved : Pos 1, 1 Bit
+0x000 PageFileAllocated : Pos 2, 1 Bit
+0x000 Unused0 : Pos 3, 1 Bit
+0x000 SwizzleBit : Pos 4, 1 Bit
+0x000 Protection : Pos 5, 5 Bits
+0x000 Prototype : Pos 10, 1 Bit
+0x000 Transition : Pos 11, 1 Bit
+0x000 PageFileLow : Pos 12, 4 Bits
+0x000 Unused1 : Pos 16, 16 Bits
+0x000 PageFileHigh : Pos 32, 32 Bits

63.png

当页面错误产生时,根据下面列出的 5 种情况下的某种 PTE 状态来解决异常。即无效的 PTE 可以指示页面错误处理例程如何正确地处理和解决该错误。具体详细转换过程可以看 FieEye 在BlackHat 的报告文章《Finding-Evil-In-Windows-10-Compressed-Memory》。

  1. 目标页面位于页面文件(Page File)中:当产生该页面错误时,表示期望得到的页面内容保存在 Page FIle 中。这种情况下换页器激发一个页面换入(in-page)操作,将 Page File 中的内容换入物理内存页,并将 PTE 的 P 位置 1。

    • Windows 共有 16 个存在本地的页面文件,Page File Index 的 4 位指出目标页面驻留在哪一个页面文件。

    • Page File Offset 的 32 位指出目标页面在该文件的页面偏移编号。

    • 目标页面文件索引对应于 PageFileLow 字段,文件中的页面偏移量(PageFileOffset)解析如下:

      $$PageFileOffset = \_MMPTE.u.Soft.PageFileHigh * 0x1000 + PageOffset$$

    • 此时:

      • Prototype = 0
      • Transition = 0
      • Page File Offset != 0 && Page File Offset != 1

    65.png

  2. 要求零页面(Demand zero)。当发生这类页面异常时,需要一个填满零的页面来满足。换页器(pager)首先检查零页面列表。如果该列表是空的,则换页器从空闲列表中取出一个页面,并且填上零。如果空闲列表也是空的,则它从备用列表中取出一个页面, 并填上零。

    此时:

    • Prototype = 0
    • Transition = 0
    • Page File Offset = 0

    66.png

  3. 目标页面在映射文件中。这种页面异常情况下,专用于由映射文件内存区支撑的页面。换页器会在进程 VAD 树中查找引发异常虚拟地址对应的 VAD 节点,并从VAD引用的映射文件激发一个页面换入操作,将内容换入物理内存。

    此时:

    • Prototype = 0
    • Transition = 0
    • Page File Offset = 1

    67.png

  4. 目标页面正在转移状态中(Transition)。目标页面仍在内存中,位于备用链表、修改链表、修改但不写出、或内存中非任何链表中。则会将该页面将从此列表中删除(如果它在某个列表中的话),并加到进程的工作集中。成员 _MMPTE_TRANSITION.PageFrameNumber 为该物理页的 PFN。

    尽管无法直接访问该页面,但其内容仍然存在,并且在物理内存中有效。在访问后,将会触发页面错误,这将导致 PTE 的状态由过渡状态再恢复为有效状态。这个过渡状态 PTE 对应于_MMPTE_TRANSITION结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    kd> dt _MMPTE_TRANSITION
    nt!_MMPTE_TRANSITION
    +0x000 Valid : Pos 0, 1 Bit
    +0x000 Write : Pos 1, 1 Bit
    +0x000 Spare : Pos 2, 1 Bit
    +0x000 IoTracker : Pos 3, 1 Bit
    +0x000 SwizzleBit : Pos 4, 1 Bit
    +0x000 Protection : Pos 5, 5 Bits
    +0x000 Prototype : Pos 10, 1 Bit
    +0x000 Transition : Pos 11, 1 Bit
    +0x000 PageFrameNumber : Pos 12, 26 Bits
    +0x000 Unused : Pos 38, 26 Bits

    在过渡状态下,PTE的目标物理地址的计算方法如下:

    $$PhysicalAddress = \_MMPTE.u.Trans.PageFrameNumber * 0x1000 + PageOffset$$

    此时:

    • Prototype = 0
    • Transition = 1
    • PageFrameNumber = PFN

    68.png

  5. 目标页面为未知状态(unknown)。这种状态下 PDE 或 PTE 全为 0。此时该页面的状态无法确定,甚至该 PTE 对应的虚拟地址是否已被提交也不能确定。所以页面错误处理例程应检查 VAD 树中对应的节点,以确定该页面的状态。如果页面已提交,则建立页表来表达新提交的地址空间,否则(页面是保留的,或者根本没有被定义过),把页面错误报告为“访问违例”(0xC0000005)异常。注意这种情况对于 x64 不同,x64 可以参考《揭秘Icebox虚拟机自检解决方案实现Windows内存自省的原理》。

1.3 原型PTE

在上一篇介绍了共享内存区,由于共享内存区仅对应一块物理内存,但是被多个进程同时使用。这时候涉及到一个难点,就是同步共享页面的裁剪。

使用原型 PTE 的原因:实际上,由于多个 PTE 可以引用同一个物理页面,因此如果操作系统决定从物理内存中删除共享页面,则它必须查找引用该页面的所有 PTE,并更新其当前状态。由于这种方法看起来效率很低,因此 Windows 使用了原型 PTE 来解决这一问题。也就是说,内存管理器引入使用原型 PTE(Prototype PTE),让所有可能需要访问共享页面的地址的 PTE 指向一个原型 PTE 来解决页面错误,可以在无需更新每个共享此页面的进程的页表的情况下,有效地管理好共享页(也就是原型 PTE 作为中间层)。下面详细解释。

由页面文件支撑的内存区,当内存区对象被第一次创建的时候,同时也会创建一批原型 PTE。

由映射文件支撑的内存区,则会根据需要,在每个视图被映射的时候才创建这些原型 PTE、这些原型 PTE 是控制区段(segment)结构的一部分。

  1. 当进程第一次引用一个被映射到内存区对象视图中的页面时(只有当 MapViewOfFile 创建视图时才生成相应的 VAD),内存管理器利用原型 PTE 中的信息来填充该进程页表中实际的 PTE
  2. 当一个共享页面被变成有效时,进程 PTE 和原型 PTE 都指向包含此数据的物理页面。为了跟踪有多少个进程 PTE 指向一个有效的共享页面,在 PFN 数据库项中有一个计数器 _MMPFN.u2.ShareCount 也会随之递增。因此,内存管理器可以确定何时一个共享页面己经不再被任何一个页表引用了,因而可以变成无效的,并且放到转移列表(transition list)中,或者写到磁盘上。
  3. 当一个共享页面被变成无效时,进程页表中的 PTE 被填充成一个特殊的 PTE,此特殊 PTE 指向了描述该页面的原型 PTE。

注意

这里有个概念可能会混淆,这个特殊的 PTE 结构为 _MMPTE_PROTOTYPE,而原型 PTE 指的是 _CONTROL_AREA.Segment.PrototypePte 成员(为 _MMPTE 结构)。段中的 PrototypePte 成员指向共享内存区对应的原型 PTE 数组的起始地址。

  • 特殊 PTE:_MMPTE_PROTOTYPE
  • 原型 PTE:_MMPTE

也就是说,当一个 PTE 结构为 _MMPTE_PROTOTYPE,它才指向原型 PTE,否则不指向原型 PTE数组。

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _MMPTE_PROTOTYPE
nt!_MMPTE_PROTOTYPE
+0x000 Valid : Pos 0, 1 Bit
+0x000 DemandFillProto : Pos 1, 1 Bit
+0x000 HiberVerifyConverted : Pos 2, 1 Bit
+0x000 ReadOnly : Pos 3, 1 Bit
+0x000 SwizzleBit : Pos 4, 1 Bit
+0x000 Protection : Pos 5, 5 Bits
+0x000 Prototype : Pos 10, 1 Bit
+0x000 Combined : Pos 11, 1 Bit
+0x000 Unused : Pos 12, 20 Bits
+0x000 ProtoAddress : Pos 32, 32 Bits

69.png

上图的特殊 PTE(_MMPTE_PROTOTYPE)指向描述共享物理页面的原型 PTE(_SEGMENT.PrototypePte)。

此时:

  • Prototype = 1
  • 高 32 位 Prototype PTE address 指向原型 PTE 的地址。

_SEGMENT.PrototypePte 原型 PTE 描述的共享物理页面有以下几种状态:

  1. 活动/有效(Active):该页面位于物理内存中,其他进程正在使用该物理页面。
  2. 位于页面文件(Page File):期望的页面驻留在一个页面文件中,对应 2.2 _MMPTE_SOFTWARE 情况 1。
  3. 要求零页面(Demand zero):期望的页面应该用一个零页面来满足,页面尚未分配,待下次访问时请求一个零页面。对应 2.2 _MMPTE_SOFTWARE 情况 2。
  4. 位于映射文件中:期望的页面驻留在一个映射文件中。对应 2.2 _MMPTE_SOFTWARE 情况 3。
  5. 转移(Transition):期望的页面位于内存中,在备用列表或者修改列表上(或者不在任何列表上)。对应 2.2 _MMPTE_SOFTWARE 情况 4。
  6. 已修改但不写出 (modified-no-write):期望的页面位于内存中,在已修改但不写出列表上。对应 2.2 _MMPTE_SOFTWARE 情况 4。

虽然这些原型 PTE 项的格式与前面描述的 无效 PTE 项的格式相同,但是,这些原型 PTE 并不用于地址转译——它们属于页表和页面帧编号数据库之间的一层,从不直接出现在页表中,位于段(segment)指向的原型 PTE 数组中。

注意:当内存区对象中的一个页面从有效变成无效时,它的软件 PTE 将直接指向原型 PTE。即当一个 PTE 结构为 _MMPTE_PROTOTYPE,它才指向原型 PTE,否则不指向原型 PTE数组。

下图演示了一个映射视图中的两个虚拟页面。一个是有效的,另一个是无效的。第一个页面是有效的,进程 PTE 和原型 PTE 指向该页面。第二个页面在页面文件中, 原型 PTE 包含了它的确切位置。进程 PTE(和任何其他也映射了此页面的进程)指向此原型PTE。

70.png

本节可参看文章:

https://bbs.pediy.com/thread-222949.htm

https://bbs.pediy.com/thread-224348.htm

https://www.anquanke.com/member.html?memberId=125661

https://www.anquanke.com/post/id/215178

https://www.anquanke.com/post/id/203237

https://posts.specterops.io/on-detection-tactical-to-functional-45e41fef7af4

2 页面错误基础

在上一篇文章最后一个实验中可以看到,只有正在被使用的线性地址才会被挂上物理页,如果一个线性地址隔了一段时间没有被使用,或者说当前的物理页快被使用完了,这时操作系统会将这些线性地址对应的物理页上的数据保存到硬盘上,并将线性地址对应PTE的P位设置为0

当CPU访问一个地址,如果其PTE的P位为0,此时会产生缺页异常。缺页异常发生时,通过中断描述符表的 0xE 号中断进行处理。

2.1 Page File—扩展的物理内存

在上一篇文章中,已经提到过页面文件(page file)相关概念,并且基于页面的共享内存,正是使用这里所说的 Page File。

使用 Page File 的原因

物理内存是有限的资源,随着越来越多的进程使用越来越多的物理页面,最终系统的可用物理内存可能会消耗光。为了避免因内存消耗光而导致应用程序或系统的正常功能无法执行,现代操作系统提供了页面交换的能力,即把有些正在使用的页面中的内容存放到磁盘上,从而腾出这些物理页面以供他用。以后,当这些被换出到磁盘上的页面再次被引用时,页面错误处理例程再把它们换回到内存中。

打开“属性—高级系统设置—高级—性能(设置)—高级—更改”可以看到并更改 page file 文件大小。

57.png

如上图,当前已经使用的 page file 大小为 1792 MB,然后找到隐藏文件 C:\pagefile.sys。该文件就是对应的页面文件($1792/1024=1.75 GB$)。

58.png

重要概念

  1. Windows 系统支持一个或多个页面文件,每个磁盘分区最多一个页面文件,用来存放被交换页面的内容。
  2. 页面文件可被看做是物理内存的一种延伸,可以看作是扩展的物理内存 RAM。只不过不同时刻位于不同的物理位置。

2.2 页面错误描述

通用 ErrorCode

当异常产生的时候,在异常采集阶段。系统会将异常错误码 ErrorCode 压入 _KTRAP_FRAMEErrCode(+0x64) 中。可参考《Windows XP 异常处理(一)异常采集》-1.4 错误码ErrCode与异常代码。

ErrorCode 格式如下图,有点像 GDT 的段选择子。

59.png

  • EXT,bit 0。外设引发的异常(外部)。
    • 1,代表外部触发的异常。
    • 0,软件引发的异常,如 INT N
  • IDT,bit 1。表示触发了中断表中的异常(陷阱门或中断门或任务门)
    • 1,ErrorCode 的 Index 为 IDT 表的下标。(Index:bit 3~bit 15)
    • 0,ErrorCode 的 Index 为 GDT/LDT 表的下标。
  • TI,bit 2。IDT 位被设置为 0 时才生效。
    • 1,Index 为 LDT 表的下标。
    • 0,Index 为 GDT 表的下标。

但是注意,在很多情况下,比如页面错误发生时,会将错误码进行重新定义。具体可见《Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes 3A, 3B, 3C, and 3D: System Programming Guide》6-13 Error Code。

引发页面错误的原因

一、在 《Intel 3A,6.15 Exception and Interrupt Reference——Interrupt 14-Page-Fault Exception(#PF)》有如下原因:

当 CPU 启用分页后(CR0 寄存器中的 PG 置 1),处理器在使用页面转换机制将线性地址转换为物理地址时,以下每个情况都可以产生页面异常:

  1. PDE 或 PTE 的 P 位为 0。
  2. 访问权限冲突。
    • 如 PDE、PTE 的 U/S 位。
    • 如果在 CR4 中设置了 SMAP 标志,内核模式下尝试访问用户模式地址的数据也可能触发页面错误
    • 如果在 CR4 中设置了 PKE 标志或 PKS 标志,在具有某些保护密钥的线性地址上进行数据访问时,保护密钥权限寄存器可能会导致页面错误。
  3. 用户模式下向只读页面写数据。如果 CR0 的 WP 位置位,在内核模式下向只读页面写数据时也会发生页面错误。
  4. PDE 或 PTE 的最高位(NX 不可执行)位置1时,如果该页面执行代码就会引发页面错误。如果在 CR4 中设置了 SMEP 标志置 1,内核模式下的进程执行用户模式代码也会引发页面错误
  5. 将 PDPTE、PDT、PTE 中的某个保留位置 1,会引发页面错误。

二、在 《Windows Internals 7 Part1 Chapter 5 Memory Management——Page fault handling》补充了如下原因

60.png

系统中使用函数 MmAccessFault 来解决页面异常。

页面错误下的 ErrorCode

发生页面错误时,32 位的 ErrorCode 每一位基本上都被重新定义,具体见下面。

61.png

62.png

CR2

产生页面错误时, CR2 寄存器保存的是产生异常的地址。如果在产生的第一次异常上又产生了新异常,CR2 保存的值为第二次产生异常的地址。

页面错误处理详细过程可以参考《Windows 内核原理与实现 P257》页面错误处理。实际上就是讲解引发页面错误的原因,以及函数 MmAccessfault 函数分析。