x86 内存管理(二)私有内存-共享内存-物理内存
😄
1 内存管理器提供的服务
内存管理器提供的系统服务包括:申请和释放虚拟内存、共享内存管理、将文件映射到虚拟内存、将虚拟页面换出到磁盘文件、维护虚拟页面信息、修改虚拟页面保护属性、将虚拟页面锁在内存中。
上述大部分系统服务都通过 API 暴露给用户使用,这些 API 包括 4 类:
- Virtual API:以页面粒度操作,非常强大。如 VirtualAllocEx,VirtualFreeEx,VirtualProtectEx 等。
- Heap API:对进程堆进行分配和管理(一般用于申请小于4KB的虚拟内存)。如 HeapAlloc,HeapFree,HeapCreate,HeapReAlloc 等。
- Local/Global APIs:这些是 16 位 Windows 的遗留部分,现在使用 Heap API 实现。
- Memory-mapped files:这些函数将文件映射到内存中,有些映射的文件还可以和其他进程共享。这些函数包括 Create FileMapping、OpenFileMapping、MapViewofFile 等。
虚线框的,因为此实现依赖于编译器并且当然不是强制性的(尽管很常见),它们使用 Heap API。
在内核中给驱动程序使用的的函数均以
Mm
为前缀。
注意几个概念:
- 已提交页面是私有内存。
- 尝试访问空闲或保留内存会导致访问冲突异常,因为该页面未映射到物理内存。
- 已提交的内存(私有内存)会被初始化为0。已提交的页面还有可能被写入到 page file(换出到磁盘)。
ReadProcessMemory
、WriteProcessMemory
进入 0 环后,都需要附加到目标进程。它们还要有目标进程的安全描述符,它们需要PROCESS_VM_READ
或PROCESS_VM_WRITE
权限,或者拥有SeDebugPrivilege
权限,默认情况下仅授予管理员组的成员。- 内存区是作为文件映射对象暴露给 Windows API 使用的。
- 共享内存、映射文件、内存区对象是三个概念。
memory-mapped files (internally called section objects)
1.1 共享内存和映射文件
共享内存:有一个物理页在多个进程中都映射了虚拟地址,这些进程都可以通过自己的虚拟地址访问这个物理页。
共享内存有以下特点:
- 如果多个程序加载同一份磁盘上的 DLL,则在物理内存中只会映射一次该 DLL。
- 如果多个进程加载统一份磁盘上的 EXE(多开),则在物理内存中只会映射一次该 EXE。
1.3 节用个实验说明这个情况。列出所有进程 EPROCESS:!dml_proc 、!process 0 0
内存管理器中用内存区对象,也叫内存区(section object, _SECTION
)来实现共享内存,在 Windows API 中它也被称为文件映射对象(file mapping object),注意不是文件对象 _FILE_OBJECT
。
磁盘上的文件(image file, page file, data file)、物理内存,他们映射到虚拟内存后,都有其对应的内存区对象。内存区对象可以被多个进程共享。
有两种类型的内存区:内存区对象有两种,一种建立在页面文件的基础上,称为页面文件支撑的内存区(page-file-backed-sections)。另一种被映射到其他文件中,称为文件支撑的内存区,也称为文件对象。
可以创建内存区对象的 3 环API:CreateFileMapping, CreateFileMappingNuma(Ex), CreateFileMappingFromApp
。其中有一个参数 HANDLE
:如果是 Mapped file 则需要指定文件对象_FIEL_OBJECT
指针;如果是页面文件支撑的内存区则该参数为 INVALID_HANDLE_VALUE(0)
。
如果内存区有名称的话,其他的进程就可以通过 OpenFileMapping
来打开该内存区。
设备驱动程序还可以利用 ZwOpenSection, ZwMapViewOfSection, ZwUnmapViewOfSection
函数来操作内存区对象。
注意:对于一个非常大的映射内存,即该内存区对象表示的内存区非常大。但是某些进程可能只是使用其中一部分,则这些进程可以只映射该内存区中的一部分使用,映射的这一部分称为该内存区的一个视图。调用 MapViewOfFile, MapViewOfFileEx, MapViewOfFileExNuma
指定映射范围即可。
使用内存区对象的地方:可执行文件映射到内存、设备驱动对象射到内存、缓存管理器。
1.2 内存区对象
一个物理页面也可以被映射到多个进程的用户空间,由这样的物理页面映射在虚存空间形成的连续区间,就称为内存区(Section)。
内存区对象(section object)代表了两个或者多个进程可以共享的一块内存,Windows 子系统将其称为文件映射对象( file
mapping object)。可以利用内存区对象把一个文件映射到进程地址空间中。
内存区对象,如同其他的对象一样,也是由对象管理器来分配和释放的。对象管理器创建并初始化一个对象头(它使用对象头来管理这些对象);而内存管理器定义内存区对象的对象体。我们主要关注对象体,就像 _EPROCESS
对象体一样。
1 | //0x28 bytes (sizeof) |
概念说多了都是扯淡,直接开干。
利用内存区,我们可以使一个映射文件被共享,也可以只是简单的建立一块共享内存。创建内存取对象使用 CreateFileMapping
:
1 | HANDLE __stdcall CreateFileMappingA( |
内存区对象分为两类,在创建内存区对象时就可以区分
1.3 练习:多开程序的物理页映射
对磁盘上同一个 EXE 双击运行两次,查看其本身镜像及加载的 DLL 在物理内存的映射情况。
如下代码:
1 |
|
加载同一个 DLL:
1 |
|
运行两个实例如下:
!process 0 0
查看进程信息。1
2
3
4
5
6
7PROCESS a6c53040 SessionId: 1 Cid: 17c0 Peb: 00d1c000 ParentCid: 0e84
DirBase: 7f0ebf00 ObjectTable: b16fa4c0 HandleCount: 83.
Image: 20220913_HeapAlloc.exe
PROCESS afdc3040 SessionId: 1 Cid: 12e4 Peb: 00c9c000 ParentCid: 0e84
DirBase: 7f0ebb80 ObjectTable: 9a1fc080 HandleCount: 83.
Image: 20220913_HeapAlloc.exe查看 VAD 树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25kd> dt _eprocess vadroot a6c53040
ntdll!_EPROCESS
+0x350 VadRoot : _RTL_AVL_TREE
kd> dx -id 0,0,afdc3040 -r1 (*((ntdll!_RTL_AVL_TREE *)0xa6c53390))
(*((ntdll!_RTL_AVL_TREE *)0xa6c53390)) [Type: _RTL_AVL_TREE]
[+0x000] Root : 0xae01f420 [Type: _RTL_BALANCED_NODE *]
kd> !vad 0xae01f420
VAD Level Start End Commit
a6d5fd90 4 80 85 2 Mapped Exe EXECUTE_WRITECOPY \Users\alvin\Desktop\Release\20220913_HeapAlloc.exe
...
a37bf290 4 6fc60 6fc7e 19 Mapped Exe EXECUTE_WRITECOPY \Users\alvin\Desktop\Release\FirstDll32.dll
...
kd> dt _eprocess vadroot afdc3040
ntdll!_EPROCESS
+0x350 VadRoot : _RTL_AVL_TREE
kd> dx -id 0,0,afdc3040 -r1 (*((ntdll!_RTL_AVL_TREE *)0xafdc3390))
(*((ntdll!_RTL_AVL_TREE *)0xafdc3390)) [Type: _RTL_AVL_TREE]
[+0x000] Root : 0xa5878df8 [Type: _RTL_BALANCED_NODE *]
kd> !vad 0xa5878df8
VAD Level Start End Commit
8141bc18 5 80 85 2 Mapped Exe EXECUTE_WRITECOPY \Users\alvin\Desktop\Release\20220913_HeapAlloc.exe
...
afcc9eb8 3 6fc60 6fc7e 19 Mapped Exe EXECUTE_WRITECOPY \Users\alvin\Desktop\Release\FirstDll32.dll
...- 可以看到两个进程中连映射的 EXE、DLL虚拟地址都一样。
- 注意看,VAD 节点地址是不一样的。
查看 EXE、DLL 是否在同一个物理页。
对于内存空间有两种描述方式:
- 一种是物理内存的角度,所有地址只分为两类,挂了物理页的地址与没有挂物理页的地址,其属性由PDE/PTE决定。
- 另一种是线性地址的角度,分为私有内存与映射内存。
内存管理器提供了一组系统服务来完成以下各种任务:分配和释放虚拟内存,在进程之 间共享内存,将文件映射到内存中,将虚拟页面刷新到磁盘中,获得有关一定范围内虚拟页 面的信息,改变虚拟页面的保护属性,以及将虚拟页面锁在内存中。
这些服务中的大多数是通过Windows API暴露给客户的。Windows API 有 4 组函数可用来管理应用程序中的内存:
- Virtual API:以页面粒度操作,非常强大。如 VirtualAllocEx,VirtualFreeEx,VirtualProtectEx 等。
- Heap API:对进程堆进行分配和管理。如 HeapAlloc,HeapFree,HeapCreate,HeapReAlloc 等。
- Local/Global APIs:这些是 16 位 Windows 的遗留部分,现在使用 Heap API 实现(已弃用)。
- Memory-mapped files:这些函数将文件映射到内存中,有些映射的文件还可以和其他进程共享。这些函数包括 Create FileMapping、OpenFileMapping、MapViewofFile 等。
这部分内容:《Windows Internals Part1 7th 5.2.5》、《Windows 10 System Programming 12, 13》、《Windows 内核原理与实现 4.3.4》、《Windows 内核情景分析 3.4》。
1 私有内存管理
如下图,用户空间的 Private
、Mapped
内存由 VadFlags.PrivateMemory
位决定:
这两类内存的区别主要有2点不同:
- 申请内存的方式不同:
- 私有内存:通过
VirtualAlloc/VirtualAllocEx
申请的(每次都要创建新的 VAD 节点)。 - 映射内存:通过
CreateFileMapping
映射的。
- 私有内存:通过
- 使用方式不同:
- 私有内存:独享物理页。
- 映射内存:可能要与其它进程共享物理页。
1.1 堆管理器
进程用户空间私有内存包括:栈、堆。而在系统层面,私有内存包括系统进程/线程栈、堆(分页/换页内存池,非分页/换页内存池)。本章我们先只研究进程用户空间的私有内存管理。
栈内存是由系统使用 ASLR 自己管理的,我们无法掌控其分配。但是堆内存我们可以自己申请。
在前面一篇文章中已经介绍过 VAD 位图的一位表示 64 KB,但是 Windows 提供了堆管理器,使得 VirtualAlloc
和VirtualAllocExNuma
来分配比最小分配粒度 64KB 小得多的内存块(分配粒度为 4KB
)。这是因为无论是从内存使用效率的角度,还是从性能的角度来看,为相对较小的内存请求分配如此大的一个区域显然不是最优的。为了满足这种需求,Windows提供了一个被称为堆管理器(heap manager)的组件,它负责管理大内存区域中的内存分配,这些大内存区域是通过一些页面粒度的内存分配函数来保留的。
堆管理器中的分配粒度相对较小:在 32 位系统上是 8 字节,在 64位 系统上是 16 字节。
堆管理器存在于两个地方:Ntdll.dll
和 Ntoskrl.exe
。都是以 Rtl
为前缀,malloc, free, new
等函数最终都会调用以 Heap
为前缀的函数,然后调用Ntdll.dll
中的原生函数。而且,从 16 位操作系统遗留的以 Local
或 Global
为前缀函数仍然是可用的,以便支持老的 Windows应用程序。
在 3 环申请内存的函数有如下一些,但是需要进行说明的是 C 语言的 malloc
和 C++的 new
他们会先申请进程创建时预留给堆使用的内存,但是当预留给堆的内存不够时还是会进入 0 环申请内存并分配 VAD 节点。下面一小节将进行实验说明。(关于 VirtualAlloc2
等函数的使用可以看《Windows 10 System Programming Chapter 13》,本节就不浪费墨水了)
每个进程至少有一个堆,直到进程结束才会释放。
进程默认堆空间大小:1MB = 256*4KB,可以在编译程序时通过 /HEAP
修改。进程运行过程中堆的大小会被自动扩充。这部分使用低碎片堆(LFH, Low Fragmentation Heap)分配策略进行管理(具体见Windows Internals –Heap manager–低碎片堆)。
有一种特殊的情况:Win32 GUI 子系统驱动程序(Win32k.sys)的堆是建立在内存映射文件区域基础之上的,它被用来在用户模式下共享GDI和User对象。
对《Windows Internals7 –Heap manager》提取一些知识点:
在 Windows 10 和 Server 2016 之前,只有一种堆类型,我们称之为 NT 堆。NT 堆由可选的前端层扩充,使用低碎片堆 (LFH) 策略分配内存。
Windows 10 引入了一种称为碎片堆(segment heap)的新堆类型。这两种堆类型包括公共元素,但结构和实现方式不同。默认情况下,分段堆由所有 UWP 应用和某些系统进程使用,而 NT 堆由所有其他进程使用。这可以在注册表中进行更改。
Windows通用应用平台 (UWP) :可以让基于该平台开发的应用运行在 运行 Windows 10 的移动端和 PC 端。
NT 堆(NT Heap)。在用户模式下堆管理器是一个两层结构:可选的前端堆层和核心堆层(后端堆层)。前段堆使用低碎片堆策略分配内存(LFH, Low Fragmentation Heap),前端堆仅用于用户模式。核心堆层处理基本的功能,处理常见的从用户模式进入内核模式的堆实现,其核心功能包括段内部的内存块的管理、段的管理、堆的扩展策略、提交内存和解除提交,以及大块的管理。(实际前端后端主要就是看进不进 0 环)。
LFH 内存分配方法:使用桶(LFH buckets),桶的分配粒度呈阶梯型,最小一个桶粒度为 8 字节,最大的桶粒度为 512 字节。总共 128 个桶,最大范围为
16384 bytes = 16384/1024 = 16KB
。malloc/aloca
等函数:- x86:最小分配 8 字节。
- x64:最小分配 16 字节。
x86:
x64:《Low Fragmentation Heap (LFH) Exploitation Windows 10 Userspace》
碎片堆(Segment Heap)。如图所示,管理分配的策略取决于申请内存的大小(16368bytes = 16KB)。
x <= 16368bytes,LFH 处理。
16368bytes < x <= 128KB,引入 VS 分配器,但是 VS 和 LFH 分配器都使用预先堆预留的内存。
128KB < x <= 508KB,后端堆提供的策略不进入 0 环。
x > 508KB,通过直接调用内存管理器(VirtualAlloc)进行处理,进入 0 环。因为这些分配非常大,以至于使用默认的 64 KB 分配粒度(并舍入到最接近的页面大小)被认为足够好。
1.2 练习:堆内存申请
对于以线性地址为角度描述内存时,分为私有内存与映射内存。申请私有内存的函数为VirtualAlloc/VirtualAllocEx,申请映射内存的函数为CreateFileMapping。C 语言的 malloc
和 C++的 new
都不是真正的申请内存,他们申请使用的是进程创建预留给堆使用的内存,但是当预留给堆的内存不够时还是会进入 0 环申请内存并分配 VAD 节点。。
malloc
与 new
的底层调用过程,具体如下:
1 | malloc -> _nh_malloc_dbg -> _heap_alloc_dbg -> _heap_alloc_base -> kernel32!HeapAlloc -> ntdll!RtlHeapAlloc |
这里就要了解一下堆的概念,什么是堆呢?堆其实就是操作系统通过调用 VirtualAlloc 函数预先分配好的一大块内存。ntdll!RtlHeapAlloc
的作用就是在这一大块已经预先分配好的内存里面,分一些小份出来用。作个比喻,可以认为 VirtualAlloc
就是批发市场,一次必须批量从操作系统那里购买内存,必须是 4KB 的整数倍才可以;而 ntdll!RtlHeapAlloc
就是零售商,从 VirtualAlloc
已经批来的货里面(堆)买一部分走。
实验:
1 |
|
运行到第一个
getchar()
时,与运行到第二个getchar()
时查看该进程当前的 VAD 树是一样的。注意:1200-14ff 是进程预留的 1MB 堆空间。如上图,可以看到堆和栈的内存都是在进程预先分配好的
Private
内存中。全局变量使用的内存是在 Image 文件映射区域中。如果
malloc
申请的内存很大时,还是会进入 0 环去申请一块内存,并建立 AVD 节点。使用
VirtualAlloc
申请内存时,尽管是 1 字节,都会进入 0 环,然后每次都要创建新的 VAD 节点(此时内存管理器会以最小单位4KB给一页)。
会发现,无论是全局变量,局部变量,或者调用 malloc 函数,它都没有分配新的内存空间,只不过是使用了当前进程已有的内存空间。
不过还是有几个需要说明一下,比如局部变量的地址是 0x110fdf8
,而它所在的内存块的范围是 0x1010 000~0x110f fff
,主要原因是,栈是从高地址向低地址延申的,因此刚开始使用的地址都是当前内存块的高地址。堆的话,就是直接使用了一块已有的内存,可以回想之前批发商与零售商的例子。全局变量,就比较与众不同了,它映射了当前进程的 .exe
文件,这部分下篇学习映射内存时会讲到。
总结:
- VirtualAlloc/VirtualAllocEx 是申请私有内存的唯一方式。
- 如果已经预留的内存满足本次申请的内存,new 与 malloc 的内部调用是 ntdll!RtlHeapAlloc,这种情况下 ntdll!RtlHeapAlloc 不进入 0 环,仅分配一些已经经过 VirtualAlloc/VirtualAllocEx 申请好的内存。
- 如果保留的内存页不能够满足本次
malloc
申请的内存时,ntdll!RtlHeapAlloc 会进入 0 环申请一块新的内存。
如下图,红框中的 13ff*0x1000+0xfff - (1200*0x1000) = 0x001fffff = 512KB
,是进程创建时预留的。当我们申请128、508、509KB时进程会多提交一页,并建立相应的 VAD节点。经过多次测试,其中有一次就是当我申请 4KB 时,内存管理器会将某两个 VAD 节点合并以提供足够的空间。(我的应用不是 UWP 应用)。
2 Mapped 内存管理
映射内存主要有两类:一种是共享物理页,另一种是共享文件。基于页面,基于文件(文件映射对象)–数据文件、镜像文件。
都是使用函数 CreateFileMapping
函数,CreateFileMapping
只是在底层准备好一个物理页/文件,想将准备好的物理页/文件与当前进程关联起来,就要依赖 MapViewOfFile
函数将物理页映射到自己进程的虚拟地址空间中。如果其他进程想要共享并使用这个内存区,只需要使用 OpenFileMapping
来获取内存区对象句柄,然后再使用 MapViewOfFile
将物理页映射到自己的地址空间中即可。
1 | NtCreateSection --> MiCreateSectionCommon --> MiCreateSection --> MiCreateImageOrDataSection/MiCreatePagingFileMap |
2.1 CreateFileMappingW
CreateFileMapping
函数原型如下:
1 | HANDLE __stdcall CreateFileMappingW( |
参数 | 含义 |
---|---|
hFile | hFile != NULL,创建基于文件的内存区对象(文件映射对象),指向文件对象的句柄。 hFile == INVALID_HANDLE_VALUE(-1),只创建一块共享的内存(内存区对象,基于页面page file)。这种情况下,还必须在dwMaximumSizeHigh和dwMaximumSizeLow参数中指定映射对象的大小,这种情况下该函数创建一个指定大小的文件映射对象。 |
lpFileMappingAttributes | 安全描述符。用于确定该函数返回的内存区对象句柄是否可以由子进程继承。典型的NULL值表示句柄无法被继承。 |
flProtect | 内存区被创建后,物理内存被访问时提供的权限。下面的保护属性可以和 SEC_COMMIT 、SEC_IMAGE 、SEC_IMAGE_NO_EXECUTE 、SEC_LARGE_PAGES 、SEC_NOCACHE 、SEC_RESERVE 、SEC_WRITECOMBINE 组合使用。 |
dwMaximumSizeHigh | 文件映射对象最大大小的高 32 位。 |
dwMaximumSizeLow | 1. 文件映射对象最大大小的低32位,映射页面文件时需要指定这两个值大小,映射文件设置为0即可。 2. 将它们设置为小于文件长度,只能映射文件的一部分,而指定一个大于文件长度的大小则会扩展文件。最后,如这个参数和 dwMaximumSizeHigh 都是零,那么文件映射对象的最大大小等于 hFile 指定文件的实际大小。 3. 映射一个大小为 0 的磁盘文件将会引发出错码为 ERROR_FILE_INVALID 的错误。应用程序应该检测大小为 0 的文件,并拒绝这些文件。 |
lpName | 1. 文件映射对象的名称 2. 如果这个参数与一个已经存在的文件映射对象的名称相同,那么该函数请求以 flProtect 指定的保护属性访问该对象 3. 如果这个参数为 NULL,那么创建的这个文件映射对象将没有名称 4. 如果 lpName 与一个现有的事件,信号量,互斥锁,可等待定时器,或工作对象同名,那么函数调用失败,并且 GetLassError 函数返回 ERROR_INVALID_HANDLE。因为这些对象共享相同的命名空间 5. 该名称可以有一个 “Global" 或 “Local" 前缀来在全局或会话命名空间中显示地创建对象。其余的名称可以包含除反斜杠字符(\)以外的任何字符。在全局命名空间中从一个会话而不是会话0中创建一个文件映射对象需要 SeCreateGlobalPrivilege 特权。更多信息,请参考 Kernel Object Namespaces 6. 快速用户切换是通过使用终端服务会话实现的。第一个用户以会话 0 登陆,第二个用户以会话 1 登陆等等。内核对象名称必须遵循为终端服务所列出的指导原则,以便应用程序可以支持多个用户 |
函数参数可以参考:CreateFileMapping–小甲鱼论坛。
该函数的调用路线如下:
1 | kernel32!CreateFileMappingA --> api-ms-win-core-memory-l1-1-1!CreateFileMappingNumaW --> |
其中 api-ms-win-core-memory-l1-1-1.dll
只是做了中转,将来自 kernel32
的函数调用中转到 kernelbase
中。具体的中转算法可以看:深入剖析 api-ms-* 系列动态链接库、 api-ms-*中转源码。
2.2 NtCreateSection
1 | NTSTATUS __stdcall NtCreateSection ( |
SectionHandle:输出的内存区对象。
DesiredAccess:内存区对象开放的访问权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SECTION_MAP_WRITE |
SECTION_MAP_READ |
SECTION_MAP_EXECUTE |
SECTION_EXTEND_SIZE)
// SECTION_ALL_ACCESS == 0x000F001F
// STANDARD_RIGHTS_REQUIRED是下面四个属性集合,表示匿名用户对对象标准访问权限ObjectAttributes:指向 OBJECT_ATTRIBUTES 结构的指针,该结构包含以对象命名空间格式表示的 Section 名称。该值取至CreateFileMapping的lpFileMappingAttributes参数。
MaximumSize:指定该内存区的最大大小(以字节为单位)。
- page file支撑的内存区:指定该部分的实际大小。
- 映射文件支撑的内存区:文件可以扩展或映射到的最大大小。
SectionPageProtection:指定内存区中每一个页面的保护属性,使用这四个值之一:PAGE_READONLY、PAGE_READWRITE、PAGE_EXECUTE或PAGE_WRITECOPY。
AllocationAttributes:内存区的分配属性,由 CreateFileMapping-flProtect.flags 标志传入。
FileHandle:可选择指定打开的文件对象的句柄。如果该值为NULL,则该部分由分页文件支持。否则,该部分由指定的文件支持。
实际上 SectionPageProtection
和 AllocationAttributes
就是将 CreateFileMapping
的参数 flProtect
的两个部分拆开。
2.3 内存区对象浅析
内存区对象,如同其他的对象一样,也是由对象管理器来分配和释放的。对象管理器创建并初始化一个对象头(它使用对象头来管理这些对象);而内存管理器定义内存区对象的对象体。我们主要关注对象体,就像 _EPROCESS
对象体一样。
1 | //0x28 bytes (sizeof) |
CreateFileMapping
函数返回值为其所创建的内存区对象句柄,该句柄指向 _SECTION
对象。 _SECTION
对象中最重要的成员就是控制区 ControlArea(_CONTROL_AREA)
成员。控制区是一个连接其他内存管理组件的中间枢纽,控制区中的段 Segment(_SEGMENT)
除了和控制区互相指向之外,最重要的就是提供内存区的原型PTE数组。
1 | kd> !handle 0 3 Section |
查看该内存区的信息。可以看到该内存区还没有映射到当前进程,还没有 VAD 节点相关信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23kd> dt _SECTION 0xf65a8780 /r1
nt!_SECTION
+0x000 SectionNode : _RTL_BALANCED_NODE
+0x000 Children : [2] (null)
+0x000 Left : (null)
+0x004 Right : (null)
+0x008 Red : 0y0
+0x008 Balance : 0y00
+0x008 ParentValue : 0
+0x00c StartingVpn : 0
+0x010 EndingVpn : 0
+0x014 u1 : <anonymous-tag>
+0x000 ControlArea : 0x80e38928 _CONTROL_AREA
+0x000 FileObject : 0x80e38928 _FILE_OBJECT
+0x000 RemoteImageFileObject : 0y0
+0x000 RemoteDataFileObject : 0y0
+0x018 SizeOfSection : 0x1540000
+0x020 u : <anonymous-tag>
+0x000 LongFlags : 0x8018080
+0x000 Flags : _MMSECTION_FLAGS
+0x024 InitialPageProtection : 0y000000000100 (0x4)
+0x024 SessionId : 0y0000000000000000000 (0)
+0x024 NoValidationNeeded : 0y0接着看一下该内存区的相关保护信息(
Flags(_MMSECTION_FLAGS)
)。可以看到
File == 1
,说明是由文件映射的内存区。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29kd> dt _MMSECTION_FLAGS 0xf65a87a0
nt!_MMSECTION_FLAGS
+0x000 BeingDeleted : 0y0
+0x000 BeingCreated : 0y0
+0x000 BeingPurged : 0y0
+0x000 NoModifiedWriting : 0y0
+0x000 FailAllIo : 0y0
+0x000 Image : 0y0
+0x000 Based : 0y0
+0x000 File : 0y1
+0x000 AttemptingDelete : 0y0
+0x000 PrefetchCreated : 0y0
+0x000 PhysicalMemory : 0y0
+0x000 ImageControlAreaOnRemovableMedia : 0y0
+0x000 Reserve : 0y0
+0x000 Commit : 0y0
+0x000 NoChange : 0y0
+0x000 WasPurged : 0y1
+0x000 UserReference : 0y1
+0x000 GlobalMemory : 0y0
+0x000 DeleteOnClose : 0y0
+0x000 FilePointerNull : 0y0
+0x000 PreferredNode : 0y000000 (0)
+0x000 GlobalOnlyPerSession : 0y0
+0x000 UserWritable : 0y1
+0x000 SystemVaAllocated : 0y0
+0x000 PreferredFsCompressionBoundary : 0y0
+0x000 UsingFileExtents : 0y0
+0x000 PageSize64K : 0y0查看控制区相关信息。可以看到控制区第一个成员
Segment
。Segment
的第一个成员又指向控制区本身,他们是互指的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48kd> dt 0x80e38928 _CONTROL_AREA /r1
nt!_CONTROL_AREA
+0x000 Segment : 0x948308d0 _SEGMENT
+0x000 ControlArea : 0x80e38928 _CONTROL_AREA
+0x004 TotalNumberOfPtes : 0x1600
+0x008 SegmentFlags : _SEGMENT_FLAGS
+0x00c NumberOfCommittedPages : 0
+0x010 SizeOfSegment : 0x1600000
+0x018 ExtendInfo : (null)
+0x018 BasedAddress : (null)
+0x01c SegmentLock : _EX_PUSH_LOCK
+0x020 u1 : <anonymous-tag>
+0x024 u2 : <anonymous-tag>
+0x028 PrototypePte : 0xeabc46a0 _MMPTE
+0x004 ListHead : _LIST_ENTRY [ 0xa36e76b8 - 0xae401270 ]
+0x000 Flink : 0xa36e76b8 _LIST_ENTRY [ 0xa36e78c8 - 0x80e3892c ]
+0x004 Blink : 0xae401270 _LIST_ENTRY [ 0x80e3892c - 0x8a23d860 ]
+0x004 AweContext : 0xa36e76b8 Void
+0x00c NumberOfSectionReferences : 2
+0x010 NumberOfPfnReferences : 0xcc0
+0x014 NumberOfMappedViews : 0x134
+0x018 NumberOfUserReferences : 0x135
+0x01c u : <anonymous-tag>
+0x000 LongFlags : 0x8080
+0x000 Flags : _MMSECTION_FLAGS
+0x020 FilePointer : _EX_FAST_REF
+0x000 Object : 0x8153ad45 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8153ad45
+0x024 ControlAreaLock : 0n0
+0x028 ModifiedWriteCount : 0
+0x02c WaitList : (null)
+0x030 u2 : <anonymous-tag>
+0x000 e2 : <anonymous-tag>
+0x03c FileObjectLock : _EX_PUSH_LOCK
+0x000 Locked : 0y0
+0x000 Waiting : 0y0
+0x000 Waking : 0y0
+0x000 MultipleShared : 0y0
+0x000 Shared : 0y0000000000000000000000000000 (0)
+0x000 Value : 0
+0x000 Ptr : (null)
+0x040 LockedPages : 1
+0x048 u3 : <anonymous-tag>
+0x000 IoAttributionContext : 0y00000000000000000000000000000 (0)
+0x000 Spare : 0y000
+0x000 ImageCrossPartitionCharge : 0
+0x000 CommittedPageCount : 0y00000000000000000000 (0)我们还是关心描述该控制区所在内存区属性的
Flags(_MMSECTION_FLAGS)
:Image: 0y0
说明该内存区映射文件是一个数据文件,非PE文件。PhysicalMemory: 0y0
说明该内存区还没有挂上物理页。Reserve/Commit: 0y0
一般用于基于 page file 的共享内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29kd> dt _MMSECTION_FLAGS 0x80e38944
nt!_MMSECTION_FLAGS
+0x000 BeingDeleted : 0y0
+0x000 BeingCreated : 0y0
+0x000 BeingPurged : 0y0
+0x000 NoModifiedWriting : 0y0
+0x000 FailAllIo : 0y0
+0x000 Image : 0y0
+0x000 Based : 0y0
+0x000 File : 0y1
+0x000 AttemptingDelete : 0y0
+0x000 PrefetchCreated : 0y0
+0x000 PhysicalMemory : 0y0
+0x000 ImageControlAreaOnRemovableMedia : 0y0
+0x000 Reserve : 0y0
+0x000 Commit : 0y0
+0x000 NoChange : 0y0
+0x000 WasPurged : 0y1
+0x000 UserReference : 0y0
+0x000 GlobalMemory : 0y0
+0x000 DeleteOnClose : 0y0
+0x000 FilePointerNull : 0y0
+0x000 PreferredNode : 0y000000 (0)
+0x000 GlobalOnlyPerSession : 0y0
+0x000 UserWritable : 0y0
+0x000 SystemVaAllocated : 0y0
+0x000 PreferredFsCompressionBoundary : 0y0
+0x000 UsingFileExtents : 0y0
+0x000 PageSize64K : 0y0关于控制区,我们还关心在 AVD 章节学习到的,文件对象
FilePointer
的快速引用_EX_FAST_REF
。可以看到
RefCnt == 0y101 == 0x5
,引用次数为15 - 5 == 10
次。1
2
3
4+0x020 FilePointer : _EX_FAST_REF
+0x000 Object : 0x8153ad45 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x8153ad45x86下,使用掩码
0xFFFFFFF8
查看文件对象_FILE_OBJECT
信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32kd> dt _FILE_OBJECT (0x8153ad45 & 0xFFFFFFF8)
nt!_FILE_OBJECT
+0x000 Type : 0n5
+0x002 Size : 0n128
+0x004 DeviceObject : 0x8b889ca0 _DEVICE_OBJECT
+0x008 Vpb : 0x89ef8dc8 _VPB
+0x00c FsContext : 0x9d5ee830 Void
+0x010 FsContext2 : 0x9d5ee9f0 Void
+0x014 SectionObjectPointer : 0x814a57fc _SECTION_OBJECT_POINTERS
+0x018 PrivateCacheMap : (null)
+0x01c FinalStatus : 0n0
+0x020 RelatedFileObject : (null)
+0x024 LockOperation : 0 ''
+0x025 DeletePending : 0 ''
+0x026 ReadAccess : 0x1 ''
+0x027 WriteAccess : 0x1 ''
+0x028 DeleteAccess : 0x1 ''
+0x029 SharedRead : 0 ''
+0x02a SharedWrite : 0 ''
+0x02b SharedDelete : 0 ''
+0x02c Flags : 0x144050
+0x030 FileName : _UNICODE_STRING "\Windows\SoftwareDistribution\DataStore\DataStore.edb"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x040 Waiters : 0
+0x044 Busy : 0
+0x048 LastLock : (null)
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : (null)
+0x070 IrpListLock : 0
+0x074 IrpList : _LIST_ENTRY [ 0x8153adb4 - 0x8153adb4 ]
+0x07c FileObjectExtension : (null)查看文件对象的
SectionObjectPointer(_SECTION_OBJECT_POINTERS)
成员信息。对于每个打开的文件(通过一个文件对象来表示),都有一个内存区对象指针数组(section object pointers)的结构
_SECTION_OBJECT_POINTERS
。此结构对于维护各种类型的文件访问的数据一致性,以及为文件提供缓存能力是非常关键的。内存区对象指针数组结构指向一个或者两个控制区域(control area)。1
2
3
4
5
6
7
8//0xc bytes (sizeof)
struct _SECTION_OBJECT_POINTERS
{
// 只有DataSectionObject、ImageSectionObject指向所在内存区的控制区
VOID* DataSectionObject; //0x0
VOID* SharedCacheMap; //0x4
VOID* ImageSectionObject; //0x8
};第一个控制区域的用途是,当该文件被当作一个数据文件来访问时,该控制区域被用于映射此文件;另一个控制区域的用途是,当该文件被当作可执行映像来运行时,它被用于映射此此文件。对于一个可执行文件来说,这两个指针是同时存在。可以看下面看雪的这篇文件,有相关介绍。
1
2
3
4
5kd> dt 0x814a57fc _SECTION_OBJECT_POINTERS
nt!_SECTION_OBJECT_POINTERS
+0x000 DataSectionObject : 0x80e38928 Void
+0x004 SharedCacheMap : 0x81497b50 Void
+0x008 ImageSectionObject : (null)可以看到第一个成员
DataSectionObject:0x80e38928
是只想上面提到的控制区_CONTROL_AREA
的。查看段相关信息。第一个成员
ControlArea
指向之前的控制区,所以段和控制区是互指的。其中:BasedAddress: NULL
,如果是PE文件,这个地方是PE文件映射到虚拟地址的ImageBase
。ASLR 动态加载计算的就是这个地方。可以参考《文件映射之地址随机化原理(Win10 x64)-“缓存”》。- 其次我们比较关系描述
_SEGMENT
属性的_SEGMENT_FLAGS
。===ImageSigningType: 0y000
说明该映射文件是一个数据文件,非PE文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58kd> dt 0x948308d0 _SEGMENT /r1
nt!_SEGMENT
+0x000 ControlArea : 0x80e38928 _CONTROL_AREA
+0x000 Segment : 0x948308d0 _SEGMENT
+0x004 ListHead : _LIST_ENTRY [ 0xa36e76b8 - 0xae401270 ]
+0x004 AweContext : 0xa36e76b8 Void
+0x00c NumberOfSectionReferences : 2
+0x010 NumberOfPfnReferences : 0xcc0
+0x014 NumberOfMappedViews : 0x134
+0x018 NumberOfUserReferences : 0x135
+0x01c u : <anonymous-tag>
+0x020 FilePointer : _EX_FAST_REF
+0x024 ControlAreaLock : 0n0
+0x028 ModifiedWriteCount : 0
+0x02c WaitList : (null)
+0x030 u2 : <anonymous-tag>
+0x03c FileObjectLock : _EX_PUSH_LOCK
+0x040 LockedPages : 1
+0x048 u3 : <anonymous-tag>
+0x004 TotalNumberOfPtes : 0x1600
+0x008 SegmentFlags : _SEGMENT_FLAGS
+0x000 TotalNumberOfPtes4132 : 0y0000000000 (0)
+0x000 Spare0 : 0y0
+0x000 SessionDriverProtos : 0y0
+0x000 LargePages : 0y0
+0x000 DebugSymbolsLoaded : 0y0
+0x000 WriteCombined : 0y0
+0x000 NoCache : 0y0
+0x000 Short0 : 0
+0x002 Spare : 0y0
+0x002 DefaultProtectionMask : 0y00110 (0x6)
+0x002 Binary32 : 0y0
+0x002 ContainsDebug : 0y0
+0x002 UChar1 : 0xc ''
+0x003 ForceCollision : 0y0
+0x003 ImageSigningType : 0y000
+0x003 ImageSigningLevel : 0y0000
+0x003 UChar2 : 0 ''
+0x00c NumberOfCommittedPages : 0
+0x010 SizeOfSegment : 0x1600000
+0x018 ExtendInfo : (null)
+0x018 BasedAddress : (null)
+0x01c SegmentLock : _EX_PUSH_LOCK
+0x000 Locked : 0y0
+0x000 Waiting : 0y0
+0x000 Waking : 0y0
+0x000 MultipleShared : 0y0
+0x000 Shared : 0y0000000000000000000000000000 (0)
+0x000 Value : 0
+0x000 Ptr : (null)
+0x020 u1 : <anonymous-tag>
+0x000 ImageCommitment : 0x6050000
+0x000 CreatingProcessId : 0x6050000
+0x024 u2 : <anonymous-tag>
+0x000 ImageInformation : 0x7346744e _MI_SECTION_IMAGE_INFORMATION
+0x000 FirstMappedVa : 0x7346744e Void
+0x028 PrototypePte : 0xeabc46a0 _MMPTE
+0x000 u : <anonymous-tag>使用控制区专用指令
!ca
来解析控制区对象。可以看到在控制区后面有很多个subsection
。对于共享内存(与映射文件相反),通常只有一个subsection的实例,所以subsection
对segment已经提供的信息没有增加多少,然而它的重要性在映射文件中会变得更明显。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46kd> !ca 0x80e38928
ControlArea @ 80e38928
Segment 948308d0 Flink a36e76b8 Blink ae401270
Section Ref 2 Pfn Ref cc0 Mapped Views 134
User Ref 135 WaitForDel 0 Flush Count 0
File Object 8153ad40 ModWriteCount 0 System Views 0
WritableRefs 1 PartitionId 0
Flags (8080) File WasPurged
\Windows\SoftwareDistribution\DataStore\DataStore.edb
Control area 80e38928 file object->sectionobjectpointers linkage invalid.
Segment @ 948308d0
ControlArea 80e38928 ExtendInfo 00000000
Total Ptes 1600
Segment Size 1600000 Committed 0
Flags (c0000) ProtectionMask
Subsection 1 @ 80e38978
ControlArea 80e38928 Starting Sector 0 Number Of Sectors 800
Base Pte 9b23e000 Ptes In Subsect 800 Unused Ptes 0
Flags d Sector Offset 0 Protection 6
Accessed
Flink 80e389ac Blink 80e389ac MappedViews 49
Subsection 2 @ 85360e18
ControlArea 80e38928 Starting Sector 800 Number Of Sectors 800
Base Pte 9b37b000 Ptes In Subsect 800 Unused Ptes 0
Flags d Sector Offset 0 Protection 6
Accessed
Flink 85360e4c Blink 85360e4c MappedViews c8
Subsection 3 @ 85387b18
ControlArea 80e38928 Starting Sector 1000 Number Of Sectors 400
Base Pte 9b3c0000 Ptes In Subsect 400 Unused Ptes 0
Flags d Sector Offset 0 Protection 6
Accessed
Flink 85387b4c Blink 85387b4c MappedViews 20
Subsection 4 @ 8a2e81b0
ControlArea 80e38928 Starting Sector 1400 Number Of Sectors 200
Base Pte b5dde000 Ptes In Subsect 200 Unused Ptes 0
Flags 1000d Sector Offset 0 Protection 6
Accessed Static
Flink 8a2e81e4 Blink 8a2e81e4 MappedViews 1在上一篇文章 VAD 中,可以看到控制区
_CONTROL_AREA
只是_SUBSECTION
的第一个成员。所以这里看起来有一些奇怪,为什么这里的控制区后面跟着这么多子内存区呢?所以我来论证这里实际上就是一个_SUBSECTION
结构:首先来查看
_SUBSECTION
结构,我猜测_CONTROL_AREA
本身就是一个_SUBSECTION
的成员,并不是一个独立使用的结构。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dt _SUBSECTION
nt!_SUBSECTION
+0x000 ControlArea : Ptr32 _CONTROL_AREA
+0x004 SubsectionBase : Ptr32 _MMPTE
+0x008 NextSubsection : Ptr32 _SUBSECTION
+0x00c GlobalPerSessionHead : _RTL_AVL_TREE
+0x00c CreationWaitList : Ptr32 _MI_CONTROL_AREA_WAIT_BLOCK
+0x00c SessionDriverProtos : Ptr32 _MI_PER_SESSION_PROTOS
+0x010 u : <anonymous-tag>
+0x014 StartingSector : Uint4B
+0x018 NumberOfFullSectors : Uint4B
+0x01c PtesInSubsection : Uint4B
+0x020 u1 : <anonymous-tag>
+0x024 UnusedPtes : Pos 0, 30 Bits
+0x024 ExtentQueryNeeded : Pos 30, 1 Bit
+0x024 DirtyPages : Pos 31, 1 Bit这里的
ControlArea
地址为0x80e38928
,第一个_SUBSECTION
地址为80e38978
:注意看:第一个成员控制区的地址
0x80e38928
,以及下一个_SUBSECTION
和上面!ca
指令输出的情况是一模一样的。所以得到验证。
注意:这些所有子内存区指向的控制区对象都是同一个,都是
0x80e38928
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dt _SUBSECTION 0x80e38978
nt!_SUBSECTION
+0x000 ControlArea : 0x80e38928 _CONTROL_AREA
+0x004 SubsectionBase : 0x9b23e000 _MMPTE
+0x008 NextSubsection : 0x85360e18 _SUBSECTION
+0x00c GlobalPerSessionHead : _RTL_AVL_TREE
+0x00c CreationWaitList : (null)
+0x00c SessionDriverProtos : (null)
+0x010 u : <anonymous-tag>
+0x014 StartingSector : 0
+0x018 NumberOfFullSectors : 0x800
+0x01c PtesInSubsection : 0x800
+0x020 u1 : <anonymous-tag>
+0x024 UnusedPtes : 0y000000000000000000000000000000 (0)
+0x024 ExtentQueryNeeded : 0y0
+0x024 DirtyPages : 0y0
下面列出几个对象之间的关系。
关于本节后续工作:
- 参考文章《Windows 7 x64内存管理》之用户范围内存管理 Final,结合 Windbg,单步解析共享内存、映射镜像文件、数据文件的差异。
- 逆向分析
NtCreateSection
全过程。
3 物理内存
学习本章前,先了解两个概念:
- 虚拟内存地址:将32位拆成两部分,高20位叫做虚拟页号(VPN),低12位为页内偏移。高20位又可以拆成[PDPTE]、PDE、PTE。
- 物理内存地址:高20位叫做物理页面编号(也叫做页面帧编号,PFN),由20位的VPN转译而来。低12位页内偏移使用的就是虚拟地址的页内偏移。页内偏移的12位并不参与地址的转译过程。
工作集描述了一个进程或者系统所拥有的驻留在内存中的页面。页面帧编号(PFN, page frame number)数据库则描述了物理内存中每个页面的状态。
查看物理内存使用情况:
打开任务管理器-资源监视器可以看到该虚拟机我分配了 2GB 内存,下图显示 2025 MB。
在
_KUSER_SHARED_DATA
中有一个成员NumberOfPhysicalPages
存储着该系统总共的物理页面数量。注意:
_KUSER_SHARED_DATA
的地址没有被随机化,固定地址如下。关于KUSER_SHARED_DATA结构结构成员可以查看该链接官方文档。内核起始地址 内核结束地址 用户起始地址 用户结束地址 32 系统 0xFFDF0000 0xFFDF0FFF 0x7FFE0000 0x7FFE0FFF 64 系统 0xFFFFF780`00000000 0xFFFFF780`00000FFF 0x7FFE0000 0x7FFE0FFF 1
2
3
4
5kd> dt _KUSER_SHARED_DATA 0xFFDF0000
ntdll!_KUSER_SHARED_DATA
...
+0x2e8 NumberOfPhysicalPages : 0x7e955
...可以看到一共有 0x7e955 页,则共 $0x7E955*4KB = 0x1FA554KB = 2073940KB = 2025MB$。
3.1 PFN 数据库
操作系统使用一个结构体数组来表示 PFN 数据库。
PFN 数据库是一个数组,页面帧编号 PFN 为该数组的索引,该数组的每一个元素为 _MMPFN
结构。该数组的大小为 MmPfnDatabase[_KUSER_SHARED_DATA.NumberOfPhysicalPages]
。
1 | //0x1c bytes (sizeof) |
MmPfnDatabase
为 PFN 数组的起始地址,PFN 为数组的索引。宏 MI_PFN_ELEMENT
可以根据 PFN 索引获取指定的 PFN 项。
MmPfnDatabase
数组结构如下:
可以参考文章:
Inside Windows Page Frame Number (PFN) - Part 1
Inside Windows Page Frame Number (PFN) – Part 2
针对以上 _MMPFN
结构,不同状态下使用不同的成员:
下面初步罗列一些成员,具体成员信息及使用请看《Windows Internals 7 物理内存-PFN数据结构》。
u1.Active:描述活动页面相关属性。
PteAddress:指向当前物理页面对应的 PTE 的虚拟地址。
OriginalPte:指向此页面 PTE 的原始内容。原始内容可能是一个原型 PTE。记录当前物理页上一次对应的 PTE,以后当改物理页面不再使用时,可以恢复为原来的 PTE。
u2.ShareCount:表明指向该物理页面的 PTE 的数量。
u3:是所有状态的 PFN 项共享的,很重要,描述物理页面的状态。
u3.e2.ReferenceCount:表示这个页面必须要保留在内存中的引用计数,包括该页面被加入工作集或者由于 I/O 的需要而被锁定的次数。
u3.e1.PageLocation:表示当前物理页处在哪一个状态中。
_MMLIST
列出所有的状态和对应的枚举值。1
2
3
4
5
6
7
8
9
10
11
12//0x4 bytes (sizeof)
enum _MMLISTS
{
ZeroedPageList = 0,
FreePageList = 1,
StandbyPageList = 2,
ModifiedPageList = 3,
ModifiedNoWritePageList = 4,
BadPageList = 5,
ActiveAndValid = 6,
TransitionPage = 7
};u3.e1.ReadInProgress/WriteInProgress:表示读操作和写操作正在进行。
u3.e1.Modified:表示物理页面已被修改。
u4.PteFrame:当前 PFN 编号。
u4.PrototypePte:表示该 PFN 项引用的 PTE 是一个原型 PTE。
页面的状态类型存放在 _MMPFN.u3.e1.PageLocation
成员里,状态值由 _MMLISTS 来枚举:
1 | //0x4 bytes (sizeof) |
在这 9 种状态中,有 6 种状态是通过链表进行维护的(除活动、转移、坏的除外)。备用状态共有 8 个链表表,对应于 8 中不同优先级的页面。零化页面、空闲页面为单链表,其余链表为双链表。
u3.e1.PageLocation
域保存的枚举值不仅说明当前物理页处于什么状态,还表明该页面处于哪一个链表中。
在 Windows XP 下全局数组 MmPageLocationList
列出了上述 6 种状态页面的链表头,该数组的索引为 _MMLISTS
枚举值,每一个元素为 _MMPFNLIST
结构:
1 | //0x14 bytes (sizeof) |
所以我们可以使用 MmPfnDatabase[PFN]
定位到该页面对应的 _MMPFN
结构,然后根据 MmPageLocationList[MMPFN.u3.e1.PageLocation]
找到对应状态页面的链表头。
注意:零化页面和空闲页面仅构成单链表,其余的是双链表。
以下为 XP 的图:
3.2 物理页面状态转移
内存管理器根据系统内存的数量以及各个进程对于内存的需求,动态地调度这些物理页面的使用。例如,当一个进程需要内存时,内存管理器可以从零化链表或空闲链表中找到一个或多个页面来满足进程的需求;当进程退出时,内存管理器回收该进程的页面;当物理内存紧缺时,内存管理器按照特定的策略将有些进程中的页面换出到外存中,从而腾出物理内存以作他用。在内存管理器的动态调度过程中,每个页面都有可能经历各种状态变化。这一节我们来讨论物理页面的状态变化。
在以下转移描述中,不考虑已修改但不写出(不用于用户模式)、坏页面、ROM。
我们从页面错误处理的角度出发,来看物理页面的状态转移。在页面错误处理过程中,当进程需要一个页面时,它有可能从以下几个链表中获得物理内存页面,从而解决页面错误。所以,这几个链表中的页面都有可能进入到一个工作集中。
如果页面错误需要一个零页面来满足(要求零的页面错误:引用一个被定义为全零的页面,或者一个从未被访问过的用户模式私有提交页面),此时内存管理器会首先试图从零页面列表中获取一个页面,如果该列表是空的,则它从空闲页面列表中获取一个,并且零化该页面(即用零填充)。如果空闲列表也是空的,则它转到备用列表,并零化此页面。
页面查找顺序:零化页面 --> 空闲页面 --> 备用链表。
从空闲链表到零化链表的转移是由一个称为零页面线程(zero page thread, System 进程中的第一个线程)的系统线程来完成的。系统在阶段 1 初始化完成以后,调用
MmZeroPageThread
函数,因此,阶段1初始化线程蜕变成零页面线程,该函数是一个无限循环。当空闲列表有 8 个或者更多页面时,零页面线程就会得到事件信号,执行零化任务。该线程的优先级为0,而用户线程最低优先级为1,所以只有在 CPU 空闲的时候才会有机会切换到零化线程上。
当内存管理器不要求一个零初始化的页面时,它首先到空闲列表上寻找页面。如果该列表是空的,则转到零化的列表上。如果零化的列表也是空的,则转到备用列表上。内存管理器在使用一个来自备用列表的页面帧以前,它必须首先向后回退一下, 从仍然指向该页面帧的无效PTE(或者原型PTE)中删除此引用。因为PFN数据库中的表项包含了指回到原先用户的页表(或者对于共享页面而言,指向原型PTE)的指针,所以,内存管理器可以很快地找到该PTE,并且做出正确的修改。
页面查找顺序:空闲页面 --> 零化页面 --> 备用链表。
当一个进程必须从它的工作集中放弃一个页面(因为它引用了一个新的页面而它的工作集是满的,或者因为内存管理器修剪了它的工作集)时,如果该页面是**干净的 (未被修改过)**,则它被转到备用列表中;如果该页面驻留于工作集的时候已被修改过,则转到修改列表中。
当一个进程退出的时候,所有的私有页面都被转到空闲列表中。而且,当一个由页 面文件支撑的内存区的最后一个引用被关闭时,如果该内存区已经没有剩余的映射视图,那么这些页面也被转到空闲列表中。
随着工作集中的页面被加入到修改链表中,修改链表可能会变得很大,而零化链表或备用链表则越来越小,到一定时候,内存管理器会把修改链表中的页面数据写到磁盘上, 从而把页面转移到备用链表中,这一任务是由一个称为修改页面写出器(modified page writer)的组件(MiModifiedPageWriter 函数)和一个称为映射页面写出器(mapped page writer)的组件( MiMappedPageWriter 函数)来完成的,关于这两个写出器的工作过程,请参考 4.5.4 节。
特别说明:申请内存时,并不会将虚拟内存挂上物理页面。当一个新分配的地址第一次被引用时,会发生一个页面错误,因为VMM在第一次实际访问这个地址前都不会为其分配物理页面。此时的错误称为demand-zero错误,因为他必须完成VA到物理页的映射,并且这个页面要初始化为0。
3.3 物理内存限制
Windows 和 Windows Server 版本的内存限制
3.4 练习:查看PFN数据库
在上面的学习过程中,了解了不同状态下使用 _MMPFN
结构不同成员。在 XP 中,有 MmPageLocationList
全局变量将描述不同状态的 6 个链表进行表达。但是 Windows 10 上并没有发现这些类似的全局变量。但是《Windows Internals 7 P442》给出了相关结构的成员,如下图:
关于结构 MI_PARTITION
,看雪这篇文章 分享一下win10的内存压缩有部分提及。
MmPfnDatabase
数组 MmPfnDatabase
结构如下:
查看
MmPfnDatabase
数组地址。1
2
3
4
5kd> dd MmPfnDatabase
821187c4 84400000 83b7f000 19801268 00000e07
821187d4 85935b28 0007f0ec 00000000 81e0f000
821187e4 82585000 825f1000 82585000 00000000
821187f4 00000000 859f5e40 23c62a67 00000000可以看到
MmPfnDatabase
数组的地址为0x84400000
。查看
MmPfnDatabase[0]
,即 PFN = 0 时,物理页面0~0xfff
对应的_MMPFN
结构全部为0
。1
2
3
4
5kd> dd 0x84400000
84400000 00000000 00000000 00000000 00000000
84400010 00000000 00000000 00000000 00000000
84400020 c07fe800 00000080 00002000 00000001
84400030 00560001 0000191a 00000000 c07fe808查看
MmPfnDatabase[8]
:1
2
3
4
5
6
7
8
9
10
11kd> dt _MMPFN 0x84400000+0x1c*8
nt!_MMPFN
+0x000 ListEntry : _LIST_ENTRY [ 0x0 - 0xc07fe838 ]
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <anonymous-tag>
+0x004 PteAddress : 0xc07fe838 _MMPTE
+0x004 PteLong : 0xc07fe838
+0x008 OriginalPte : _MMPTE
+0x010 u2 : _MIPFNBLINK
+0x014 u3 : <anonymous-tag>
+0x018 u4 : <anonymous-tag>
!pfn pageframe
使用 !pfn PageFrame
查看对应 PFN
的信息:
1 | kd> !pfn 8 |
可以看到该指令解析的信息和 dt _MMPFN 0x84400000+0x1c*8
是一样的。
!vtop CR3 VirtualAddress
!vtop
指令可以将虚拟地址转换成对应的物理地址。格式为:!vtop CR3 VirtualAddress
。使用该指令之前需要使用到 !process
:
1 | !process 0 0 // 列出所有进程信息 |
步骤如下:
1 | kd> !process 0 0 |
可以看到物理地址为7ae000e0
,则 PFN == 7ae00
。
!pte VirtualAddress/Physical Address
该指令用来识别一个地址是虚拟地址还是物理地址,并将地址对应的 PDE、PTE 列出来。
1 | kd> !pte 0x844000E0 |
可以看到虚拟地址 0x844000E0
被识别出来是一个虚拟地址,其对应的 PTE 为0xC0422000
(虚拟地址)。PFN 为7ae00
。
!memusage与!vm
指令 !memusage
可以列出所有物理内存使用的情况,该指令建议一般不要使用,不要需要加载很久遍历整个物理内存。
指令 !vm
分析虚拟内存的使用情况。
可以参考文章:
Inside Windows Page Frame Number (PFN) - Part 1
Inside Windows Page Frame Number (PFN) – Part 2
!dml_proc
!dml_proc
可以罗列出所有进程的 EPROCESS
。
3.5 练习:申请私有内存查看是否分配物理内存
本实验目的:先在指定内存申请一块内存,仅预留(MEM_RESERVE)不提交(MEM_COMMIT),此时查看该虚拟内存是否挂上 PTE。然后去提交内存,看 PTE 变化。
一、申请预留内存后进行提交,然后访问内存,观察 PTE
在
0x00520120
申请预留内存。1
2
3
4
5
6
7
8
9
10
11
12kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS b11cd040 SessionId: 1 Cid: 1e98 Peb: 00939000 ParentCid: 0ad8
DirBase: 7f0a9800 ObjectTable: b15f34c0 HandleCount: 36.
Image: 20220921_VirtualAlloc_Test2.exe
kd> !vtop 7f0a9800 520000
X86VtoP: Virt 0000000000520000, pagedir 000000007f0a9800
X86VtoP: PAE PDPE 000000007f0a9800 - 000000000f86a801
X86VtoP: PAE PDE 000000000f86a010 - 0000000000000000
X86VtoP: PAE zero PDE
Virtual address 520000 translation fails, error 0xD0000147.可以看到此时内存还没有提交,该虚拟地址还没有挂上相应的 PTE,也没有对应的物理地址。
注意:这里的返回地址
Va
是页面起始地址0x520000
。如下图:现在去提交内存(
Va = (int*)VirtualAlloc((LPVOID)0x520120, 4*0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
)。1
2
3
4
5
6
7kd> !vtop 7f0a9800 520000
X86VtoP: Virt 0000000000520000, pagedir 000000007f0a9800
X86VtoP: PAE PDPE 000000007f0a9800 - 000000000f86a801
X86VtoP: PAE PDE 000000000f86a010 - 0000000009c98867
X86VtoP: PAE PTE 0000000009c98900 - 00002000000000c0
X86VtoP: PAE PTE not present, pagefile 0:0000000000002000
Virtual address 520000 translation fails, error 0x10000114.可以看到此时 PTE(
00002000000000c0
) 的 P 位还是为0
。此时 PAT、PCD 位都被置1。此时去往地址里面写数据(访问地址)。
1
2
3
4
5
6
7kd> !vtop 7f0a9800 520000
X86VtoP: Virt 0000000000520000, pagedir 000000007f0a9800
X86VtoP: PAE PDPE 000000007f0a9800 - 000000000f86a801
X86VtoP: PAE PDE 000000000f86a010 - 0000000009c98867
X86VtoP: PAE PTE 0000000009c98900 - 000000003d819967
X86VtoP: PAE Mapped phys 000000003d819000
Virtual address 520000 translates to physical address 3d819000.注意:这里的返回地址
Va
是页面起始地址0x520000
。
二、申请预留内存后,不提交,直接访问内存,观察 PTE
在
0x00520120
申请预留内存,观察 PTE。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> !dml_proc
Address PID Image file name
8c2b0040 1040 20220921_Virtu..
kd> !process 1040 0
Searching for Process with Cid == 1040
PROCESS 8c2b0040 SessionId: 1 Cid: 1040 Peb: 01022000 ParentCid: 0ad8
DirBase: 7f0a9800 ObjectTable: b15f4900 HandleCount: 36.
Image: 20220921_VirtualAlloc_Test2.exe
kd> !vtop 7f0a9800 520000
X86VtoP: Virt 0000000000520000, pagedir 000000007f0a9800
X86VtoP: PAE PDPE 000000007f0a9800 - 000000005efa7801
X86VtoP: PAE PDE 000000005efa7010 - 0000000000000000
X86VtoP: PAE zero PDE
Virtual address 520000 translation fails, error 0xD0000147.此时 PDE 为 0,没有挂上 PTE。
直接往地址
0x520000
写入数据,观察 PTE。1
2
3
4
5
6kd> !vtop 7f0a9800 520000
X86VtoP: Virt 0000000000520000, pagedir 000000007f0a9800
X86VtoP: PAE PDPE 000000007f0a9800 - 000000005efa7801
X86VtoP: PAE PDE 000000005efa7010 - 0000000000000000
X86VtoP: PAE zero PDE
Virtual address 520000 translation fails, error 0xD0000147.此时 PDE 还为 0,没有挂上 PTE。
3.6 MDL 物理内存
《Windows 内核原理与实现 P279》、《Windows 内核情景分析下 9.12 P918》。