《Undocumented Windows 2000 Secrets》翻译 --- 第四章(5)
第四章 探索 Windows 2000 的内存管理机制
翻译: Kendiv( fcczj@263.net )
更新: Sunday, February 17, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
IOCTL 函数 SPY_IO_INTERRUPT
SPY_IO_INTERRUP 类似于 SPY_IO_SEGEMT ,不过该函数仅影响存储在系统中断描述符表( IDT )的中断描述符,不会涉及 LDT 或 GDT 描述符。 IDT 最多可容纳 256 个描述符,这些描述符可用来描述任务门、中断门或陷阱门(参见 Intel 1999c, pp. 5-11ff )。顺便说一下,中断和陷阱在本质上十分相似,二者只存在微小的差异:在进入一个中断处理例程后,总是会屏蔽其他中断;而进入陷阱处理例程却不会修改中断标志。 SPY_IO_INTERRUPT 的调用者提供一个 0 到 255 之间的中断号,该中断号将位于输入缓冲区中,而一个 SPY_INTERRUPT 结构将作为输出数据被存放到输出缓冲区中,如果成功返回,该结构中将包含对应的中断处理例程的属性。由 Dispatcher 调用的帮助函数 SpyOutputInterrupt() 只是一个简单的外包函数,它实际上调用 SpyInterrupt() 函数并且将需要返回的数据复制到输出缓冲区中。 列表 4-18 给出了这两个函数,以及它们操作的 SPY_INTERRUPT 结构。稍后一些, SpyInterrupt() 函数将填充如下项目:
l Selector 用来指定一个任务状态段( Task-State Segment, TSS )或代码段( Code Segment )的选择器。代码段选择器用来确定中断或陷阱处理例程所在的段。
l Gate 用来表示一个 64 位的任务门、中断门或陷阱门描述符,由 Selector 确定其地址。
l Segment 包含段的属性,该段的地址由前面的 Gate 给出。
l pOffset 指定中断或陷阱处理例程的入口地址相对基地址的偏移量。这里的基地址是指中断或陷阱处理例程所在代码段的起始地址。因为任务门不包含偏移量,所以,如果输入的选择器指向一个 TSS ,则忽略该成员。
l fOk 一个标志变量,用来指示 SPY_INTERRUPT 结构中的数据是否有效。
通常情况下, TSS 被用来保证一个错误情况可以被一个有效的任务处理。这是一个特殊的系统段类型( system segment type ),它可以保存 104 个字节的进程状态信息,该信息在任务切换时,用来进行任务的恢复,如 表 4-3 所示。当与任务相关的中断发生时, CPU 总是强制切换该任务,并将所有的 CPU 寄存器保存到 TSS 中。 Windows 2000 在中断位置 0x02 (非屏蔽中断 [NMI] , 0x08[Double Fault] 和 0x12[ 堆栈段故障 ] )处保存任务门。剩余的位置指向中断处理例程。不使用的中断由一个哑元例程 ---KiUnexpectedInterruptNNN() 处理,这里的 NNN 为一个十进制数。这些哑元例程最后都汇集到内部函数 KIEndUnexpectedRange() ,在这里,这些例程将依次进入 KiUnexpectedInterruptTail() 。
typedef struct _SPY_INTERRUPT
{
X86_SELECTOR Selector;
X86_GATE Gate;
SPY_SEGMENT Segment;
PVOID pOffset;
BOOL fOk;
}
SPY_INTERRUPT, *PSPY_INTERRUPT, **PPSPY_INTERRUPT;
#define SPY_INTERRUPT_ sizeof (SPY_INTERRUPT)
// -----------------------------------------------------------------
NTSTATUS SpyOutputInterrupt (DWord dInterrupt,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_INTERRUPT si;
SpyInterrupt (dInterrupt, &si);
return SpyOutputBinary (&si, SPY_INTERRUPT_,
pOutput, dOutput, pdInfo);
}
// -----------------------------------------------------------------
BOOL SpyInterrupt (DWORD dInterrupt,
PSPY_INTERRUPT pInterrupt)
{
BOOL fOk = FALSE;
if (pInterrupt != NULL)
{
if (dInterrupt <= X86_SELECTOR_LIMIT)
{
fOk = TRUE;
if (!SpySelector (X86_SEGMENT_OTHER,
dInterrupt << X86_SELECTOR_SHIFT,
&pInterrupt->Selector))
{
fOk = FALSE;
}
if (!SpyIdtGate (&pInterrupt->Selector,
&pInterrupt->Gate))
{
fOk = FALSE;
}
if (!SpySegment (X86_SEGMENT_OTHER,
pInterrupt->Gate.Selector,
&pInterrupt->Segment))
{
fOk = FALSE;
}
pInterrupt->pOffset = SpyGateOffset (&pInterrupt->Gate);
}
else
{
RtlZeroMemory (pInterrupt, SPY_INTERRUPT_);
}
pInterrupt->fOk = fOk;
}
return fOk;
}
// -----------------------------------------------------------------
PVOID SpyGateOffset (PX86_GATE pGate)
{
return (PVOID) (pGate->Offset1 | (pGate->Offset2 << 16));
}
列表 4-18. 查询中断属性
表 4-3. 任务状态段( TSS )中的 CPU 状态域
偏移量
位数
ID
描 述
0x00
16
前一个任务的链接
0x04
32
ESP0
Ring0 级的堆栈指针寄存器
0x08
16
SS0
Ring0 级的堆栈段寄存器
0x0C
32
ESP1
Ring1 级的堆栈指针寄存器
0x10
16
SS1
Ring1 级的堆栈段寄存器
0x14
32
ESP2
Ring2 级的堆栈指针寄存器
0x18
16
SS2
Ring2 级的堆栈段寄存器
0x1C
32
CR3
页目录基址寄存器( PDBR )
0x20
32
EIP
指令指针寄存器
0x24
32
EFLAGS
处理器标志寄存器
0x28
32
EAX
通用寄存器
0x2C
32
ECX
通用寄存器
0x30
32
EDX
通用寄存器
0x34
32
EBX
通用寄存器
0x38
32
ESP
堆栈指针寄存器
0x3C
32
EBP
基地址指针寄存器
0x40
32
ESI
源索引寄存器
0x44
32
EDI
目标索引寄存器
0x48
16
ES
扩展段寄存器
0x4C
16
CS
代码段寄存器
0x50
16
SS
堆栈段寄存器
0x54
16
DS
数据段寄存器
0x58
16
FS
附加的数据段寄存器 #1
0x5C
16
GS
附加的数据段寄存器 #2
0x60
16
LDT
本地描述符标的段选择器
0x64
1
1
调试陷阱标志
0x66
16
I/O Map 的基地址
0x68
-
CPU 状态信息结束
SpyInterrupt() 调用的 SpySegment() 、 SpySelector() 函数已经在 列表 4-5 和 列表 4-16 中给出。 SpyGateOffset() 位于 列表 4-18 的末尾,它的工作和 SpyDescriptorBase() 、 SpyDescriptorLimit() 类似,从 X86_GATE 结构中取出 Offset1 和 Offset2 位域,并适当的组织它们以构成一个 32 位地址。 SpyIdtGaet() 定义于 列表 4-19 。它与 SpyDescriptor() 十分类似。汇编指令 SIDT 存储一个 48 位的值,该值就是 CPU 的 IDT 寄存器的内容,它由一个 16 位的表大小限制值和 IDT 的 32 位线性基地址构成。 列表 4-19 中的剩余代码将选择器的描述符索引和 IDT 的大小限制值进行比较,如果 OK ,则对应的中断描述符将被复制到调用者提供的 X86_GATE 结构中。否则,门结构的所有成员都将被设置为 0 。
BOOL SpyIdtGate (PX86_SELECTOR pSelector,
PX86_GATE pGate)
{
X86_TABLE idt;
PX86_GATE pGates = NULL;
BOOL fOk = FALSE;
if (pGate != NULL)
{
if (pSelector != NULL)
{
__asm
{
sidt idt.wLimit
}
if ((pSelector->wValue & X86_SELECTOR_INDEX)
<= idt.wLimit)
{
pGates = idt.pGates;
}
}
if (pGates != NULL)
{
RtlCopyMemory (pGate,
pGates + pSelector->Index,
X86_GATE_);
fOk = TRUE;
}
else
{
RtlZeroMemory (pGate, X86_GATE_);
}
}
return fOk;
}
列表 4-19. 获取 IDT 门的值
IOCTL 函数 SPY_IO_PHYSICAL
SPY_IO_PHYSICAL 函数很简单,它完全依赖于 ntoskrnl.exe 导出的 MmGetPhysicalAddress() 函数。该 IOCTL 函数通过简单的调用 SpyInputPointer() (参见 列表 4-10 )来获取需要转换的线性地址,然后让 MmGetPhysicalAddress() 查找对应的物理地址,最后将结果作为 PHYSICAL_ADDRESS 结构返回给调用者。注意, PHYSICAL_ADDRESS 是一个 64 位的 LARGE_INTEGER 。在大多数 i386 系统上,其高 32 位总是为 0 。不过,若系统启用了物理地址扩展( Physical Address Extension, PAE ),并且安装的内存大于 4GB ,这些位可能就是非 0 值了。
MmGetPhysicalAddress() 使用起始于线性地址 0xC0000000 的 PTE 数组,来进行物理地址的查找。其基本的工作机制如下:
l 如果线性地址位于: 0x80000000----0x9FFFFFFF ,则其高 3 位将被设为零,最后产生的物理地址位于: 0x00000000-----0x1FFFFFFF 。
l 否则,线性地址的高 20 位将作为 PTE 数组(起始于 0xC0000000 )的索引。
l 如果目标 PTE 的 P 位已被设置,这表示其对应得数据页存在于物理内存中。除了 20 位的 PFN 外,所有的 PTE 位都可以被剥离出来,线性地址最低的 12 位将作为在数据页中的偏移量被加到最后的 32 位物理地址上去。
l 如果数据页没有存在于物理内存中, MmGetPhysicalAddress() 返回 0 。
MmGetPhysicalAddress() 假设内核内存范围: 0x80000000----0x9FFFFFF 之外的所有线性地址都使用 4KB 的页。而其他函数,如 MmIsAddressValid() ,会首先加载线性地址的 PDE ,并且检查该 PDE 的 PS 位,以检查页大小是 4KB 还是 4MB 。这是一个非常通用的方法,可以处理任意的内存配置。不过上述两个函数都会返回正确的结果,这是因为 Windows 2000 仅针对内存范围: 0x80000000-----0x9FFFFFFF ,使用 4MB 页。不过某些内核 API 函数,显然设计的比其它的灵活许多。
IOCTL 函数 SPY_IO_CPU_INFO
个别的 CPU 指令仅对运行于 Ring 0 级的代码有效, Ring 0 是五个特权级( Intel 系列的 CPU 只支持两个特权级: Ring0 和 Ring3 )中级别最高的一个。用 Windows 术语来说, Ring 0 意味着内核模式( Kernel-mode )。这些被禁止的指令有:读取控制寄存器 CR0 、 CR2 和 CR3 的内容。因为这些寄存器中保存着非常有趣的信息,应用程序可能想要找到一个办法来访问它们,解决方案就是 SPY_IO_CPU_INFO 函数。如 列表 4-20 所示, IOCTL 处理例程调用的 SpyOutputCpuInfo() 函数使用了一些嵌入式汇编来读取控制寄存器,以及其他一些有价值的信息,比如 IDT 的内容, GDT 和 LDT 寄存器以及存储在寄存器 CS 、 DS 、 ES 、 FS 、 GS 、 SS 和 TR 中的段选择器。任务寄存器( Task Register, TR )还包含一个涉及当前任务的 TSS 的选择器。
typedef struct _SPY_CPU_INFO
{
X86_REGISTER cr0;
X86_REGISTER cr2;
X86_REGISTER cr3;
SPY_SEGMENT cs;
SPY_SEGMENT ds;
SPY_SEGMENT es;
SPY_SEGMENT fs;
SPY_SEGMENT gs;
SPY_SEGMENT ss;
SPY_SEGMENT tss;
X86_TABLE idt;
X86_TABLE gdt;
X86_SELECTOR ldt;
}
SPY_CPU_INFO, *PSPY_CPU_INFO, **PPSPY_CPU_INFO;
#define SPY_CPU_INFO_ sizeof (SPY_CPU_INFO)
// -----------------------------------------------------------------
NTSTATUS SpyOutputCpuInfo (PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_CPU_INFO sci;
PSPY_CPU_INFO psci = &sci;
__asm
{
push eax
push ebx
mov ebx, psci
mov eax, cr0
mov [ebx.cr0], eax
mov eax, cr2
mov [ebx.cr2], eax
mov eax, cr3
mov [ebx.cr3], eax
sidt [ebx.idt.wLimit]
mov [ebx.idt.wReserved], 0
sgdt [ebx.gdt.wLimit]
mov [ebx.gdt.wReserved], 0
sldt [ebx.ldt.wValue]
mov [ebx.ldt.wReserved], 0
pop ebx
pop eax
}
SpySegment (X86_SEGMENT_CS, 0, &sci.cs);
SpySegment (X86_SEGMENT_DS, 0, &sci.ds);
SpySegment (X86_SEGMENT_ES, 0, &sci.es);
SpySegment (X86_SEGMENT_FS, 0, &sci.fs);
SpySegment (X86_SEGMENT_GS, 0, &sci.gs);
SpySegment (X86_SEGMENT_SS, 0, &sci.ss);
SpySegment (X86_SEGMENT_TSS, 0, &sci.tss);
return SpyOutputBinary (&sci, SPY_CPU_INFO_,
pOutput, dOutput, pdInfo);
}
列表 4-20. 查询 CPU 状态信息
可使用帮助函数 SpySegement() 获取段选择器,在前面,我们已讨论过该函数。参见 列表 4-15 。