电脑技术学习

《Undocumented Windows 2000 Secrets》翻译 --- 第四章(9)

dn001

第四章 探索 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 )

标签: