系列文章目录
3.1.4 Hyperspace 的临时映射
再看如何获取本进程即当前进程本身的页面映射表项,或者获取系统空间的页面映射表项。这时候就不用为目标进程的页面映射表建立临时映射了,因为页面映射表在本进程的虚存空间总是有映射的,而且所占的位置也固定不变总是PAGETABLEMAP,这个常数定义为
#define PAGETABLE_MAP (0xc0000000)
从PAGETABLE_MAP即0xc0000000开始,直到0xc03ffFf的4M字节,是个一维数组,这就是整个4GB虚存空间的页面(映射)表的大小。
CPU 的页面映射机制之所以要采用两层的结构,而不采用单层的页面映射表,主要是出于经济上的考虑。要是光从MMU映射的角度看,采用单层的映射本来也是可以的。那就是:用线性地址Address的高20位即Address/1024作为下标,在一个大小为1M(即1024*1024)的数组中获取目标页面的映射表项。显然,这样一个一维数组占地1024个页面,因为每个页面有1024个表项。但是,那样一来页面映射表的大小就必须是不折不扣的IM个表项,十足占1024个页面即 4MB 的物理空间,而其中有许多页面其实是空白不用的。对于价格还比较昂贵的物理内存而言,这显然是不经济的。相比之下,如果采用两层的结构,凡是空白的目录项就无须为其配备用做二级映射表的物理页面,这样就可以省去许多物理页面。可是,物理页面是一回事,虚存页面是另一回事,虚存页面几乎是“得来全不费功夫”的。所以,如果要为一个进程的页面映射表创建映射,在虚存空间就大可采用单层的、大小为1M的连续的数组,占1024个虚存页面就1024个虚存页面。
如前所述,在这1024个虚存页面中,有一个页面是特殊的,这个页面的1024个映射表项分别指向从PAGETABLEMAP开始的1024个页面(其中之一就是这个页面本身)。由于这1024个页面都是二级页面表,这个特殊的页面实际上就是页面目录。事实上,页面目录项与页面映射表项的格式是一样的。那么哪一个页面是特殊的呢?这要看把这1024个连续的页面放在什么地方。如果放在地址为0的地方,那么第一个页面就是特殊的,就是页面目录。余可类推。
现在,既然把整个页面映射表放在0xc0000000即PAGETABLEMAP的地方,那么这个特殊页面的起始地址就是(0xc0000000+(PAGETABLEMAP/(1024))):
#define PAGEDIRECTORY_MAP (0xc0000000 + (PAGETABLE_MAP / (1024)))
倘若给定一个地址v,则该地址所属(映射表的)目录项所在的地址为:
#define ADDR_TO_PDE(v) (PULONG)(PAGEDIRECTORY_MAP + \
((((ULONG)(v)) / (1024 * 1024))&(~0x3)))
而这个地址所在页面的映射表项的地址为:
#define ADDR_TO_PTE(v) (PULONG)(PAGETABLE_MAP + ((((ULONG)(v) / 1024))&(~0x3)))
明白了这些,下面的代码就容易理解了:
PageDir = ADDR_TO_PDE(Address);
if (0 == InterlockedCompareExchangeUL(PageDir, 0, 0))
{
if (Address >= MmSystemRangeStart)
{
if (0 == InterlockedCompareExchangeUL(&MmGlobalKernelPageDirectory[PdeOffset], 0, 0))
{
if (Create == FALSE)
{
return NULL;
}
Status = MmRequestPageMemoryConsumer(MC_NPPOOL, FALSE, &Pfn);
if (!NT_SUCCESS(Status) || Pfn == 0)
{
KEBUGCHECK(0);
}
Entry = PFN_TO_PTE(Pfn) | PA_PRESENT | PA_READWRITE;
if (Ke386GlobalPagesEnabled)
{
Entry |= PA_GLOBAL;
}
if(0 != InterlockedCompareExchangeUL(&MmGlobalKernelPageDirectory[PdeOffset], Entry, 0))
{
MmReleasePageMemoryConsumer(MC_NPPOOL, Pfn);
}
}
(void)InterlockedExchangeUL(PageDir, MmGlobalKernelPageDirectory[PdeOffset]);
}
else
{
if (Create == FALSE)
{
return NULL;
}
Status = MmRequestPageMemoryConsumer(MC_NPPOOL, FALSE, &Pfn);
if (!NT_SUCCESS(Status) || Pfn == 0)
{
KEBUGCHECK(0);
}
Entry = InterlockedCompareExchangeUL(PageDir, PFN_TO_PTE(Pfn) | PA_PRESENT | PA_READWRITE | PA_USER, 0);
if (Entry != 0)
{
MmReleasePageMemoryConsumer(MC_NPPOOL, Pfn);
}
}
}
return (PULONG)ADDR_TO_PTE(Address);
首先找到虚拟地址 Address所属的目录项,如果这个目录项空白就为其分配一个物理页面作为二级页面映射表的载体,并相应修改目录项。具体的操作则又因Address的性质而异。如果Address是个系统空间的地址,则页面映射的变化首先要反映在全局的内核映射目录MmGlobalKermeIPageDirectory,然后再修改当前进程页面目录中的相应目录项。如果 Address 是个用户空间的地址,那就简单了,因为用户空间的地址是当前进程所独享的。
至此,地址Address所属的目录项已经不为空白,相应二级页面映射表的页面已经到位,ADDRTO_PTE(Address)就是这个地址所属页面映射表项所在的地址。当然,这个映射表项还有可能是空白的,但那是MmGetPageTableForProcessO的调用者的事了。
在前面的代码中我们看到,如果内核即系统空间的映射有所变化,则该变化首先是反映在内核映射目录 MmGlobalKermelPageDirectory]中,然后才复制到当前目录的页面目录中,这是因为所有进程的系统空间基本上都是公共的,只有PAGETABLE_MAP和HYPERSPACE这两块地方例外。所以,内核中提供了一个通用的函数MmUpdatePageDir(),用来比较和更新一个进程的页面目录。给定一个地址区间,该函数将目标进程页面目录中的相关目录项与内核映射目录中的对应目录项进行比较,如果不同就从内核映射目录复制过来,以更新目标进程的页面目录。
MmUpdatePageDir
VOID
NTAPI
MmUpdatePageDir(PEPROCESS Process, PVOID Address, ULONG Size)
{
ULONG StartOffset, EndOffset, Offset;
if (Address < MmSystemRangeStart)
{
KEBUGCHECK(0);
}
if (Ke386Pae)
{
PULONGLONG PageDirTable;
PULONGLONG Pde;
ULONGLONG ZeroPde = 0LL;
ULONG i;
for (i = PAE_ADDR_TO_PDTE_OFFSET(Address); i <= PAE_ADDR_TO_PDTE_OFFSET((PVOID)((ULONG_PTR)Address + Size)); i++)
{
if (i == PAE_ADDR_TO_PDTE_OFFSET(Address))
{
StartOffset = PAE_ADDR_TO_PDE_PAGE_OFFSET(Address);
}
else
{
StartOffset = 0;
}
if (i == PAE_ADDR_TO_PDTE_OFFSET((PVOID)((ULONG_PTR)Address + Size)))
{
EndOffset = PAE_ADDR_TO_PDE_PAGE_OFFSET((PVOID)((ULONG_PTR)Address + Size));
}
else
{
EndOffset = 511;
}
if (Process != NULL && Process != PsGetCurrentProcess())
{
PageDirTable = MmCreateHyperspaceMapping(PAE_PTE_TO_PFN(Process->Pcb.DirectoryTableBase.QuadPart));
Pde = (PULONGLONG)MmCreateHyperspaceMapping(PTE_TO_PFN(PageDirTable[i]));
MmDeleteHyperspaceMapping(PageDirTable);
}
else
{
Pde = (PULONGLONG)PAE_PAGEDIRECTORY_MAP + i*512;
}
for (Offset = StartOffset; Offset <= EndOffset; Offset++)
{
if (i * 512 + Offset < PAE_ADDR_TO_PDE_OFFSET(PAGETABLE_MAP) || i * 512 + Offset >= PAE_ADDR_TO_PDE_OFFSET(PAGETABLE_MAP)+4)
{
(void)ExfInterlockedCompareExchange64UL(&Pde[Offset], &MmGlobalKernelPageDirectoryForPAE[i*512 + Offset], &ZeroPde);
}
}
MmUnmapPageTable((PULONG)Pde);
}
}
else
{
PULONG Pde;
StartOffset = ADDR_TO_PDE_OFFSET(Address);
EndOffset = ADDR_TO_PDE_OFFSET((PVOID)((ULONG_PTR)Address + Size));
if (Process != NULL && Process != PsGetCurrentProcess())
{
Pde = MmCreateHyperspaceMapping(PTE_TO_PFN(Process->Pcb.DirectoryTableBase.u.LowPart));
}
else
{
Pde = (PULONG)PAGEDIRECTORY_MAP;
}
for (Offset = StartOffset; Offset <= EndOffset; Offset++)
{
if (Offset != ADDR_TO_PDE_OFFSET(PAGETABLE_MAP))
{
(void)InterlockedCompareExchangeUL(&Pde[Offset], MmGlobalKernelPageDirectory[Offset], 0);
}
}
if (Pde != (PULONG)PAGEDIRECTORY_MAP)
{
MmDeleteHyperspaceMapping(Pde);
}
}
}
这段代码就留给读者自己阅读了