来源:https://www.xfocus.net/bbs/index.php?act=ST&f=2&t=58182
内存与进程管理器
==========================
But I fear tomorrow I'll be crying,
Yes I fear tomorrow I'll be crying.
King Crimson'69 -Epitaph
关于Windows NT内存管理器的高层次信息已经够多的了,所以这里不会再讲什么FLAT模型、虚拟内存之类的东西。这里我们只讲具体的底层的东西。我假定大家都了解>i386的体系结构。
目录
==========
00.内核进程线程结构体
01.页表
02.Hyper Space
03.System PTE'S
04.Frame data base (MmPfnDatabase)
05.Working Set
06.向pagefile换页
07.page fault的处理
08.从内存管理器角度看进程的创建
09.上下文切换
0a.某些未公开的内存管理器函数
0b.结语
附录
0c.某些未公开的系统调用
0d.附注及代码分析草稿
00.内核进程线程结构体
===================================
Windows NT中的每一个进程都是EPROCESS结构体。此结构体中除了进程的属性之外还引用了其它一些与实现进程紧密相关的结构体。例如,每个进程都有一个或几个线程,线程在系统中就是ETHREAD结构体。我来简要描述一下存在于这个结构体中的主要的信息,这些信息都是由对内核函数的研究而得知的。首先,结构体中有KPROCESS结构体,这个结构体中又有指向这些进程的内核线程(KTHREAD)链表的指针(分配地址空间),基优先级,在内核模式或是用户模式执行进程的线程的时间,处理器affinity(掩码,定义了哪个处理器能执行进程的线程),时间片值。在ETHREAD结构体中还存在着这样的信息:进程ID、父进程ID、进程映象名、section指针。quota定义了所能使用的分页和非分页池的极限值。VAD(virtual address descriptors)树定义了用户地址空间内存区的状况。关于Working Set的信息定义了在给定时间内有那些物理页是属于进程的。同时还有limit与statistics。ACCESS TOKEN描述了当前进程的安全属性。句柄表描述了进程打开的对象的句柄。该表允许不在每一次访问对象时检查访问权限。在EPROCESS结构体中还有指向PEB的指针。
ETHREAD结构体还包含有创建时间和退出时间、进程ID和指向EPROCESS的指针,启动地址,I/O请求链表和KTHREAD结构体。在KTHREAD中包含有以下信息:内核模式和用户模式线程的创建时间,指向内核堆栈基址和顶点的指针、指向服务表的指针、基优先级与当前优先级、指向APC的指针和指向TEB的指针。KTHREAD中包含有许多其它的数据,通过观察这些数据可以分析出KTHREAD的结构。
01.页表
==================
通常操作系统使用页表来进行内存操作。在Windows NT中,每一个进程都有自己私有的页表(进程的所有线程共享此页表)。相应的,在进程切换时会发生页表的切换。为了加快对页表的访问,硬件中有一个translation lookaside buffer(TLB)。在Windows NT中实现了两级的转换机制。在386+处理器上将虚拟地址转换为物理地址过程(不考虑分段)如下:
Virtual Address
+-------------------+-------------------+-----------------------+
|3 3 2 2 2 2 2 2 2 2|2 2 1 1 1 1 1 1 1 1|1 1 |
|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2 1 0|
+-------------------+-------------------+-----------------------+
| Directory index | Page Table index | Offset in page |
+-+-----------------+----+--------------+-----+-----------------+
| | |
| | |
| Page Directory (4Kb)| Page Table (4Kb) | Frame(4Kb)
| +-------------+ | +-------------+ | +-------------+
| | 0 | | | 0 | | | |
| +-------------+ | +-------------+ | | |
| | 1 | | | 1 | | | |
| +-------------+ | +-------------+ | | |
| | | +->| PTE +-+ | | |
| +-------------+ +-------------+ | | | ----------- |
+->| PDE +-+ | | | +->| byte |
+-------------+ | +-------------+ | | ----------- |
| | | | | | | |
+-------------+ | +-------------+ | | |
| | | | | | | |
... | ... | | |
| 1023 | | | 1023 | | | |
CR3->+-------------+ +----->+-------------+ +--->+-------------+
Windows NT 4.0使用平面寻址。NT的地址空间为4G。这4G地址空间中,低2G(地址0-0x7fffffff)属于当前用户进程,而高2G(0x80000000-0xffffffff)属于内核。在上下文切换时,要更新CR3寄存器的值,结果就更换了用户地址空间,这样就达到了进程间相互隔绝的效果。
注:在Windows NT中,从第4版起,除4Kb的页之外同时还使用了4Mb的页(Pentium及更高)来映射内核代码。但是在Windows NT中没有实际对可变长的页提供支持。
PTE和PDE的格式实际上是一样的。
PTE
+---------------+---------------+---------------+---------------+
|3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1 | |
|1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
+---------------------------------------+-----------------------+
| |T P C U R D A P P U R P|
| Base address 20 bits |R P W C W S W |
| |N T D T |
+---------------------------------------+-----------------------+
一些重要的位在i386+下的定义如下:
---------------------------------------------------------------------------
P - 存在位。此位如果未设置,则在地址转换时会产生异常。一般说来,在一些情况下NT内核会使用未设置此位的PTE。
例如,如果向pagefile换出页,保留这些位可以说明其在页面文件中的位置和pagefile号。
U/S - 是否能从user模式访问页。正是借助于此位提供了对内核空间的保护(通常为高2G)。
RW - 是否能写入
NT使用的为OS设计者分配的空闲位
---------------------------------------------------------------------------
PPT - proto pte
TRN - transition pte
当P位未设置时,第5到第9位即派上用场(用于page fault处理)。它们叫做Protection Mask,样子如下:
--------------------------------------------------------------------------------------
* MiCreatePagingFileMap
9 8 7 6 5
---------
| | | | |
| | | | +- Write Copy
| | | +--- Execute
| | +----- Write
| +------- NO CACHE
+--------- Guard
GUARD | NOCACHE组合就是NO ACCESS
* MmGetPhysicalAddress
函数很短,但能从中获得很多信息。在虚地址0xc0000000 - 0xc03fffff上映射有进程的页表。并且,映射的机制非常精巧。在Directory Table(以下称DT)有1100000000b个表项(对应于地址0xc000..-0xc03ff..)指向自己,也就是说对于这些地址DT用作了页表(Page Table)!如果我们使用,比如说,地址(为方便起见使用二进制)
1100000000.0000000101.0000001001.00b
---------- ---------- --------------
0xc0... 页表选择 页表内偏移
页目录
通过页表101b的1001b号,我们得到了PTE。但这还没完——DT本身映射在地址0xc0300000-0xc0300ffc上。在MmSystemPteBase中有值0xc0300000。为什么这样——看个例子就知道了:
1100000000.1100000000.0000001001.00b
---------- ---------- --------------
0xc0... 0xc0... 页目录偏移
页目录 页表-
页目录
选择
最后,在c0300c00包含着用于目录本身的PDE。这个PDE的基地址的值保存在MmSystemPageDirectory中。同时系统为映射物理页MmSystemPageDirectory保留了一个PTE,这就是MmSystemPagePtes。
这样做能简化寻址操作。例如,如果有PTE的地址,则PTE描述的页的地址就等于PTE<<10。反过来:PTE=(Addr>>10)+0xc0000000。
除此之外,在内核中存在着全局变量MmKseg2Frame = 0x20000。该变量指示在从0x80000000开始的哪个地址区域直接映射到了物理内存,也就是说,此时虚拟地址0x80000000 - 0x9fffffff映射到了物理地址00000000-1f000000。
还有几个有意思的地方。从c0000000开始有个0x1000*0x200=0x200000=2M的描述地址的表(0-7fffffff)。描述这些页的PDE位于地址c0300000-0xc03007fc。对于i486,在地址c0200000-c027fffc应该是描述80000000到a0000000的512MB的表,但对于Pentium在区域0xc0300800-0xc03009fc是4MB的PDE,其描述了从0 到1fc00000的步长为00400000的4M的物理页,也就是说选择了4M的页。对应于这些PDE的虚地址为80000000, 9fffffff。
这样我们就得到了页表的分布:
范围 c0000000 - c01ffffc 用于00000000-7fffffff的页表
范围 c0200000 - c027ffff "吃掉" 4M地址页的地址
范围 c0280000 - c02ffffc 包含用于a0000000 - bfffffff的页
范围 c0300000 - c0300ffc PD 本身 (描述范围c0000000 - c03fffff)
范围 c0301000 - c03013fc c0400000 - c04fffff HyperSpace (更准确的说, 是1/4的hyper space)
范围 c0301400 - c03fffff 包含用于c050000 - ffffffff的页
注:在0xc0301000-0xc0301ffc包含有描述hyper space的页表。这是内核的地址空间,且对于不同的进程映射的内容是不同的(另一方面,内核空间又总是在每个用户进程的上下文中)。这是进程私有的区域。例如,working set就位于hyper space中。页表的前256个PTE(hyper space的前1/4)为内核保留,而且在需要快速向frame中映射虚拟地址时使用。
我给出一个向区域0xc0200000-0xc027f000中一个地址进行映射的例子。
1100000000.1000000000.000000000000 = 0xc0200000
1) 解析出 PDE #1100000000 (4k 页) 并选出 PageDirectory
2) 在 Directory 中选出 PTE #1000000000 (c0300800)
这是个 4MB 的 PDE - 但这里忽略位长度,
因为 PDE 用作了 PTE. 结果 c0200000 - c0200fff 被映射为
80000000-80000fff
c0201000 映射到下面的 - 80400000- 80400fff.
等等直到 c027f000 - 9fc00000
PTE, 位于c0200000到c027fffc - 描述了80000000 - 9ffffc00 (512m)
02.Hyper Space
==============
HyperSpace是内核空间中的一块区域 (4mb), 不同的进程映射内容不同。对于转换,4MB足够放下页表完整的一页。这个表位于地址0xc0301000 - 0xc0301ffc(PDE的第0个表项位于0xc0300c04)。在内部,为向HyperSpace区域中映射物理页(当需要快速为某个frame组织虚拟地址时)要使用函数:
DWORD MiMapPageInHyperSpace(DWORD BaseAddr,OUT PDWORD Irql);
它返回HyperSpace中的虚拟地址,这个虚拟地址被映射到所要的物理页上。这个函数是如何工作的,工作的时候用到了什么?
在内核中有这样的变量:
MmFirstReservedMappingPte=0xc0301000
MmLastReservedMappingPte=0xc03013fc
这两个变量描述了255个pte,这些pte描述了区域:
0xc0400000-0xc04fffff (1/4 HyperSpace)
在MmFirstReservedMappingPte处是一个pte,其中的基址扮演了计数器的角色(从0到255)(当然,pte是无效的,p位无效)。为所需地址添加pte时要依赖计数器当前的值……并且计数器使用了下开口堆栈的原理,从ff开始。一般来说,页表中的pte用作信息上的目的并不是唯一的情况。
03.System PTE'S
===============
在内核中有一块这样的内存——系统pte。什么是系统pte,以及内核如何使用系统pte?
*见函数 MiReserveSystemPtes(...)
系统为空闲PTE维护了某些结构体。首先为了快速满足密集请求(当内核需要pte映射某些物理页时)系统中有个Sytem Ptes Pool。而且pool中有pte blocks(blocks表示请求是以block为单位来满足的,一个block中有一些pte,1、2、4、8和16个pte)。
系统中有以下这些表:
BYTE MmSysPteTables[16]={0,0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4};
DWORD MmSysPteIndex[5]={1,2,4,8,16};
DWORD MmFreeSysPteListBySize[5];
PPTE MmLastSysPteListBySize[5];
DWORD MmSysPteListBySizeCount[5];
DWORD MmSysPteMinimumFree[5]={100,50,30,20,20}
PVOID MmSystemPteBase;// 0xc0200000
在pool中的空闲PTE被组织成了链表(当然,pte是位于页表中,也就是说链表结构体位于页表中,这是真的)。链表的元素:
typedef struct _FREE_SYSTEM_PTES_BLOCK{
/*pte0*/ SYSPTE_REF NextRef; // 指向后面的block
/*pte1*/ DWORD FlushUnkn; // 在Flush时使用
/*pte2*/ DWORD ArrayOfNulls[ANY_SIZE_ARRAY]; // 空闲 PTE
}FREE_SYSTEM_PTES_BLOCK PFREE_SYSTEM_PTES_BLOCK;
用作指向后面元素指针的PTE的地址可如此获得:VA=(NextRef>>10)+MmSystemPteBase (低10位永远为0,相应的p位也为0)。链表最后一个元素NextRef域的值为0xfffff000 (-1) 。相应的,链表有5个(block大小分别为1,2,4,8和16个pte)。
*见函数 MiReserveSystemPtes2(...) / MiInitializeSystemPtes
除pool外还有一个undocumented的空闲系统pte链表。
PPTE MmSystemPtesStart[2];
PPTE MmSystemPtesEnd[2];
SYSPTE_REF MmFirstFreeSystemPte[2];
DWORD MmTotalFreeSystemPtes[2];
在两个链表中有两个引用。链表的元素:
typedef struct _FREE_SYSTEM_PTES{
SYSPTE_REF Next; // #define ONLY_ONE_PTE_FLAG 2, last = 0xfffff000
DWORD NumOfFreePtes;
}FREE_SYSTEM_PTES PFREE_SYSTEM_PTES;
而且,1号链表原则上没有组织。0号链表(MiReleaseSystemPtes)用于释放的pte。pte有可能进入System Ptes Pool。若在请求MiReserveSystemPtes(...)时pte的数目大于16,则同时pte从0号链表分配。也就是说,0号链表与pool有关联,而1号则没有。
为了使工作的结果不与TLB相矛盾,系统要么使用重载cr3,要么使用命令invlpg。“高级”函数
MiFlushPteList(PTE_LIST* PteList, BOOLEAN bFlushCounter, DWORD PteValue);
进行以下工作:
初始化PTE并调用invlpg(汇编指令)。
typedef struct PTE_LIST{
DWORD Counter; // max equ 15
PVOID PtePointersInTable[15];
PVOID PteMappingAddresses[15];
};
如果Counter大于15,则调用KeFlushCurrentTb(只是重载CR3),并且如果设置了bFlushCounter,则向MmFlushCounter加0x1000。
04.Page Frame Number Data Base (MmPfnDatabase)
======================================
内核将有关物理页的信息保存在pfn数据库中(MmPfnDatabase)。本质上讲,这只是个0x18字节长的结构体块。每一个结构体对应一个物理页(顺序排列,所以元素常被称为Pfn - page frame number)。结构体的数量对应于系统中4KB页的数量(或者说是内核可见的页的数量,需要的话可以在boot.ini中使用相应的选项来为NT内核做出这块“坏”页区)。通常,结构体形式如下:
typedef struct _PfnDatabaseEntry
{
union {
DWORD NextRef; // 0x0 如果frame在链表中,则这个就是frame的号
// 最后的一个为 -1
DWORD Misc; // 同时另外一项信息, 依赖于上下文
// 见伪代码 (通常 TmpPfn->0...)
// 通常这里有 *KTHREAD, *KPROCESS,
// *PAGESUPPORT_BLOCK...
};
PPTE PtePpte; // 0x4 指向 pte 或 ppte
union { // 0x8
DWORD PrevRef; // 前面的frame或 (-1, 第一个)
DWORD ShareCounter; // Share 计数器
};
WORD Flags; // 0xc 见下面
WORD RefCounter; // 0xe 引用计数
DWORD Trans; // 0x10 ?? 见下面. 用于 pagefile
DWORD ContFrame;//ContainingFrame; // 14
}PfnDatabaseEntry;
/*
Flags (名字取自windbg !pfn的结果)
掩码 位 名字 值
----- ---- --- --------
0001 0 M Modifyied
0002 1 R Read In Progress
0004 2 W WriteInProgress
0008 3 P Shared
0070 [4:6] Color Color (In fact Always null for x86)
0080 7 X Parity Error
0700 [8:10] State 0- Zeroed
/List 1- Free
2- StandBy
3- Modified
4- ModifiedNoWrite
5- BadPage
6- Active
7- Trans
0800 11 E InPageError
Trans域的值用在frame的内容位于PageFile中的时候或是frame的内容位于与这个Page File PTE对应的其它映象文件中的时候。
我给出未设置P位的PTE的例子(这种PTE不由平台体系结构确定,而由OS确定)。
* 取自 @MiReleasePageFileSpace (Trans)
Page File PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
| offset |T|P|Protect. |page |0|
| |R|P|mask |file | |
| |N|T| |Num | |
+---------------------------------------+-+-+---------+-------+-+
Transition PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
| PFN |T|P|Protect. |C W O W|0|
| |R|P|mask |D T | |
| |N|T| | | |
+---------------------------------------+-+-+---------+-------+-+
W - write
O - owner
WT - write throuth
CD - cache disable
可能所有这些现在还不很易懂,但是看完下面就能明白了。当然,这个结构体是未公开的。显然,结构体能够组织成链表。frame由以下结构体支持:
struct _MmPageLocationList{
PPfnListHeader ZeroedPageListhead; //&MmZeroedPageListhead
PPfnListHeader FreePageListHead; //&MmFreePageListHead
PPfnListHeader StandbyPageListHead; //&MmStandbyPageListHead
PPfnListHeader ModifiedPageListHead; //&MmModifiedPageListHead
PPfnListHeader ModifiedNoWritePageListHead;//&MmModifiedNoWritePageListHead
PPfnListHeader BadPageListHead; //&MmBadPageListHead
}MmPageLocationList;
这其中包含了6个链表。各域的名字很好的说明了它们的用处。frame的状态与这些链表密切关联。下面列举了frame的状态:
+---------------+----------------------------------------------------+------+
|状态 |描述 | 链表 |
+---------------+----------------------------------------------------+------+
|Zero |清零的可用空闲页 | 0 |
|Free |可用空闲页 | 1 |
|Standby |不可用但可轻易恢复的页 | 2 |
|Modified |要换出的dirty页 | 3 |
|ModifiedNoWrite|不换出的dirty页 | 4 |
|Bad |不可用的页(有错误) | 5 |
|Active |活动页,至少映射一个虚拟地址 | - |
+---------------+----------------------------------------------------+------+
frame可能处在6个链表中的某一个,也可能不在这些链表中(状态为Active)。如果页属于某个进程,则这个页就被记录在Working Set中(见后面)。同时,如果frame由内存管理器自己使用,则一般可以不考虑这些frame的位置。








