虚拟内存管理
在64位的操作系统下,理论上可以使用的虚拟地址空间为2^64,也就是16EB。但是处理器对此做了限制,不能使用高16位,因为实现64位地址宽度会增加系统的复杂度和地址转换的成本,仅仅48位虚拟地址就用了四级页表甚至五级页表转换为物理地址。
Windows操作系统又限制使用了高16位的高4位即[44,47]位。在用户空间下,高20位为0;在内核空间下,高20位为1。
内核空间内存管理
Windows系统在初始化时创建了两种内存池,即可以换入磁盘的分页内存池和不可换入的非分页内存池。
分页内存无法从IRQL>=DPC调度级别上访问。因为会触发页面异常,在IRQL>=DPC时,无法处理页面异常(时钟中断用到了一个DPC的软中断。当IRQL>=DPC时,是不会被打断的,也就是不会进行由时钟中断引发的线程切换。当触发#PF会导致IO操作,进而引起线程切换,这时会将一个内核特殊APC插入线程以触发软中断。然而,此时不能进行线程切换,也无法接收APC的软中断。)。
可以通过某些API将内存地址“锁住”,如r3的VirtualLock
,r0的MmProbeAndLockPage
。
进程的CR3指向的PML4表的高256项相同,内核空间的大部分是共享的。
在windbg中可以使用!poolused [Flags [TagString]]
查看系统内存使用摘要。
用户空间内存管理
进程用户空间分配的是虚拟内存,不能直接使用物理内存。虚拟地址会通过四级页表查询找到对应物理地址。分配连续的虚拟内存,在物理内存上不一定是连续的。
对于每个进程,操作系统用虚拟地址描述符VAD(平衡二叉树)树来管理,记录了进程中所分配内存的属性。
定位一下进程的VAD树,在进程结构体_EPROCESS
的+0x7d8偏移处(不同版本的操作系统可能不同)。
VadRoot
字段指向一个 MM_AVL_TABLE
结构体,该结构体包含了一个平衡二叉树,用于存储该进程的虚拟地址空间描述符节点。每个节点都包含了虚拟地址空间的起始地址、结束地址以及一些其他描述符信息,如是否可读、可写、可执行等。
在Windbg中使用
!vad VAD-Root [Flag]
!vad Address 1
可显示vad树的信息。
查看进程KmdManager.exe的Vad树信息。
当系统调用VirtualAlloc
等函数时,则会在vad树上增加一个结点(_MMVAD
结构体)。
vad的那一列表示树节点结构体_MMVAD
地址。
Level表示在树的第几层,根节点为0。
Start表示起始虚拟页号,End表示结束虚拟页号。
Commit表示提交了几个物理页。
Mapped或者Private 表示是映射内存还是私有内存。
-
Private Memory :
VirtualAlloc
/VirtualAllocEx
, 私有内存。 -
Mapped Memory :
CreateFileMapping
,映射内存,共享文件,镜像文件。
镜像文件通常通过LoadLibrary
加载,属性WRITECOPY
,避免影响到其它使用此文件的。
后面的话就是表示内存区域的属性,这里对WRITECOPY
写拷贝进行较为详细解释:
当向此属性的页面中进行写操作时,会分配一个新的物理内存页面,将原来的数据复制到新的内存页面,重新执行该写操作。
当向内存页面中进行写操作时,会执行如下操作:
第0步:检查被写入内存页的PTE属性,如果页面的PTE属性的U/S位为0,触发页保护异常,写入失败返回0XC00000005错误码;否则进入下一步。
第1步:检查PTE的Write/Read位,如果为1表示检查通过则直接写入数据到内存,写入成功!如果为0,表示此PTE描述的内存页面为只读页,继续检查PTE的第9位,如果此位为0,触发页保护异常。
第2步:进入页保护异常后就完全将控制权交给操作系统,操作系统会先检查被写入页面的进程VAD属性,如果为不存在写拷贝属性,写入失败,返回0xc00000005错误码;如果存在写拷贝属性,进入下一步。
第3步:如果存在写拷贝属性,操作系统会在将被写入页面的PTE的第9位置1,将CPU的EIP设置为之前程序往内存写入数据的那条指令的地址,进入下一步。
第4步:继续执行写入数据到内存的那条指令,注意此时PTE属性的write/Read仍然为0,仍然会再次触发页保护异常,返回第0步,接着走第2步,此时操作系统会发现PTE的第9位被置1了,如果PTE属性的第9位为1,则分配一个新的物理内存页面,将原来的数据复制到新的内存页面,修改PTE并且将PTE的write/Read位置1,页保护异常返回到程序之前写入数据到内存的那条指令,再次执行这条指令因为PTE的Write/Read位为1,会直接写入成功!
需要注意的是,栈不受VAD树管理。系统在创建线程时,会为该线程分配一段物理内存页,并映射到该线程的栈空间中,随后,将栈空间的起始地址记录在该线程的 线程环境块TEB
中。
_MMVAD
VAD树结点——结构体_MMVAD
。
//0x88 bytes (sizeof)
struct _MMVAD
{
struct _MMVAD_SHORT Core; //0x0
union
{
ULONG LongFlags2; //0x40
volatile struct _MMVAD_FLAGS2 VadFlags2; //0x40
} u2; //0x40
struct _SUBSECTION* Subsection; //0x48
struct _MMPTE* FirstPrototypePte; //0x50
struct _MMPTE* LastContiguousPte; //0x58
struct _LIST_ENTRY ViewLinks; //0x60
struct _EPROCESS* VadsProcess; //0x70
union
{
struct _MI_VAD_SEQUENTIAL_INFO SequentialVa; //0x78
struct _MMEXTEND_INFO* ExtendedInfo; //0x78
} u4; //0x78
struct _FILE_OBJECT* FileObject; //0x80
};
需要注意,私有内存不涉及section对象,所以对于Privite Memory来说,在_MMVAD
结构中, 仅仅只有 _MMVAD_SHORT
类型的Core成员有用。至于_MMVAD
结构扩展字段对于Privite Memory 是没有意义的。也可以说Privite Memory对应的vad树结点类型是_MMVAD_SHORT
类型。
Core(_MMVAD_SHORT
)
//0x40 bytes (sizeof)
struct _MMVAD_SHORT
{
union
{
struct
{
struct _MMVAD_SHORT* NextVad; //0x0
VOID* ExtraCreateInfo; //0x8
};
struct _RTL_BALANCED_NODE VadNode; //0x0
};
ULONG StartingVpn; //0x18
ULONG EndingVpn; //0x1c
UCHAR StartingVpnHigh; //0x20
UCHAR EndingVpnHigh; //0x21
UCHAR CommitChargeHigh; //0x22
UCHAR SpareNT64VadUChar; //0x23
LONG ReferenceCount; //0x24
struct _EX_PUSH_LOCK PushLock; //0x28
union
{
ULONG LongFlags; //0x30
struct _MMVAD_FLAGS VadFlags; //0x30
struct _MM_PRIVATE_VAD_FLAGS PrivateVadFlags; //0x30
struct _MM_GRAPHICS_VAD_FLAGS GraphicsVadFlags; //0x30
struct _MM_SHARED_VAD_FLAGS SharedVadFlags; //0x30
volatile ULONG VolatileVadLong; //0x30
} u; //0x30
union
{
ULONG LongFlags1; //0x34
struct _MMVAD_FLAGS1 VadFlags1; //0x34
} u1; //0x34
union
{
ULONGLONG EventListULongPtr; //0x38
UCHAR StartingVpnHigher:4; //0x38
} u5; //0x38
};
_MMVAD_SHORT
为_MMVAD
的主要结构。
StartingVpn为起始虚拟页号,EndingVpn为结束虚拟页号。
Core.u.VadFlags(_MMVAD_FLAGS
)
//0x4 bytes (sizeof)
struct _MMVAD_FLAGS
{
ULONG Lock:1; //0x0
ULONG LockContended:1; //0x0
ULONG DeleteInProgress:1; //0x0
ULONG NoChange:1; //0x0
ULONG VadType:3; //0x0
ULONG Protection:5; //0x0
ULONG PreferredNode:7; //0x0
ULONG PageSize:2; //0x0
ULONG PrivateMemory:1; //0x0
};
NoChange可以用来锁页,但是只针对私有内存。
VadType枚举定义如下:
typedef enum _MI_VAD_TYPE {
VadNone,
VadDevicePhysicalMemory,
VadImageMap,
VadAwe,
VadWriteWatch,
VadLargePages,
VadRotatePhysical,
VadLargePageSection
} MI_VAD_TYPE, *PMI_VAD_TYPE;
Protection表示当前VAD结点内存区域的读写属性。
0x1为READONLY; 0x2为EXECUTE; 0x3为EXECUTE _READ; 0x4为READWITER; 0x5为WRITECOPY; 0x6为EXECUTE _READWITER; 0x7为EXECUTE_WRITECOPY
PrivateMemory表示是私有内存(Private)还是映射内存(Private)。
Subsection(_SUBSECTION
*)
//0x38 bytes (sizeof)
struct _SUBSECTION
{
struct _CONTROL_AREA* ControlArea; //0x0
struct _MMPTE* SubsectionBase; //0x8
struct _SUBSECTION* NextSubsection; //0x10
union
{
struct _RTL_AVL_TREE GlobalPerSessionHead; //0x18
struct _MI_CONTROL_AREA_WAIT_BLOCK* CreationWaitList; //0x18
struct _MI_PER_SESSION_PROTOS* SessionDriverProtos; //0x18
};
union
{
ULONG LongFlags; //0x20
struct _MMSUBSECTION_FLAGS SubsectionFlags; //0x20
} u; //0x20
ULONG StartingSector; //0x24
ULONG NumberOfFullSectors; //0x28
ULONG PtesInSubsection; //0x2c
union
{
struct _MI_SUBSECTION_ENTRY1 e1; //0x30
ULONG EntireField; //0x30
} u1; //0x30
ULONG UnusedPtes:30; //0x34
ULONG ExtentQueryNeeded:1; //0x34
ULONG DirtyPages:1; //0x34
};
Subsection.ControlArea(_CONTROL_AREA
*)
ControlArea是SubSection中的重要成员。
//0x80 bytes (sizeof)
struct _CONTROL_AREA
{
struct _SEGMENT* Segment; //0x0
union
{
struct _LIST_ENTRY ListHead; //0x8
VOID* AweContext; //0x8
};
ULONGLONG NumberOfSectionReferences; //0x18
ULONGLONG NumberOfPfnReferences; //0x20
ULONGLONG NumberOfMappedViews; //0x28
ULONGLONG NumberOfUserReferences; //0x30
union
{
ULONG LongFlags; //0x38
struct _MMSECTION_FLAGS Flags; //0x38
} u; //0x38
union
{
ULONG LongFlags; //0x3c
struct _MMSECTION_FLAGS2 Flags; //0x3c
} u1; //0x3c
struct _EX_FAST_REF FilePointer; //0x40
volatile LONG ControlAreaLock; //0x48
ULONG ModifiedWriteCount; //0x4c
struct _MI_CONTROL_AREA_WAIT_BLOCK* WaitList; //0x50
union
{
struct
{
union
{
ULONG NumberOfSystemCacheViews; //0x58
ULONG ImageRelocationStartBit; //0x58
};
union
{
volatile LONG WritableUserReferences; //0x5c
struct
{
ULONG ImageRelocationSizeIn64k:16; //0x5c
ULONG SystemImage:1; //0x5c
ULONG CantMove:1; //0x5c
ULONG StrongCode:2; //0x5c
ULONG BitMap:2; //0x5c
ULONG ImageActive:1; //0x5c
ULONG ImageBaseOkToReuse:1; //0x5c
};
};
union
{
ULONG FlushInProgressCount; //0x60
ULONG NumberOfSubsections; //0x60
struct _MI_IMAGE_SECURITY_REFERENCE* SeImageStub; //0x60
};
} e2; //0x58
} u2; //0x58
struct _EX_PUSH_LOCK FileObjectLock; //0x68
volatile ULONGLONG LockedPages; //0x70
union
{
ULONGLONG IoAttributionContext:61; //0x78
ULONGLONG Spare:3; //0x78
ULONGLONG ImageCrossPartitionCharge; //0x78
ULONGLONG CommittedPageCount:36; //0x78
} u3; //0x78
};
控制域ControlArea,描述section的一些内容,内存区对象的核心。
一般情况下,
一个完整的的控制域对象之后还紧跟着N个_SUBSECTION
结构。
一个EXE或DLL有多个段,.text,.data有多少个段就有多少个Subsection,每个Subsection对应文件中的一个section,用于描述文件中每节的映射信息,可读可写写时复制。
PE文件有多少个节就有多少个Subsection,所有的Subsection构成一个单链表,每个Subsection都有一个指针回到_CONTROL_AREA
结构。
FilePointer:这是映射文件特有的,如果是共享内存是靠内存页面支撑的,那么此处为空。指向的是内存块对应的_FILE_OBJECT
文件对象(后3位需要置0),如果_FILE_OBJECT
文件对象有效就可以得到对应文件的FileName等信息。
ControlArea.Segment(_SEGMENT
*)
//0x48 bytes (sizeof)
struct _SEGMENT
{
struct _CONTROL_AREA* ControlArea; //0x0
ULONG TotalNumberOfPtes; //0x8
struct _SEGMENT_FLAGS SegmentFlags; //0xc
ULONGLONG NumberOfCommittedPages; //0x10
ULONGLONG SizeOfSegment; //0x18
union
{
struct _MMEXTEND_INFO* ExtendInfo; //0x20
VOID* BasedAddress; //0x20
};
struct _EX_PUSH_LOCK SegmentLock; //0x28
union
{
ULONGLONG ImageCommitment; //0x30
ULONG CreatingProcessId; //0x30
} u1; //0x30
union
{
struct _MI_SECTION_IMAGE_INFORMATION* ImageInformation; //0x38
VOID* FirstMappedVa; //0x38
} u2; //0x38
struct _MMPTE* PrototypePte; //0x40
};
_SEGMENT.TotalNumberOfPtes
表示总共有多少Ptes
_SEGMENT.BasedAddress
为对应内存块的基地址。
_SEGMENT.SizeOfSegment
为对应内存块的大小。
_SEGMENT.PrototypePte
表示PTE数组,这个与_MMVAD.FirstPrototypePte
相同。记录的是内存区域第一页的PTE,查看物理内存的值,其值正是模块刚开始时的数据。
NextSubsection(_SUBSECTION
*)
指向下一个SubSection。所有的SubSection的ControlArea都相等,同时与真正的Section对象的ControlArea一样。一个节区Section会被分成很多的SubSection,所以他们指向的ControlArea相等。
FirstPrototypePte与LastContiguousPte
VAD对于共享内存会包含他的原型PTE。
+0x50偏移FirstPrototypePte为第一个原型PTE即数组开始地址,+0x58偏移的LastContiguousPte为最后一个原型PTE。 对Private Memory无意义。
ViewLinks
链表结构。表示有哪些进程加载了这个镜像文件,如果是DLL,可以通过此链表遍历出加载此DLL的进程。
ViewLinks连接的是_EPROCESS
+0x18的位置。
VadsProcess
内存区域所属主进程的_EPROCESS
。
虚拟内存的两种状态
reserved(保留)与comitted(提交)。
- reserved 预留,表示预先分配的虚拟内存,但还没有映射到物理内存,在使用时需要先命中物理页
- commited 已经提交,表示虚拟内存已经映射到了物理内存或已经缓存在磁盘
- commited pages 也是 private pages,表示不能与其他进程共享
保留是指只在VAD树中进行的属性进行挂载,不会挂物理页,以后会用。
提交就是指挂上物理页。
当内存页面被挂上物理页才能被使用。
物理空间内存管理
Windows实现页面换出与换入,当页面换出时,对应PTE表项就会变成无效PTE。
Windows使用内存页帧数据库MMPFNDATABASE来描述每个物理页的属性。
MMPFNDATABASE
_MMPFN
MMPFNDATABASE是_MMPFN
结构体数组。_MMPFN
结构大小为0x30.
描述:
1.全局结构体数组,称为“页帧数据库”,包含了当前操作系统中所有的物理页。
2.每一个物理页都有一个对应的_MMPFN
结构体。
3.通过全局变量MmPfnDatabase可以找到这个结构体的起始位置(在WinDbg中使用命令dq MmPfnDatabase进行查看)。
4.每一个MMPFN结构体之间在内存中是紧挨着的。
查看MmPfnDataBase。
查看第0个物理页。
//0x30 bytes (sizeof)
struct _MMPFN
{
union
{
struct _LIST_ENTRY ListEntry; //0x0
struct _RTL_BALANCED_NODE TreeNode; //0x0
struct
{
union
{
struct _SINGLE_LIST_ENTRY NextSlistPfn; //0x0
VOID* Next; //0x0
ULONGLONG Flink:40; //0x0
ULONGLONG NodeFlinkLow:24; //0x0
struct _MI_ACTIVE_PFN Active; //0x0
} u1; //0x0
union
{
struct _MMPTE* PteAddress; //0x8
ULONGLONG PteLong; //0x8
};
struct _MMPTE OriginalPte; //0x10
};
};
struct _MIPFNBLINK u2; //0x18
union
{
struct
{
USHORT ReferenceCount; //0x20
struct _MMPFNENTRY1 e1; //0x22
};
struct
{
struct _MMPFNENTRY3 e3; //0x23
struct
{
USHORT ReferenceCount; //0x20
} e2; //0x20
};
struct
{
ULONG EntireField; //0x20
} e4; //0x20
} u3; //0x20
struct _MI_PFN_ULONG5 u5; //0x24
union
{
ULONGLONG PteFrame:40; //0x28
ULONGLONG ResidentPage:1; //0x28
ULONGLONG Unused1:1; //0x28
ULONGLONG Unused2:1; //0x28
ULONGLONG Partition:10; //0x28
ULONGLONG FileOnly:1; //0x28
ULONGLONG PfnExists:1; //0x28
ULONGLONG NodeFlinkHigh:5; //0x28
ULONGLONG PageIdentity:3; //0x28
ULONGLONG PrototypePte:1; //0x28
ULONGLONG EntireField; //0x28
} u4; //0x28
};
第一个_MMPFN
成员管理0-0xFFF的物理内存,第二个成员管理0x1000-0x1FFF的物理内存…以此类推,即0x30+PFN。
MMPFN与物理页的对应关系:
- 通过当前_MMPFN结构体找到对应的物理页:
物理页 = 当前_MMPFN索引值*0x1000
- 通过当前物理页找到对应的MMPFN结构体:
_MMPFN = *MmPfnDatabase + 0x30 * (物理ff>页/0x1000)
_MMPFN.PteAddress
在私有内存的情况下,_MMPFN.PteAddress==页表的PteAddress
。对于私有内存,_MMPFN
的PteAddress成员保存的是进程PteBase区域的PteAddress。
对于共享内存,_MMPFN.PteAddress
是原型PTE,也就是Section(SubSection).ControlArea.Segment.PrototypePte
。
windbg出了点儿问题,图就先不发了,后面可能会补上。
什么是原型PTE呢?
原型PTE是共享内存所特有的,对于共享内存,可能会发生如下状况:
在处理可共享内存时,Windows 会为共享内存的每一页创建一种全局 PTE——称为原型
PTE。此原型始终表示共享页面的物理内存的真实状态。如果标记为Valid,则此原型 PTE 可以像在任何其他情况下一样充当硬件PTE。如果标记为Not Valid,原型将向页面错误处理程序指示内存需要从磁盘调回。当给定内存页存在原型 PTE 时,该页的页帧数据库条目将始终指向原型 PTE。
共享内存可能处于转换状态,也可能处于被换页出去硬件异常状态(意味着共享内存这个页被换到磁盘上),如果是转换状态,页面异常的处理只需要改变硬件PTE的V位即可,如果是硬件异常,则需要重新映射。
如下图,硬件PTE无效,但是原型PTE有效,因此Page Fault Handler处理时只需要把无效的硬件PTE修改V位即可。不必重新映射。
而对于私有内存,不需要原型PTE这个概念。因为私有内存不会转换,修建工作集。
windbg出了点儿问题,图就先不发了,后面可能会补上。
_MMPFN.OriginalPte
_MMPFN.OriginalPte
是一个叫做“原始页表项”的内存管理数据结构中的一个成员变量。它用于存储Windows
API查询时的页面属性,以便在操作系统中正确管理页面。它由硬件管理器维护,当硬件发生变化时,硬件管理器会自动更新该值。
简而言之,使用Windows API查询时,返回的是_MMPFN.OriginalPte
。
MmGetVirtualForPhysical
可以用MmGetVirtualForPhysical
从物理地址转换到虚拟地址。其实MmGetVirtualForPhysical
也利用了页帧数据库来获取。
MmGetVirtualForPhysical proc near
mov rax, rcx
shr rax, 0Ch ; 右移12位, 索引
lea rdx, [rax+rax*2] ; rax*3
add rdx, rdx ; rdx=rax*6
mov rax, 0FFFFDE0000000008h ; MmPfnDataBase+8 PteAddress
mov rax, [rax+rdx*8] ; (MmPfnDataBase+8)+索引*0x30 , 算出PteAddress
shl rax, 19h
mov rdx, 0FFFFF68000000000h ; rdx = PteBase
shl rdx, 19h
and ecx, 0FFFh ; ecx = 页内偏移
sub rax, rdx
sar rax, 10h
add rax, rcx
retn
MmGetVirtualForPhysical endp
对于共享物理内存,PteAddress到底是谁的进程的?不清楚,因此共享物理内存无法计算。
对于共享内存,这个地方是原型pte的地址。
MmGetVirtualForPhysical
返回值是基于特定CR3的虚拟地址。如果没有附加进程,并且当前进程没有这块物理地址,所返回的虚拟地址也是没有意义的。