JIURL玩玩Win2k内存篇 Page Frame Number Database
作者: JIURL
日期: 2003-7-30
引子
当系统需要提供一个物理页给应用程序,来满足应用程序要求的时候,将遇到一个问题,系统如何知道哪些物理页已经被使用,哪些物理页没有被使用。Page Frame Number (PFN) DataBase (页帧号数据库)和它的相关结构用来解决这个问题。
Working Set 简介
一个进程的 Working Set 是进程当前所使用在物理内存中的页的集合。程序使用这些页不会引发 page fault。
页帧号数据库 ( PFN DataBase ) 概述
物理内存被分页,对于32位的CPU来说,每个物理页大小是4K。对于每一个物理页,系统使用一个24字节长的结构来保存它的相关信息,比如该物理页是否已经被使用。为了便于描述,我们把这个结构叫做PfnDataBaseEntry ,页帧号数据库项。页帧号数据库就是一个 PfnDataBaseEntry 数组,这个数组的每一项对应一个物理页。比如 PfnDataBase 数组第0项,对应物理页0,也就是页帧号为0的物理页。第1项,对应物理1,也就是页帧号为1的物理页。系统把PfnDataBase的首地址保存在全局变量 MmPfnDatabase 中。现在我们来分析一下 物理页的页帧号(PFN),物理页的物理地址范围,物理页的页帧号数据库项之间的关系。对于物理页i,它的页帧号是i。由物理地址从i*0x1000到i*0x1000+0xFFF,这4KB物理内存单元组成。对应的页帧号数据库项为第i项,虚拟地址为 *MmPfnDatabase+i*0x18。比如在当前我所在的Win2k中,对于物理页3,它的页帧号是3,由物理地址0x3000-0x3FFF这4k的物理内存单元组成,当前我的 MmPfnDatabase 中的值为 0x81456000,即PfnDataBase 的首地址为 0x81456000,所以对应的 PfnDataBaseEntry 虚拟地址为 0x81456000 + 3*0x18 = 0x81456048。
PfnDataBaseEntry 的具体内容如下
struct PfnDataBaseEntry (大小24个字节,即0x18个字节)
/*00*/ uint32 flink
/*04*/ uint32 pteaddress
/*08*/ uint32 blink / share count
/*0C*/ byte flags
/*0D*/ byte page state
/*0E*/ uint16 reference count
/*10*/ uint32 restore pte
/*14*/ uint32 containing page
/*00*/ uint32 flink
链上前一个 PfnDataBaseEntry 的页帧号,如果为 0xFFFFFFFF 表示没有前一个 PfnDataBaseEntry。
/*08*/ uint32 blink / share count
对于使用链表链在一起的物理页,表示后一个 PfnDataBaseEntry 的页帧号,如果为 0xFFFFFFFF 表示没有后一个 PfnDataBaseEntry。对于没有使用链表链在一起的物理页,表示共享计数。
/*0C*/ byte flags
一些标志信息。
/*0D*/ byte page state
指出物理页的状态,就是从这里判断一个物理页是否已经被使用。物理页共有8种不同的状态。
Active(Valid):
这个物理页在某个进程的 Working Set 中,该进程的一个有效的页表项中的高20bit正是这个物理页的PFN。
Transition:
系统正在从一个文件将内容读入该物理页,或者正在向一个文件写出该物理页内容。
Standby:
这个物理页曾经在某个进程的 Working Set 中,并且物理页中的内容在被该进程使用时没有被改变过。但是现在已经被移出该进程的 Working Set,不过物理页中的内容仍是在该进程 Working Set 中时的内容。该进程相应的PTE中的高20bit仍然是这个物理页的页帧号,只是该PTE被标为 invalid 和 transition。当该进程需要再次访问这一页的内容时,只需要重新设定该PTE的标志,并把该PTE变为有效。把该物理页从 Standby 状态变为Active(Valid) 状态就可以了。
Modified:
这个物理页曾经在某个进程的 Working Set 中,并且物理页中的内容在被该进程使用时被改变过。但是现在已经被移出了该进程的 Working Set,不过物理页中的内容仍是被移出时的内容。该进程相应的PTE中的高20bit仍然是这个物理页的页帧号,只是该PTE被标为 invalid 和 transition。当该进程需要再次访问这一页的内容时,只需要重新设定该PTE的标志,并把该PTE变为有效。把该物理页从 Modified 状态变为Active(Valid) 状态就可以了。在该物理页被系统作为其他用途使用之前,该物理页中的内容需要被写入硬盘中的交换文件的相应页。
Modified no-write:
内存管理器的 Modified Page Writer 将不会把这种物理页写入硬盘,其他和 Modified 物理页一样。比如,NTFS使用这个状态来保证在log之前,该页不会被写入硬盘。
Free:
该物理页中的内容不再被需要,比如一个进程结束,这个进程所使用的一些物理页就会变为 Free 状态。(由于安全的原因,这些页需要被用零初始化,才能给一个用户进程使用)
Zeroed:
该页free并且已经被用零初始化。
Bad:
该页存在硬件错误,不能被使用。
其中 Zeroed,Free,Standby,Modified,ModifiedNoWrite,Bad 这6种状态的 PfnDataBaseEntry 使用PfnDataBaseEntry 的 flink,blink 链在该状态的 PfnDataBaseEntry链上。系统通过全局变量MmZeroedPageListHead,MmFreePageListHead,MmStandbyPageListHead,MmModifiedPageListHead,MmModifiedNoWritePageListHead,MmBadPageListHead 可以找到这6个链。这样当需要使用物理页的时候系统就可以很快的从相应的链中取出物理页。Active(Valid) 和 Transition 状态的 PfnDataBaseEntry 没有被任何链表链在一起,但是通过各个进程的PTE我们就可以找到,PTE的高20bit就是一个页帧号。
页帧号数据库 ( PFN DataBase ) 分析
全局变量 MmZeroedPageListHead,MmFreePageListHead,MmStandbyPageListHead,MmModifiedPageListHead,MmModifiedNoWritePageListHead,MmBadPageListHead 是6个保存有链表信息的结构,为了便于描述,我们把这个结构叫 PageListHead ,这个结构大小为16个字节,定义如下
struct PageListHead (大小16个字节,即0x10个字节)
/*00*/ uint32 NumberOfPagesInList
/*04*/ uint32 TypeOfList
/*08*/ uint32 FirstPage
/*0C*/ uint32 LastPage
/*00*/ uint32 NumberOfPagesInList
这个链表的项数
/*04*/ uint32 TypeOfList
这个链表上 PfnDataBaseEntry 的类型(Zeroed,Free,Standby,Modified,ModifiedNoWrite,Bad 这6种中的一种)
/*08*/ uint32 FirstPage
flink的第一项,是一个页帧号。
/*0C*/ uint32 LastPage
blink的第一项,是一个页帧号。
8种 page state 对应的值 ( PfnDataBaseEntry 中的 page state 和 PageListHead 中的 TypeOfList)
Zeroed 0x00
Free 0x01
Standby 0x02
Modified 0x03
Modified no-write 0x04
Bad 0x05
Active (Valid) 0x06
Transition 0x07
实际上还存在着一个数组 MmPageLocationList 。这个数组共6项,每项4个字节。数组第0项的内容为全局变量 MmZeroedPageListHead 中的值,注意 Zeroed 类型的值为0。数组第1项的内容为全局变量MmFreePageListHead 中的值,注意 Free 类型的值为1。以此类推。存在这个数组的原因是一些函数中可以使用物理页的类型作为索引,在这个数组中找到该类型物理页链的 PageListHead 结构的地址。
对于 Win2k Build 2195 来说,使用 kd 来获得全局变量的地址
比如
kd> ? MmPfnDatabase
? MmPfnDatabase
Evaluate expression: -2142854772 = 8046a18c
用这种方法得到
6个 PageListHead 的地址为
MmZeroedPageListHead 0x80470a00
MmFreePageListHead 0x80470a10
MmStandbyPageListHead 0x80470a20
MmModifiedPageListHead 0x80470a30
MmModifiedNoWritePageListHead 0x80470a40
MmBadPageListHead 0x80470a50
MmPageLocationList 数组的地址为
MmPageLocationList 0x80470a60
可以发现他们是紧紧的挨在一起的。
下面我就来看一个具体的例子,以下内容通过 SoftICE 获得。
:dd 80470a00 l 80
0010:80470A00 0000090E 00000000 00007F0A 0000009E ...............
0010:80470A10 00000004 00000001 00002987 00006ACA .........)...j..
0010:80470A20 0000242B 00000002 000042A6 00003ADB +$.......B...:..
0010:80470A30 000001CB 00000003 FFFFFFFF FFFFFFFF ................
0010:80470A40 00000000 00000004 FFFFFFFF FFFFFFFF ................
0010:80470A50 00000000 00000005 FFFFFFFF FFFFFFFF ................
0010:80470A60 80470A00 80470A10 80470A20 80470A30 ..G...G. .G.0.G.
0010:80470A70 80470A40 80470A50 00000000 00000000 @.G.P.G.........
// 6个链表的 PageListHead 结构,可以看到每个 PageListHead 结构偏移 +4 处的 4个字节内容就是该链表// 上物理页的状态值
// MmPageLocationList 数组共有6项,每项4个字节,相应物理页状态值作为索引的项的内容,就是相应的
// PageListHead 结构的地址
// 下面我们来遍历 FreePage List ,可以看到这个链上一共有4项,我们从FirstPage遍历这个链表。
// 从 PageListHead 中,我们可以看到 FirstPage 值为 00002987
// PfnDataBase的首地址保存在全局变量 MmPfnDatabase 中
// 前面已经看到 Win2k Build 2195 的 MmPfnDatabase 地址为8046a18c,
// 对于当前我的系统中,地址8046a18c处的值为 81456000 ,
// 即当前 PfnDataBase的首地址为 81456000
// 第2987项 PfnDataBaseEntry 的虚拟地址为 81456000+18*2987 ,该结构大小为 0x18
:dd 81456000+18*2987 l 18
0010:814944A8 00006B68 FFFFFFFF FFFFFFFF 00000101 hk..............
0010:814944B8 FFFFFFFF 0000064C 00002EB1 FFFFFFFF ....L...........
// 可以看到该项的后一项为 FFFFFFFF ,说明了这一项的确是 FirstPage
// 第6B68项
:dd 81456000+18*6b68 l 18
0010:814F71C0 00000169 FFFFFFFF 00002987 00000101 i........)......
0010:814F71D0 FFFFFFFF 0000064C 0000268D C038CA6C ....L....&..l.8.
// 第169项
:dd 81456000+18*169 l 18
0010:814581D8 00006ACA FFFFFFFF 00006B68 00000101 .j......hk......
0010:814581E8 FFFFFFFF 0000064C 000042E6 E17BE324 ....L....B..$.{.
// 第6ACA项
:dd 81456000+18*6aca l 18
0010:814F62F0 FFFFFFFF FFFFFFFF 00000169 00000101 ........i.......
0010:814F6300 FFFFFFFF 0000064C 00004EC4 E35E0BE0 ....L....N....^.
// 它的flink项的值为FFFFFFFF,表明flink已经结束了。也表明它是LastPage,从 PageListHead 结构中// 我们也看到 LastPage 的确是 00006ACA
注意我们遍历的是 FreePage List ,我们检查每项的 TypeOfList 字段(偏移+d处的一个字节),值都为01。的确是Free类型。
从6个 PageListHead 结构之后紧跟着 MmPageLocationList 数组,以及 MmPageLocationList 数组只有6项,可以看出系统中只有这6种状态类型的物理页存在着链表。另两种状态类型的物理页并没有被链表链起来。对于这6个 PageListHead 结构,进行一定时间观察后,发现 MmModifiedNoWritePageListHead,MmBadPageListHead 的两个链总是空的。我的内存中没有硬件损坏的单元,所以 MmBadPageListHead 应该是空的。MmModifiedNoWritePageListHead 链上我也没有看到过有链表项(我硬盘中没有ntfs分区)。我能看到经常变化的是 Zeroed,Free,Standby,Modified 这4种链。需要说明的是,这4种链的变化是非常频繁的。一定要注意这一点,如果你这一秒你读出了 Free 链上的一项,那么下一秒它很有可能已经不在 Free 链上了,系统的一些线程可能已经对该页做了某些操作,并把该页对应的 PfnDataBaseEntry 做相应修改。不过对于 SoftICE 不存在这个问题。可以使用 SoftICE 的命令 BPMD 来对这种变化,和引起这个变化的函数进行观察。 BPMD 就是在指定地址的 DWORD 大小的内存上下断点,任何访问(读写)这个 DWORD 的指令,将被 SoftICE 断到。加上参数W,就只中断对这个DWORD写的指令。比如使用 BPMD 80470a00 W 来观察 Zeroed Page List 的变化,因为 80470a00 处是MmZeroedPageListHead结构的 entry count (这个链的链表项项数),链如果变化,必须要相应的改变这个值。使用 dex 0 80470a00 来使数据窗口0 始终观察 80470a00 处的内存,或者 data 1 开一个新的数据窗口,然后 dex 1 80470a00 用数据窗口1始终观察 80470a00 处的内存。方便我们的观察。同样的方法观察另外3个链,或者同时观察几个链。你就会看到这些链的变化是多么的频繁。
我们还可以注意到 FreePageList 这个链上的项数总是不多,几个,十几个,或者为零。这是由于,当Free 链上的项数到达一定数量,会引发 zero-page thread ,如果系统空闲的话,zero-page thread 就会被执行,它会把把 Free 链上的物理页清零,放到 Zeroed 链上。
关于物理页状态变化,链的变化更多的内容可以参考 Mark Russinovich 的文章《Inside Memory Management》,这篇文章你可以在网上找到。
对于每一个物理页,都在页帧数据库中有相应的一个 PfnDataBaseEntry ,这样每一个物理页本身就需要使用一定的物理内存。一页物理页大小为4KB,需要一个大小为24字节的PfnDataBaseEntry。即4KB物理内存需要24B内存来保存信息,24/4096=0.005859375,也就是说 整个物理内存的 0.005859375 需要被PfnDataBase
使用。对于128M物理内存来说,需要0.75M内存来保存PfnDataBase中的信息。128M物理内存,将被分成
(128*1024*1024)/(4*1024)=(32*1024)页。所以PfnDataBase需要的页数是(32*1024)*24/4096=192页。对于128MB物理内存来说,最大的页帧号也不会超过(128*1024*1024)/(4*1024)=0x8000,也就是说PfnDataBase 的内容中,形成链的PfnDataBaseEntry的flink,blink 处的值除了0xFFFFFFFF,不会超过0x8000。而0xFFFFFFFF 经常在 PfnDataBaseEntry 中出现。根据这些,我们对于一段内存可以感觉到它是否是PfnDataBaseEntry 中的内容。我们观察 PfnDataBase 之后的第192页中的内容,可以感觉到是 PfnDataBase 的内容。而第193页中的内容就已经明显不是PfnDataBase 的内容了。
需要注意的是,PfnDataBaseEntry 结构,根据物理页状态的不同,结构的各字段的含义也是不一样的。前面对 PfnDataBaseEntry 结构的定义只是帮助你建立起大概的印象,是很不准确的。各种状态下,各字段的含义,会在其他用到的地方做更多的介绍。
8种状态对应值的获得 以及 PageListHead 结构的获得
8种状态对应值,是使用 kd 得到的。kd 的命令 !pfn 可以分析指定页帧号的PfnDataBaseEntry,指出这个PfnDataBaseEntry是哪种状态类型的。于是我利用驱动程序,申请了一块内存。由于每个PfnDataBaseEntry的地址都是从PfnDataBase 开始以0x18为边界的地址。所计算出在申请的这段内存中第一个从 PfnDataBase 开始以0x18为边界的地址,和这个地址对应的页帧号(其实申请的这块内存并不在PfnDataBase中,不过kd的命令!pfn并不管这些,只是根据页帧号算地址,然后分析该地址处的24个字节)。在这个地址开始的内存中写入一些值。使用!pfn分析。实际上我一次申请了10个0x18字节,对齐之后的9个0x18字节中,第一个0x18字节中每个字节都写入0x00。第二个0x18字节中每个字节都写入0x01,直到第九个0x18字节中每个字节都写入0x09。然后用这些地址相应的页块号做参数,使用!pfn命令,就得到了每个值所代表的物理页状态类型。
MmPageLocationList 数组,以及 PageListHead 结构是使用SoftICE跟函数 MiRemovePageByColor 的过程中发现的。使用 kd 的 u MiRemovePageByColor ,反汇编 MiRemovePageByColor 我们可以得到 MiRemovePageByColor 的地址,还可以看到 MmPageLocationList 这个全局变量。 SoftICE 在该地址上下断点, bpx 地址。在跟这个函数的过程中,你可以看到对 PageListHead 结构的操作,从而判断出这个结构中每项的作用。后来又在 www.insidewindows.info 的 unofficial ntifs.h 中也看到了这个结构,和我定义的内容是一样的。不过名字起的更好一些,于是我把该结构中各字段的名字改成了 unofficial ntifs.h 中的名字。
欢迎交流,欢迎交朋友,