第四章 探索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。
………………待续…………………