3.1.4 Hyperspace 的临时映射1
系列文章目录
文章目录
- 系列文章目录
- 3.1.4 Hyperspace 的临时映射
- MmCreateHyperspaceMapping
- MmGetPageTableForProcess
3.1.4 Hyperspace 的临时映射
Windows内核有时候需要将某些物理页面临时映射到内核的虚存区间,用做“草稿”或其他临时的用途。为此,Windows内核在系统空间划出了一块(虚存)区间专门用于这样的临时映射,称为“超级空间(Hyperspace)”。采用32位物理地址时,这个“空间”的起点HYPERSPACE 定义为0xc0400000:
#define HYPERSPACE (Ke386Pae ? 0xc0800000 : 0xc0400000)
区间的大小为 0x400000,即4MB。换言之,这个区间的大小是1024个页面函数 MmCreateHyperspaceMapping()就用来建立这样的临时映射:
MmCreateHyperspaceMapping
PVOID
NTAPI
MmCreateHyperspaceMapping(PFN_TYPE Page)
{
PVOID Address;
ULONG i;
if (Ke386Pae)
{
...
}
else
{
ULONG Entry;
PULONG Pte;
Entry = PFN_TO_PTE(Page) | PA_PRESENT | PA_READWRITE;//构建页面选项
Pte = ADDR_TO_PTE(HYPERSPACE) + Page % 1024;//页面表象的地址
if (Page & 1024)
{ //物理页面的号bit10为1
for (i = Page % 1024; i < 1024; i++, Pte++)//从Page%1024开始向上搜索
{
if (0 == InterlockedCompareExchange((PLONG)Pte, (LONG)Entry, 0))
{
break;/发现了一个无映射的虚存页面
}
}
if (i >= 1024)//在Page %1024以上无空闲,回头在Page%1024 以下搜索
{
Pte = ADDR_TO_PTE(HYPERSPACE);
for (i = 0; i < Page % 1024; i++, Pte++)//从HYPERSPACE 开始向上搜索
{
if (0 == InterlockedCompareExchange((PLONG)Pte, (LONG)Entry, 0))
{
break;//发现了一个无映射的虚存页面
}
}
if (i >= Page % 1024)
{
KEBUGCHECK(0);
}
}
}
else //物理页面号的Bit10为0
{
for (i = Page % 1024; (LONG)i >= 0; i--, Pte--)//从Page%1024开始向下搜索
{
if (0 == InterlockedCompareExchange((PLONG)Pte, (LONG)Entry, 0))
{
break;//发现了一个无映射的虚存页面
}
}
if ((LONG)i < 0)//在 Page %1024以下无空闲,回头在Page%1024 以上搜索
{
Pte = ADDR_TO_PTE(HYPERSPACE) + 1023;
for (i = 1023; i > Page % 1024; i--, Pte--)//从上向下搜索
{
if (0 == InterlockedCompareExchange((PLONG)Pte, (LONG)Entry, 0))
{
break;//发现了一个无映射的虚存页面
}
}
if (i <= Page % 1024)
{
KEBUGCHECK(0);
}
}
}
}
Address = (PVOID)((ULONG_PTR)HYPERSPACE + i * PAGE_SIZE);//虚拟地址
__invlpg(Address);
return Address;
}
先构建页面映射表项PTE的内容,该内容由物理页面号、PA_PRESENT标志位,以及表示访问模式 PA_READWRITE 的位段三个部分构成。其实这里还有一个表示访问权限要求的位段 DPL,但因为所要求的是0环即系统态,故而数值恰好为0。注意页面表项的内容与所用的虚拟地址即虚拟页面号是无关的,虚拟页面号只决定 PTE在页面表中的位置。
然后就是虚拟页面号的确定了。在理想的情况下,如果物理页面号为Page,那么虚拟页面就应该是HYPERSPACE中的第(Page%1024)个页面。可是,这种理想的状况未必就会发生,因为这个虚拟页面有可能已在此前被分配使用了(同样也是用于临时的映射),这就得在HYPERSPACE 中搜索空闲的虚拟页面作为替代。怎样搜索呢?最简单的当然是完全的顺序搜索,更好的方法则是引入-些类似于散列(Hash)的算法。这里所用的算法是:根据物理页面号中的Bit0将所有的物理页面号分成两个集合,如果物理页面号的Bit10为1就从(Page%1024)开始向上搜索,搜索到HYPERSPACE的上沿以后再折回到HYPERSPACE,的下沿,继续向上搜索:Bit0为0则相反,即从(Page % 1024)开始向下搜索。
对虚拟页面是否空闲的判定是通过InterockedCompareExchange()进行的,这个函数提供一种原子的“比较并交换”操作。以InterlockedCompareExchange(Pte,Entry,0)为例,这里Pe是个PLONG指针,0是比较的目标,Entry是要写入Pte的值,这个函数先比较Pte所指是否为0,如果是就将Entry 写入 Pte 所指的 32 位字并返回 Pte 原来的值;否则就只是返回 Pte所指的值。所有这些操作一气呵成,事实上是只由一条指令实现的,所以是不可中断不可分割的原子操作。
因此,一旦发现某个虚拟页面空闲,这个页面的映射表项即已被设置成所需的内容,下面所需要的就是通过_invlpg(Address)冲刷该表项在TLB中的映像,并通过指针 Address 返回虚拟页面的起始地址了。
前面代码中所引用的宏操作PFN_TO_PTE和ADDR_TO_PTE 分别定义为:
#define PFN_TO_PTE(X) ((X) << PAGE_SHIFT)
#define ADDR_TO_PTE(v) (PULONG)(PAGETABLE_MAP + ((((ULONG)(v) / 1024))&(~0x3)))
注意由MmCreateHyperspaceMapping()创建的HYPERSPACE页面映射只是临时的,使用了以后就要加以释放,一般都是通过 MmDeleteHyperspaceMapping()完成。
了解了Hyperspace 的映射,我们再来看一个运用Hyperspace 映射的实例,即函数 MmGetPageTableForProcess()
如前所述,每个进程都有自己的页面映射,因而都有自己的页面映射表。每当调度一个进程运行时,就将其映射表的物理地址装入CPU的控制寄存器CR3,因为MMU是通过物理地址访问页面映射表的。另一方面,如前所述,CPU本身(ALU)却只能通过虚拟地址访问页面映射表,而本进程的页面映射表总是在0xc0000000的地方。可是,如果要访问的是另一个进程的页面映射表,那该怎么办呢?
给定一个目标进程以及这个进程用户空间的一个地址,MmGetPageTableForProcess()将这个地址所属的二级页面表映射到Hyperspace,并返回指向其所在页面(在目标进程页面映射表中)的PTE的指针。这个函数名有些误导,容易使人以为是返回指向整个页面映射表的指针,而实际上却是指向具体表项的指针。
MmGetPageTableForProcess
static PULONG
MmGetPageTableForProcess(PEPROCESS Process, PVOID Address, BOOLEAN Create)
{
ULONG PdeOffset = ADDR_TO_PDE_OFFSET(Address);//计算页面目录项下标
NTSTATUS Status;
PFN_TYPE Pfn;
ULONG Entry;
PULONG Pt, PageDir;
if (Address < MmSystemRangeStart && Process && Process != PsGetCurrentProcess())
{
PageDir = MmCreateHyperspaceMapping(PTE_TO_PFN(Process->Pcb.DirectoryTableBase.QuadPart));
if (PageDir == NULL)
{
KEBUGCHECK(0);
}
if (0 == InterlockedCompareExchangeUL(&PageDir[PdeOffset], 0, 0))
{/所属的页面目录项空白
if (Create == FALSE)
{
MmDeleteHyperspaceMapping(PageDir);
return NULL;
}
//分配空白物理页面
Status = MmRequestPageMemoryConsumer(MC_NPPOOL, FALSE, &Pfn);
if (!NT_SUCCESS(Status) || Pfn == 0)
{
KEBUGCHECK(0);
}
//使所属的目录项指向这个物理页面
Entry = InterlockedCompareExchangeUL(&PageDir[PdeOffset], PFN_TO_PTE(Pfn) | PA_PRESENT | PA_READWRITE | PA_USER, 0);
if (Entry != 0)
{
MmReleasePageMemoryConsumer(MC_NPPOOL, Pfn);
Pfn = PTE_TO_PFN(Entry);
}
}
else
{
Pfn = PTE_TO_PFN(PageDir[PdeOffset]);//获取二级映射表所在的物理页面号
}
MmDeleteHyperspaceMapping(PageDir);//删除页面目录表的临时映射
Pt = MmCreateHyperspaceMapping(Pfn);//为二级映射表建立临时映射
if (Pt == NULL)
{
KEBUGCHECK(0);
}
return Pt + ADDR_TO_PTE_OFFSET(Address);//返回指向映射表项的指针
}
}
参数 Process 指向目标进程的 EPROCESS数据结构。如果参数Create为TRUE,就表示如果给定的地址尚无映射则为其创建一个映射。
这个函数的代码分成两部分,第一部分处理的是最为一般化的情况,即目标进程并非当前进程自身,并且目标地址在其用户空间(Address<MmSystemRangeStart)。而第二部分处理的则是当前进程自身或者目标地址在系统空间的情况。搞清了第一部分的代码,后一部分也就不难了。
如前所述,页面映射表是个二层的结构,其顶层是页面目录,根据给定的地址可以从页面目录中找到其页面映射表项在哪一个二级映射表中。每个二级映射表的大小是一个4KB页面,页面目录的大小也是 4KB。
代码中先通过宏操作ADDR_TO_PDE_OFFSET计算虚拟地址Address所属目录表项在页面目录中的位置。这个宏操作的定义如下:
#define ADDR_TO_PDE_OFFSET(v) ((((ULONG)(v)) / (1024 * PAGE_SIZE)))
页面目录中有1024个目录项,每个目录项占4个字节,所以页面目录占一个页面,即4KB。每个目录项只要不是空白就通过物理页面号指向一个物理页面,这就是一个二级页面表,每个二级页面表也各有1024个表项,其占一个页面,每个表项定义了一个页面的映射和保护模式。
所以,ADDRTO_PDEOFFSETO)计算给定的地址落在哪一个目录项中,所得到的是这个目录项在目录表中的下标。除以(1024*PAGE_SIZE)跟右移 22位的效果是一样的。换言之就是取虚拟地址的高10位为页面目录中的下标。
可是目标进程的页面映射目录在哪里呢?“进程控制块”EPROCESS中有个成分
#define PTE_TO_PFN(X) ((X) >> PAGE_SHIFT)
DirectoryTableBase 记录着该进程的页面映射表,实际上是页面目录所在的物理地址。物理地址的数据类型为LARGE_INTEGER,所以字段名 DirectoryTableBase后面加上了后缀.QuadPart,表示将其看做64位整数LONGLONG。知道了页面目录表所在的物理地址以后,还要通过宏操作PTETO PFNO)将其转换成物理页面号:
这里的PAGE_SHIFT定义为12,将物理地址右移12位,就成了物理页面号PFN。但是这只是个物理页面号,必须将其映射到当前进程的虚拟地址空间才能访问这个页面。然而目标进程并非当前进程,其页面目录在当前进程的页面映射中一般是无映射的,这时候就需要建立一个临时映射。需要用到MmCreateHyperspaceMapping()了。
为目标进程的页面目录表建立了临时映射,就可以读出目标地址所属的目录项,而目录项又指向目标地址所属的二级页面映射表,于是再为该二级页面映射表建立临时映射。
注意这里并未删除为二级页面表创建的临时映射,因为MmGetPageTableForProcess()所返回的是映射表项的指针,这个函数的调用者势必还要访问这个映射表项。由此可见,MmGetPageTableForProcessO)的调用者承担着删除临时映射的责任。
下面一篇文章看如何获取本进程当前的页面映射表象