第四章 探索 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"rnContiguous memory blocks:"
L"rn-------------------------rnrn");
do {
if (!IoControl (hDevice, SPY_IO_PAGE_ENTRY,
&pbPage, PVOID_,
&spe, SPY_PAGE_ENTRY_))
{
n += _printf (L" !!! Device I/O error !!!rn");
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)rn",
pbPage-1, pbPage-pbBase);
pbBase = INVALID_ADDRESS;
}
}
}
while (pbPage += spe.dSize);
if (pbBase != INVALID_ADDRESS)
{
n += _printf (L"0x%08lXrn", pbPage-1);
}
n += _printf (L"rn"
L" Present bytes: 0x%08lXrn"
L" Total bytes: 0x%08lXrn",
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 ) |
标签: