Windows内存管理
内存有两种,一种是由内存条构成的所谓的物理内存,这种内存读取速度快;
还有一种是虚拟内存,由硬盘构成,也就是把硬盘的一部分容量拿来当作内存用,但是速度没有内存条那么快。
两者的关系:
每个进程都有4GB的虚拟内存,但不是所有的虚拟内存都有对应的物理内存,它其实是在物理内存和磁盘之间进行倒换的。
虚拟内存在用的时候,会从磁盘倒入物理内存,不用的时候就还是存在磁盘上。因此,同一个物理内存在不同的时间可以被用于不同进程的不同虚拟内存。
基于页面映射的虚拟内存机制:
在硬件上:由CPU芯片内部或外部的存储管理单元MMU支持。
在软件上:由操作系统内核中的内存管理模块实现。
采用页式内存管理时,程序中所使用的内存地址,即CPU中的运算单元ALU所发出的都是虚拟地址。虚拟地址是不能直接用来访问物理内存的,需要转换成物理地址才能访问。
名词解释:
PDE:Page Directory Entry,页目录表项。
PTE:Page Table Entry,页表项。
PAE:Physical Address Extension,物理地址扩展。
PFT:Page Frame Table,页帧表。
PFN:Page Frame Number,页帧号。 PFN = 物理地址/4KB
虚拟地址转换为物理地址(x86)
虚拟地址转换为物理地址:由MMU(也就是存储管理单元)将虚拟地址映射到某个物理页面上,从而转换成物理地址。
每个进程都有一个页面映射表,首先是根据CR3寄存器找到页面映射表。
页面映射表分为两层,第一层是页目录表,它实际上是一个指针数组,有1024个表项,每一项PDE都指向一个页表,页表也有1024项,每一项PTE指向对应的物理页面。
x86下,虚拟地址有32位,
31-22(10位):页目录表索引
21-12(10位):页表索引
11-0(12位):页内偏移
根据它的前20位能定位到相应的物理页面首地址,然后再加上页面偏移就得到物理地址了。
PAE 模式
PAE,Physical Address Extension,物理地址扩展。
它是通过设置CR4寄存器的第5位(PAE标志)来开启的,开启之后,CR3寄存器就指向了页目录指针表。
开启PAE:
bcdedit /set pae forceenable
关闭PAE:
bcdedit /set pae forcedisable
可以通过bcdedit命令来验证是否成功,开启成功显示:
开启PAE之后,页表和页目录中的表项都从32位扩为64位(8字节),而页表和页目录的总大小不变(4KB),所以页表和页目录都变成了512个表项。
这样就需要有4个页目录表,因此又多加了一个页目录指针表(32字节),里边有4项,每一项指向一个页目录表。
不会算的看这里:
原来是1个页目录表,1024个页表,一共有 1024 * 1024个PTE
现在每个页表有512个PTE,一共1024 * 1024个PTE,所以需要2048个页表
1个页目录表指向512个页表,所以现在需要4个页目录表。
x86下开启PAE模式,虚拟地址有32位,
31-30(2位):页目录指针表的索引
29-21(9位):页目录表索引
20-12(9位):页表索引
11-0(12位):页内偏移
Windows内存空间布局
x86下,每个进程有4GB的虚拟内存,用户空间和内核空间各占2GB。
用户空间的2GB:
前64KB(0x00000000-0x0000FFFF)是空指针赋值分区,
后64KB(0x7FFF0000-0x7FFFFFFF)是禁入分区,
中间(0x00010000-0x7FFEFFFF)才是真正的用户空间。
有一些特殊的地址:
-
0xFFDF0000 是 SharedUserData 的地址,SharedUserData在内核空间,但是又划出来让用户空间的程序访问,目的是让用户空间的程序访问内核中的一些数据。
SharedUserData是由所有进程所共享的,指向KUSER_SHARED_DATA结构,用户程序通过指针SharedUserData来读取这个结构中各个成分的内容。 -
进程的PEB总是安排在0x7FFDF000的地方,占4KB;TEB在PEB的上方(0x7FFDE000),进程中有几个线程就有几个TEB,每个TEB占一个4KB的页面。
-
进程EPROCESS中有个成员VadRoot,它指向该进程的用户空间。
#define MM_LOWEST_USER_ADDRESS 0x00010000
#define MM_HIGHEST_USER_ADDRESS MmHighestUserAddress
#define MM_USER_PROBE_ADDRESS MmUserProbeAddress
#define KI_USER_SHARED_DATA 0xFFDF0000
#define SharedUserData ((KUSER_SHARED_DATA* const) KI_USER_SHARED_DATA)
3: kd> dd MmHighestUserAddress
849ac848 7ffeffff
3: kd> dd MmUserProbeAddress
849ac850 7fff0000
3: kd> dd MmSystemRangeStart
849ac84c 80000000
3: kd> dd SharedUserData
7ffe0000 00000000 0f99a027 34ed95a7 00000000
7ffe0010 00000000 8d296be5 01d60a39 01d60a39
3: kd> dd FFDF0000
ffdf0000 00000000 0f99a027 34ed95a7 00000000
ffdf0010 00000000 8d296be5 01d60a39 01d60a39
内核对物理内存的管理(MmPfnDatabase)
Windows通过几个全局变量来对物理内存进行管理:
MmPfnDatabase,页帧数据库(也叫FPN数据库),是MMPFN结构体类型的数组,它的索引是物理地址页帧号。
获得MmPfnDatabase的地址:84e00000
1: kd> dd MmPfnDatabase
83f7b700 84e00000 00000000 00000001 0005ffff
查看结构体发现,结构体大小为0x1C
1: kd> dt _MMPFN
nt!_MMPFN
+0x000 u1 : <unnamed-tag>
+0x004 u2 : <unnamed-tag>
+0x008 PteAddress : Ptr32 _MMPTE
+0x008 VolatilePteAddress : Ptr32 Void
+0x008 Lock : Int4B
+0x008 PteLong : Uint4B
+0x00c u3 : <unnamed-tag>
+0x010 OriginalPte : _MMPTE
+0x010 AweReferenceCount : Int4B
+0x018 u4 : <unnamed-tag>
第一个数组项,也就是PFN为1的物理页面:
1: kd> dd 84e00000 + 1c
84e0001c 00000000 00000001 c07fe838 00460001
84e0002c 00000000 00000000 0000018a
验证一下,可以发现各数据成员的值都是能对的上的:
1: kd> !pfn 1
PFN 00000001 at address 84E0001C
flink 00000000 blink / share count 00000001 pteaddress C07FE838
reference count 0001 Cached color 0 Priority 0
restore pte 00000000 containing page 00018A Active
第二个数组项:
1: kd> dd 84e00000 + 1c*2
84e00038 00000000 00000001 c07fe920 00460001
84e00048 00000000 00000000 0000018a
同样再验证一下:
1: kd> !pfn 2
PFN 00000002 at address 84E00038
flink 00000000 blink / share count 00000001 pteaddress C07FE920
reference count 0001 Cached color 0 Priority 0
restore pte 00000000 containing page 00018A Active
......
以此类推,第n个数组项的地址是:84e00000 + 0x1c*n
MmNumberOfPhysicalPages,物理页面的个数,也是MmPfnDatabase数组的长度。
1: kd> dd MmNumberOfPhysicalPages
83f7b710 0005ff7e 7ffeffff 80000000 7fff0000
0x5ff7e * 4KB = 1535MB
看一下MmPfnDatabase的最后一个数组项
1: kd> dd 84e00000 + 1c*0005ff7e
8587f1c8 00000000 00000001 c042cbf8 00560001
8587f1d8 00000000 00000000 00005a05
1: kd> !pfn 0005ff7e
PFN 0005FF7E at address 8587F1C8
flink 00000000 blink / share count 00000001 pteaddress C042CBF8
reference count 0001 Cached color 0 Priority 0
restore pte 00000000 containing page 005A05 Active M
Modified
MmPageLocationList,链表头数组,物理页有六种状态,相同状态的物理页用一个链表维护。关于链表的详细内容后续会补充一下。
1: kd> dd MmPageLocationList
83e5b894 83f7c000 83f7c040 83f7c080 83f7c0c0
83e5b8a4 83f3b94c 83f3b960 00000000 00000000
typedef struct _MMPFNLIST
{
PFN_NUMBER Total;
MMLISTS ListName;
PFN_NUMBER Flink;
PFN_NUMBER Blink;
} MMPFNLIST, *PMMPFNLIST;
PMMPFNLIST MmPageLocationList[] =
{
&MmZeroedPageListHead, // 零化链表,可以使用并已经清零的内存
&MmFreePageListHead, // 空闲链表,被释放的内存,会定期检测是否有数据,如果有就清空并挂到MmZeroedPageListHead中
&MmStandbyPageListHead, // 备用链表
&MmModifiedPageListHead, // 修改过的内存
&MmModifiedNoWritePageListHead, // 修改过但是没有写出的内存,即不会写到磁盘上
&MmBadPageListHead, // 损坏的内存
NULL,
NULL
};
获得MmPfnDatabase的地址
可以通过KeCapturePersistentThreadState函数获得,
在windbg中查看x86下函数的汇编,我们可以发现其中有很多重要信息,
除了MmPfnDatabase,还可以获得PsLoadedModuleList 、PsActiveProcessHead等地址。
PsLoadedModuleList 是维护系统加载的所有内核模块的双向链表头;
PsActiveProcessHead 在之前的文章中有简单介绍过,是维护系统中所有进程EPROCESS的双向链表头。
0: kd> u KeCapturePersistentThreadState l 50
nt!KeCapturePersistentThreadState:
83edaad3 8bff mov edi,edi
83edaad5 55 push ebp
......
83edab3d 894310 mov dword ptr [ebx+10h],eax
83edab40 a100b7f783 mov eax,dword ptr [nt!MmPfnDatabase (83f7b700)]
83edab45 894314 mov dword ptr [ebx+14h],eax
83edab48 c7431850b8f583 mov dword ptr [ebx+18h],offset nt!PsLoadedModuleList (83f5b850)
83edab4f c7431c183ff583 mov dword ptr [ebx+1Ch],offset nt!PsActiveProcessHead (83f53f18)
83edab56 c743204c010000 mov dword ptr [ebx+20h],14Ch
83edab5d a16cb9f783 mov eax,dword ptr [nt!KeNumberProcessors (83f7b96c)]
83edab62 894324 mov dword ptr [ebx+24h],eax
......
具体获得方法:
首先获得KeCapturePersistentThreadState的地址FuncPtr,然后遍历其内存
for (PBYTE i = FuncPtr; i < (PBYTE)FuncPtr + 0xFF; i++)
{
// 89 43 10 mov [ebx+10h],eax
// a1 xxxxxxxx mov eax, ds:MmPfnDataBase
if (MmIsAddressValid(PAGE_ALIGN(i)) &&
*(i + 0) == 0x89 &&
*(i + 1) == 0x43 &&
*(i + 2) == 0x10 &&
*(i + 3) == 0xa1)
{
RtlCopyMemory(&MmPfnDatabaseAddr, i + 4, sizeof(ULONG32));
break;
}
}