第四章 探索Windows 2000的内存管理机制
翻译:Kendiv( [email protected] )
更新: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) |
……………….本章完……………… 
|