avatar

Catalog
物理内存的管理

前言

前面3篇文章,以线性地址的角度认识了地址空间的管理与分配;本篇则以物理内存的角度来看地址的管理情况,在介绍段、页时,学习了不少关于页的知识,也有必要作为参考:

认识物理内存

最大物理内存

  • 10-10-12分页:最多可识别物理内存为4GB
  • 2-9-9-12分页:最多可识别物理内存为64GB

操作系统限制

为什么在XP中,明明是2-9-9-12分页,却仍然无法超越4GB呢?

在内核函数中,有一个函数MmAddPhysicalMemoryEx,它调用了一个ExVerifySuite函数,这个ExVerifySuite限制了操作系统无法识别超过4GB,这里不展开,感兴趣可以自行分析它。(网上有补丁可以突破4GB的限制,原理也就是对这个函数做手脚)

实际物理内存

如何来看实际的物理内存的大小。它可以通过任务管理器来查看,如图:

可以看到,这里的值是523696(10进制),单位是KB,这是操作系统实际管理出来的物理内存。

还有另一种方式,通过全局变量MmNumberOfPhysicalPages,如下图所示:

全局变量MmNumberOfPhysicalPages记录了物理页的总数,单位是4KB,通过运算,可以看到,刚好就是任务管理器中查询到的值。

空闲页的管理

学习物理内存的管理,分为两个角度,一是空闲页的管理,另一个是活动状态页的管理。这部分主要介绍空闲页的管理机制。

全局数组

在操作系统中,有一个全局数组,数组中的成员记录了所有的物理页的信息(包括空闲页与正在使用的页),描述如下:

Code
1
2
数组指针:_MMPFN* MmPfnDatabase
数组长度:MmNumberOfPhysicalPages

如图,框住的地址指向一个MMPFN结构,MMPFN是一个用来描述物理页的信息的结构,其大小为0x1C(或者0x18,因系统环境不同可能会变化),不过MMPFN结构本身并没有记录自己描述的是哪个物理页的信息,那么如何找到某个MMPFN描述的是哪个物理页呢?

微软用了一种很巧妙的办法解决了这个问题。前面提到MmNumberOfPhysicalPages存的物理页的总数,而每个MMPFN用来描述一个物理页,因此就让每个物理页与全局数组中每个MMPFN按照顺序一一对应起来。以图中的MMPFN为例:

  • 0x80c8600+0x1c*0:指向全局数组中第一个MMPFN,描述所有物理页中第一个物理页(0x0000~0x0fff)
  • 0x80c8600+0x1c*1:指向全局数组中第二个MMPFN,描述所有物理页中第二个物理页(0x1000~0x1fff)
  • 0x80c8600+0x1c*2:指向全局数组中第三个MMPFN,描述所有物理页中第三个物理页(0x2000~0x2fff)
  • ……以此类推

数组成员

前面一直提到全局数组中的成员——MMPFN结构,它被用来描述一个物理页的信息,现在来看看这个结构各个字段的含义:

c
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
58
kd> dt _MMPFN
nt!_MMPFN
+0x000 u1 : __unnamed
+0x004 PteAddress : Ptr32 _MMPTE
+0x008 u2 : __unnamed
+0x00c u3 : __unnamed
+0x010 OriginalPte : _MMPTE
+0x018 u4 : __unnamed

typedef struct _MMPFN
{
union
{
PFN_NUMBER Flink;
ULONG WsIndex; //该页面在进程工作集链表中的索引
PKEVENT Event;
NTSTATUS ReadStatus;
SINGLE_LIST_ENTRY NextStackPfn;
SWAPENTRY SwapEntry;
} u1;
PMMPTE PteAddress; //执行此页面的PTE的虚拟地址
union
{
PFN_NUMBER Blink;
ULONG_PTR ShareCount; //指向该页面的PTE数量
} u2;
union
{
struct
{
USHORT ReferenceCount; //代表这个页面必须要保留在内存中的引用计数
MMPFNENTRY e1;
};
struct
{
USHORT ReferenceCount;
USHORT ShortFlags;
} e2;
} u3;
union
{
MMPTE OriginalPte; //包含了指向此页面的PTE的原始内容
LONG AweReferenceCount;
PMM_RMAP_ENTRY RmapListHead;
};
union
{
ULONG_PTR EntireFrame;
struct
{
ULONG_PTR PteFrame:25;
ULONG_PTR InPageError:1;
ULONG_PTR VerifierAllocation:1;
ULONG_PTR AweAllocation:1;
ULONG_PTR Priority:3;
ULONG_PTR MustBeCached:1;
};
} u4; //指向该页面的PTE所在的页表页面的物理页帧编号,以及一些标志位

由于MMPFN结构包含了4个共用体,每个共用体又包含多个成员,以至于在不同状态下,该结构成员的含义也有所不同。接下来就来看看物理页有哪些状态。

物理页状态(空闲)

_MMPFN.u3.e1处的结构是一个位段,其结构如下:

其中PageLocation这个成员标识了当前物理页的状态,每一种状态对应一个链表,该链表串着所有当前状态的物理页,具体参考下表:

取值 对应的全局链表 状态类型
0 MmZeroedPageListHead 零化
1 MmFreePageListHead 空闲
2 MmStandbyPageListHead 备用
3 MmModifiedPageListHead 修改
4 MmModifiedNoWritePageListHead 已修改但不写出
5 MmBadPageListHead 损坏

下面来解释每种状态及其对应链表的含义:

  • 坏状态(bad):页面产生硬件错误,系统不再使用这样的页面。
  • 零化状态(zero):页面是空闲的,不属于任何一个工作集,零化链表串着的每个物理页的内容已经被全部清零
  • 空闲状态(free):页面是空闲的,不属于任何一个工作集,但是物理页包含了不确定的数据,系统空闲时有专门的线程从这个队列摘取物理页,加以清零后再挂入零化链表中
  • 备用状态(standby):这种页面原来属于某个进程或系统工作集,但现在已经从工作集中移除。这种页面包含的数据对于原来的工作集仍然是有效的,原来工作集中的PTE仍然指向该页面,但是已被标记成正在转移的无效PTE。是导致缺页异常的主要因素之一。
  • 已修改状态(modified):类似于备用状态,已经从原来的工作集中移除,但是页面包含的内容已经被修改过。原来工作集中的PTE仍然指向物理页面,但是已被标记成正在转移的无效PTE。如果系统要把这种页面回收作它用,则必须将其中的内容写到磁盘上。
  • 已修改但不写出(modified no-write):类似于已修改状态,但区别在于,内存管理器不会将它的内容写到磁盘上。

查询链表实验

我们现在知道了,所有物理页,都在一个全局数组里,每个物理页对应一个MMPFC物理页描述结构,空闲的物理页有6种状态,每种状态对应一个全局链表,那么如何把这些知识点串起来呢,下面以一个实验来巩固以上知识,以零化链表为例,查询该链表上所有物理页:

  1. 首先根据全局变量,找到描述物理页信息的MMPFN全局数组

    框住的就是所有物理页中第一个物理页对应的MMPFN。通过该地址+0x1c*下标,可以找到任一物理页。

  2. 然后通过全局变量MmZeroedPageListHead查找零化链表

    这个零化链表怎么看呢,这要参考下面这个关于各个状态的链表结构(注意,这里是各个状态对应的链表结构,不是各个状态对应的MMPFN结构,来源自WRK)

    c
    1
    2
    3
    4
    5
    6
    7
    MMPFNLIST MmZeroedPageListHead = {0, ZeroedPageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmFreePageListHead = {0, FreePageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmStandbyPageListHead = {0, StandbyPageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmModifiedPageListHead = {0, ModifiedPageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmModifiedNoWritePageListHead = {0, ModifiedNoWritePageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmBadPageListHead = {0, BadPageList, LIST_HEAD, LIST_HEAD};
    MMPFNLIST MmRomPageListHead = {0, StandbyPageList, LIST_HEAD, LIST_HEAD};

    这个是WRK中定义的各个链表的结构,我们这里关注零化链表,可以注意到,它的第三个成员和第四个成员都是LIST_HEAD,区别在于,一个是从前往后数,另一个是从后往前数

  3. 以第三个成员为例,将其代入,观察下图:

    这个图是理解各个状态链表如何将物理页串起来的核心,看懂了就都明白了。它很复杂,我第一遍看的时候也是半天没搞懂,这里把关键的要素都框住了,看上去会方便很多,下面来逐个进行讲解:

    • 首先来看红色方框,第一个红框是全局数组的首地址(指向第一个物理页对应的MMPFN结构),第二个红框是零化链表中第一个零化物理页对应的MMPFN结构的下标,这个如何理解呢,需要结合橙色方框来看。

    • 看到第一个橙色方框,这里用全局数组首地址+第一个零化物理页下标*MMPFC结构的大小。得到的就是第一个零化物理页对应的MMPFN结构。现在就可以理解了,零化链表中LIST_HEAD是什么含义,它就是当前零化物理页的MMPFN结构在全局数组中的下标。该下标*4KB,得到是就是该零化物理页对应的地址了。

    • 前两步,都好理解,到了蓝色方框,这里使用了第二个橙色方框的值,作为下标进行运算。要解释这一步,需要重新审视MMPFN结构。

      观察MMPFN,前面说过由于MMPFN使用了大量的共用体,导致它在不同状态下,所代表的含义也是不同的,而此时,当MMPFN指向一个零化物理页时,u1与u2共用体采用的值分别是Flink与Blink,分别指向前一个零化物理页在全局数组中的下标与后一个零化物理页在全局数组中的下标。按照前一张图中的规律,就可以得出下图:

      这张图就可以很好的描述全局数组,零化链表与MMPFN的对应关系。

小结

至此,以一个零化链表的查询例子,了解了全局数组,零化链表与其对应的MMPFN之间的关系,但是仅限用于零化状态的物理页,其余状态的物理页与MMPFN的对应关系如下:

当然这里面也包括了活动状态页对应的MMPFN。

活动页的管理

这部分海哥讲到的不多,可以参考《Windows内核原理与实现》p286~293,简单说一下吧。

这张图是Windows10系统任务管理器中查看到的,在性能一栏,打开资源监视器,可以看到有工作集这么一栏,这里记录的是进程实际使用的物理页总大小

任意打开一个进程,定位到进程结构体0x1f8的位置,有一个Vm成员,是MMSUPPORT结构

MMSUPPORT结构体中,VmWorkingSetList记录了与当前进程相关的工作集信息。VmWorkingSetList对应的结构体MMWSL内部还有一个Wsle成员,用来描述一个有效页面。这部分具体参考《Windows内核原理与实现》p286~287。

参考资料

参考书籍:

  • 《Windows内核原理与实现》p265~293 —— 潘爱民

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/03/%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E7%9A%84%E7%AE%A1%E7%90%86/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶