第四章 探索 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 。
标签: