第四章 探索Windows 2000的内存管理机制
翻译:Kendiv( fcczj@263.net )
更新:Tuesday, February 22, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
Windows 2000的分段和描述符
w2k_mem.exe的另一个很棒的选项是+e,该选项将显示和说明处理器的段寄存器和描述表的内容。示列4-13给出了其典型输出。CS、DS和ES段寄存器的内容非常清晰的证明了Windows 2000为每个进程提供了平坦的4GB地址空间:起始于0x00000000,终止于0xFFFFFFFF。示列4-13中最右边的标志符用来表示段的类型,该段的类型由它的描述符的Type成员给出。代码和数据段的Type属性可分别符号化为“cra”和“ewa”。省略号“-”意味着相应的属性没有设置。一个任务状态段(Task State Segment,TSS)仅能有“a”(可用)和“b”(忙)两种属性。表4-5给出了所有可用的属性。示列4-13展示了Windows 2000的CS段的不一致性,CS段允许执行和读取,而DS、ES、FS和SS段的属性则是可扩展和读/写访问。另一个不明显但十分重要的细节是CS、FS和SS段的DPL在用户模式和内核模式并不相同。DPL是描述符特权级别(Descriptor Privilege Level)。对于代码段(CS),仅当调用者位于其DPL指定的特权级时才能调用该段中的代码(参考Intel 1999c, pp. 4-8f)。在用户模式,CS段的DPL为3;在内核模式,其DPL为0。对于数据段(DS),其DPL是最低的特权级,在用户模式下,所有特权级都可访问它,而在内核模式下,仅允许特权0访问。
示列4-13. 显示CPU信息
IDT和GDT寄存器的内容显示了GDT的范围是:0x8003F000 --- 0x8003F3FF,紧随其后的就是IDT,其地址范围是:0x8003F400 --- 0x8003FBFF。由于每个描述符占用64位,故GDT和IDT分别包含128和256个项。注意,GDT可容纳8,192个项,但Windows 2000仅使用了其中的一小部分。
表4-5 代码和数据段的Type属性
段
属 性
描 述
CODE
c
使段一致(低特权的代码可能进入)
CODE
r
允许读访问(和仅执行访问相斥)
CODE
a
段可以访问
DATA
e
向下扩展段(堆栈段的典型属性)
DATA
w
允许写访问(和仅读取访问相斥)
DATA
a
段可以访问
TSS32
a
任务状态段可用
TSS32
b
任务状态段繁忙
W2k_mem.exe还提供了两个很有特色的选项----+g和+i,这两个选项可显示GDT和IDT的更多细节。示列4-14示范了+g选项的输出。它很类似于示列4-13中的“kernel-model segment:”一节,但列出了在内核模式下所有可用的段选择子(selector),而不仅仅是存储在段寄存器中的那些。W2k_mem.exe通过遍历整个GDT来获取所有的段选择子,可通过IOCTL函数SPY_IO_SEGMENT来指示Spy设备查询段信息。仅显示有效的选择子。比较示列4-13和4-14中的GDT选择子将十分有趣,GDT的选择子定义于ntddk.h中,汇总在表4-6。显然,它们与w2k_mem.exe的输出是一致的。
示列4-14. 显示GDT描述符
表4-6. 定义于ntddk.h中的GDT选择子(selector)
符 号
值
注 释
KGDT_NULL
0x0000
空的段选择子(无效)
KGDT_R0_CODE
0x0008
内核模式的CS寄存器
KGDT_R0_DATA
0x0010
内核模式的SS寄存器
KGDT_R3_CODE
0x0018
用户模式的CS寄存器
KGDT_R3_DATA
0x0020
用户模式的DS、ES和SS寄存器,内核模式的DS和ES寄存器
KGDT_TSS
0x0028
位于用户和内核的任务状态段
KGDT_R0_PCR
0x0030
内核模式的FS寄存器(处理器控制区域)
KGDT_R3_TEB
0x0038
用户模式的FS寄存器(线程环境块)
KGDT_VDM_TILE
0x0040
基地址0x00000400,限制0x0000FFFF(DOS虚拟机)
KGDT_LDT
0x0048
本地描述符表
KGDT_DF_TSS
0x0050
Ntoskrnl.exe 变量 KiDoubleFaultTSS
KGDT_NMI_TSS
0x0058
Ntoskrnl.exe 变量 KiNMITSS
示列4-14中的选择子(selector)没有在表4-6中列出,其中的某些选择子可以通过查找熟悉的基地址或其内存内容来确认它们。使用内核调试器可查找其中某些选择子的基地址对应的符号。表4-7给出了我已经确认的选择子。
W2k_mem.exe的+i选项可转储IDT中的门描述符(Gate Descriptor)。示列4-15给出了IDT的门描述符的部分内容,Intel仅定义了IDT中的前20个门描述符(Intel 1999c, pp. 5-6)。IDT中的中断0x14到0x1F由Intel保留;剩余的0x20到0xFF由操作系统使用。
在表4-8中,我给出了所有可确认的特殊的中断、陷阱和任务门。大多数用户自定义的中断都指向哑元例程---KiUnexpectedinterruptnNNN(),在前面我们已经解释过它。对于某些中断处理例程的地址,内核调试器也无法解析其地址对应的符号。
表4-7. 更多的GDT选择子(selector)
值
基地址
描 述
0x0078
0x80400000
Ntoskrnl.exe的代码段
0x0080
0x80400000
Ntoskrnl.exe的数据段
0x00A0
0x814985A8
TSS(EIP成员指向HalpMcaExceptionHandlerWrapper)
0x00E0
0xF0430000
ROM BIOS代码段
0x00F0
0x8042DCE8
Ntoskrnl.exe函数KiI386CallAbios
0x0100
0xF0440000
ROM BIOS数据段
0x0108
0xF0440000
ROM BIOS数据段
0x0110
0xF0440000
ROM BIOS数据段
示列4-15. 显示IDT门描述符
表4-8. Windows 2000 中断、陷阱和任务门
INT
Intel定义的描述符
拥有者
处理例程/TSS
0x00
整除错误(DE)
ntoskrnl.exe
KiTrap00
0x01
调试(DB)
ntoskrnl.exe
KiTrap01
0x02
NMI中断
ntoskrnl.exe
KiNMITSS
0x03
断点(BP)
ntoskrnl.exe
KiTrap03
0x04
溢出(OF)
ntoskrnl.exe
KiTrap04
0x05
越界(
BR)
ntoskrnl.exe
KiTrap05
0x06
未定义的操作码(UD)
ntoskrnl.exe
KiTrap06
0x07
没有数学协处理器(NM)
ntoskrnl.exe
KiTrap07
0x08
Double Fault(DF)
ntoskrnl.exe
KiDouble
0x09
协处理器段溢出
ntoskrnl.exe
KiTrap09
0x0A
无效的TSS(TS)
ntoskrnl.exe
KiTrap0A
0x0B
段不存在(NP)
ntoskrnl.exe
KiTrap0B
0x0C
堆栈段故障(SS)
ntoskrnl.exe
KiTrap0C
0x0D
常规保护(GP)
ntoskrnl.exe
KiTrap0D
0x0E
页故障(PF)
ntoskrnl.exe
KiTrap0E
0x0F
Intel保留
ntoskrnl.exe
KiTrap0F
0x10
Math Fault(MF)
ntoskrnl.exe
KiTrap10
0x11
对齐检查(AC)
ntoskrnl.exe
KiTrap11
0x12
Machine Check(MC)
?
?
0x13
流SIMD扩展
ntoskrnl.exe
KiTrap0F
0x14-0x1F
Intel保留
ntoskrnl.exe
KiTrap0F
0x2A
用户自定义
ntoskrnl.exe
KiGetTickCount
0x2B
用户自定义
ntoskrnl.exe
KiCallbackReturn
0x2C
用户自定义
ntoskrnl.exe
KiSetLowWaitHighThread
0x2D
用户自定义
ntoskrnl.exe
KiDebugSerice
0x2E
用户自定义
ntoskrnl.exe
KiSystemService
0x2F
用户自定义
ntoskrnl.exe
KiTrap0F
0x30
用户自定义
hal.dll
HalpClockInterrupt
0x38
用户自定义
hal.dll
HalpProfileInterrupt
Windows 2000的内存区域
W2k_mem.exe的最后一个还未讨论的选项是:+b选项。该选项会产生4GB地址空间中相邻内存区域的列表,这个列表非常大。W2k_mem.exe使用Spy设备的IOCTL函数SPY_IO_PAGE_ENTRY遍历整个PTE数组(位于地址0xC0000000)来生成这个列表。在作为结果的每个SPY_PAGE_ENTRY结构中,通过将它们的dSize成员与其对应的PTE线性地址相加即可得到下一个PTE的地址。列表4-30给出了该选项的实现方式。
DWORD WINAPI DisplayMemoryBlocks (HANDLE hDevice)
{
SPY_PAGE_ENTRY spe;
PBYTE pbPage, pbBase;
DWORD dBlock, dPresent, dTotal;
DWORD n = 0;
pbPage = 0;
pbBase = INVALID_ADDRESS;
dBlock = 0;
dPresent = 0;
dTotal = 0;
n += _printf (L"\r\nContiguous memory blocks:"
L"\r\n-------------------------\r\n\r\n");
do {
if (!IoControl (hDevice, SPY_IO_PAGE_ENTRY,
&pbPage, PVOID_,
&spe, SPY_PAGE_ENTRY_))
{
n += _printf (L" !!! Device I/O error !!!\r\n");
break;
}
if (spe.fPresent)
{
dPresent += spe.dSize;
}
if (spe.pe.dValue)
{
dTotal += spe.dSize;
if (pbBase == INVALID_ADDRESS)
{
n += _printf (L"%5lu : 0x%08lX ->",
++dBlock, pbPage);
pbBase = pbPage;
}
}
else
{
if (pbBase != INVALID_ADDRESS)
{
n += _printf (L" 0x%08lX (0x%08lX bytes)\r\n",
pbPage-1, pbPage-pbBase);
pbBase = INVALID_ADDRESS;
}
}
}
while (pbPage += spe.dSize);
if (pbBase != INVALID_ADDRESS)
{
n += _printf (L"0x%08lX\r\n", pbPage-1);
}
n += _printf (L"\r\n"
L" Present bytes: 0x%08lX\r\n"
L" Total bytes: 0x%08lX\r\n",
dPresent, dTotal);
return n;
}
列表4-30. 查找相邻的线性内存块
示列4-16摘录了在我的机器上使用+b选项的输出列表,可以看出其中的几个区域非常有趣。一些非常明显的地址是:0x00400000,这是w2k_mem.exe内存映像的起始地址(第13号块),还有一个是0x10000000,此处是w2k_lib.dll的基地址(第23号块)。TEB和PEB页也很容易认出(第104号块),hal.dll(第105号块),ntoskrnl.exe(第105号块),win32k.sys(第106号块)。第340---350号块是系统PTE数组的一小段,第347号块是页目录的一部分。第2122号块包含SharedUserData区域,第2123号块由KPCR、KPRCB和包含线程和进程状态信息的CONTEXT结构组成。
示列4-16. 相邻内存块列表示列
还需要补充一下,W2k_mem.exe的+b选项会报告有大量的内存被使用,这可能超出了一个合理的值(比如,你机器上的物理内存数)。请注意示列4-16底部给出的汇总信息。我现在真的使用了700MB的内存吗?Windows 2000的任务管理器显示是150MB,那么这儿的又是什么呢?这种奇特的效果都是由第105号内存块产生的,该内存块表示的范围:0x80000000----0xA01A5FFF占用了0x201A6000字节,也就是说占用了538,599,424字节。这显然是不可能的。问题是整个线性地址空间:0x80000000 ---- 0x9FFFFFFF都被映射到了物理内存:0x00000000 ---- 0x1FFFFFFF,在前面我已经提及过这一点。该区域中的所有4MB页都对应地址0xC0300000处的页目录中的一个有效的PDE,我们可以使用w2k_mem +d #0x200 0xC0300800命令来证明这一点(示列4-17)。因为结果列表中的所有PDE都是奇数(译注:如果PDE为奇数,证明其P位肯定为1),所以它们对应的页都必须存在;不过,它们并不需真正占用物理内存。事实上,这一内存区域的大部分都是“空洞(hole)”,如果将其复制到缓冲区中,可发现它们都被0xFF填充。因此,对于w2k_mem.exe输出的内存使用情况,你不需要过于认真。
示列4-17. 地址范围是:0x80000000 --- 0x9FFFFFFF的PDE
Windows 2000 的内存布局
本章的最后一部分将给出在一个Windows 2000进程“看”来,4GB线性地址空间的总体布局是什么样子。表4-9给出了多个基本数据结构的内存范围。它们之间的“大洞(big hole)”有不同的用途,如,用于进程模块和设备驱动程序的加载区域,内存池,工作集链表等等。注意,有些内存地址和内存块的大小在不同的系统之间有很大的差异,这取决于物理内存和硬件的配置情况、进程的属性以及其他一些系统变量。因此,这里给出的仅仅是一个草图而已,并不是精确的布局图。
有些物理内存块在线性地址空间中出现的两次或更多次。例如,SharedUserData区域位于线性地址0xFFDF0000,并且并镜像到0x7FFE0000。这两个地址都指向物理内存中的同一个页,这意味着,如果向0xFFDF0000+n处写入一个字节,那么0x7FFE0000+n处的值也会随之改变。这是一个虚拟内存的世界----一个物理地址可以被映射到线性地址空间中的任何地方,即使一个物理地址在同一时间映射到多个线性地址也是可以的。回忆一下图4-3和图4-4,它们清楚地展示了线性地址的这种“虚假行为”。它们的目录和表位域正确的指向用来确定数据实际位置的结构体。如果两个PTE的PFN恰好是相同的,那么它们对应的线性地址将指向物理内存相同位置。
表4-9. 进程地址空间中的可确认的内存区域
起始地址
结束地址
十六进制大小
类型/描述
0x00000000
0x0000FFFF
10000
底部的受保护块(Lower guard block)
0x00010000
0x0001FFFF
10000
WCHAR[]/环境字符串,在一个4KB页中分配
0x00020000
0x0002FFFF
10000
PROCESS_PARAMETERS/在一个4KB页中分配
0x00030000
0x0012FFFF
1000000
DWORD[4000]/进程堆栈(默认;1MB)
0x7FFDD000
0x7FFDDFFF
1000
TEB/1#线程的线程环境块
0x7FFDE000
0x7FFDEFFF
1000
TEB/2#线程的线程环境块
0x7FFDF000
0x7FFDFFFF
1000
PEB/进程环境块
0x7FFE0000
0x7FFE02D7
2D8
KUSER_SHARED_DATA/用户模式下的SharedUserData
0x7FFF0000
0x7FFFFFFF
10000
顶部的受保护块(Upper guard block)
0x80000000
0x800003FF
400
IVT/中断向量表
0x80036000
0x800363FF
400
KGDTENTRY[80]/全局描述符表
0x80036400
0x80036BFF
800
KIDTENTRY[100]/中断描述符表
0x800C0000
0x800FFFFF
40000
VGA/ROM BIOS
0x80244000
0x802460AA
20AB
KTSS/内核任务状态段(繁忙)
0x8046AB80
0x8046ABBF
40
KeServiceDescriptorTable
0x8046AB
0x8046ABFF
40
KeServiceDescriptorTableShadow
0x80470040
0x804700A7
68
KTSS/KiDoubleFaultTSS
0x804700A8
0x8047010F
68
KTSS/KiNMITSS
0x804704D8
0x804708B7
3E0
PROC[F8]/KiServiceTable
0x804708B8
0x804708BB
4
DWORD/KiServiceLimit
0x804708BC
0x804709B3
F8
BYTE[F8]/KiArgumentTable
0x814C6000
0x82CC5FFF
1800000
PFN[100000]/MmPfnDatabase(最大为4GB)
0xA01859F0
0xA01863EB
9FC
PROC[27F]/W32pServiceTable
0xA0186670
0x A01863EE
27F
BYTE[27F]W32pArgumentTable
0xC0000000
0xC03FFFFF
400000
X86_PE[100000]/页目录和页表
0xC1000000
0xE0FFFFFF
20000000
系统缓存(MmSystemCacheStart, MmSystemCacheEnd)
0xE1000000
0xE77FFFFF
6800000
页池(Paged Pool)(MmPagedPoolStart, MmPagedPoolEnd)
0xF0430000
0xF043FFFF
10000
ROM BIOS代码段
0xF0440000
0xF044FFFF
10000
ROM BIOS数据段
0xFFDF0000
0xFFDF02D7
2D8
KUSER_SHARED_DATA/内核模式下的SharedUserData
0xFFDFF000
0xFFDFF053
54
KPCR/处理器控制区(内核模式FS段)
0xFFDFF120
0xFFDFF13B
1C
KPRCB/处理器控制块
0xFFDFF13C
0xFFDFF407
2CC
CONTEXT/线程CONTEXT(CPU状态)
0xFFDFF620
0xFFDFF71F
100
后备链表目录(Lookaside list directories)
……………….本章完………………