《Undocumented Windows 2000 Secrets》翻译 --- 第四章(9)
第四章 探索 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 )