系列文章目录
文章目录
3.1.5 系统空间的映射
如前所述,所有进程的系统空间基本上都是公共的,只有PAGETABLE_MAP和HYPER_SPACE这两块地方例外。所以,每个进程的页面映射表实际上分成两大部分。一部分即系统空间是公共的,其内容来自全局的内核映射表,这一部分的页面映射是常驻的,不受进程切换的影响。另一部分即用户空间则是局部于特定进程的,进程一切换,这一部分页面映射就受到冲刷。为了更好地理解这一点,我们来看一下进程的创建与切换。
每当创建一个进程的时候,内核通过MmCopyMmInfo()为其从内核映射表复制系统空间的映射。
从函数 PspCreateProcess()的代码中可以看出一个进程的页面映射表的来源,PspCreateProcess()当然是个相当复杂的函数,但是与内存管理及页面映射表有关的代码不过寥寥数行:
PspCreateProcess()
NTSTATUS
NTAPI
PspCreateProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess OPTIONAL,
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL,
IN BOOLEAN InJob)
{
...
PHYSICAL_ADDRESS DirectoryTableBase = {{0}};
...
/* Set Process's Directory Base */
MmCopyMmInfo(Parent ? Parent : PsInitialSystemProcess,
Process,
&DirectoryTableBase);
....
/* Create the Process' Address Space */
Status = MmCreateProcessAddressSpace(Process,
(PROS_SECTION_OBJECT)SectionObject,
&Process->SeAuditProcessCreationInfo.
ImageFileName);
...
}
从表面上看,MmCopyMmInfo0)似乎是从父进程复制有关存储管理的信息到这里的DirectoryTableBase 数据结构中,也许设计这个函数时的初衷也确实如此,但是实际的实现却与父进程毫无关系,而只是从内核映射表MmGlobalKernelPageDirectory]复制,并为PAGETABLE_MAP和HYPER_SPACE各自分配一个目录页面。
MmCopyMmInfo()
NTSTATUS
STDCALL
MmCopyMmInfo(PEPROCESS Src,
PEPROCESS Dest,
PPHYSICAL_ADDRESS DirectoryTableBase)
{
PFN_TYPE Pfn[7];
Count = Ke386Pae ? 7 : 2;
for (i = 0; i < Count; i++)
{
//分配所需得物理页面
Status = MmRequestPageMemoryConsumer(MC_NPPOOL, FALSE, &Pfn[i]);
if (!NT_SUCCESS(Status))
........
}
}
if (Ke386Pae)
{
........
}
else
{
PULONG PageDirectory;
PageDirectory = MmCreateHyperspaceMapping(Pfn[0]);
memcpy(PageDirectory + ADDR_TO_PDE_OFFSET(MmSystemRangeStart),
MmGlobalKernelPageDirectory + ADDR_TO_PDE_OFFSET(MmSystemRangeStart),
(1024 - ADDR_TO_PDE_OFFSET(MmSystemRangeStart)) * sizeof(ULONG));
DPRINT("Addr %x\n",ADDR_TO_PDE_OFFSET(PAGETABLE_MAP));
PageDirectory[ADDR_TO_PDE_OFFSET(PAGETABLE_MAP)] = PFN_TO_PTE(Pfn[0]) | PA_PRESENT | PA_READWRITE;
PageDirectory[ADDR_TO_PDE_OFFSET(HYPERSPACE)] = PFN_TO_PTE(Pfn[1]) | PA_PRESENT | PA_READWRITE;
MmDeleteHyperspaceMapping(PageDirectory);
}
DirectoryTableBase->QuadPart = PFN_TO_PTE(Pfn[0]);
DPRINT("Finished MmCopyMmInfo(): %I64x\n", DirectoryTableBase->QuadPart);
return(STATUS_SUCCESS);
}
先在 for 循环中通过 MmRequestPageMemoryConsumer()分配若干物理页面,对于常规的 32 位物理地址模式是两个页面,准备分别用于PAGETABLE_MAP和HYPER_SPACE两个区域的映射。注意参数MC_NPPOOL表明这两个页面是不允许被倒换出去的。
然后先为分配到的第一个页面建立起Hyperspace映射(否则便无法访问该页面),然后就通过memcpy()从内核映射表复制,复制的范围是从MmSystemRangeStart即0x80000000 开始,长度为自此以上全部页面的目录项。但是注意这里有两个目录项是特殊的。
第一个是地址PAGETABLE_MAP所属的目录项,这个目录项应该指向页面目录所在的页面,原来当然是指向 MmGlobalKemelPageDirectory[]里面的某个页面,但是现在则要使其指向子进程的页面目录所在的页面,就是刚才所分配的页面Pfn[0],所以要修改这个目录项。
第二个是地址HYPER_SPACE所属的目录项,这个目录项应该指向Hyperspace的二级映射表所在的物理页面(正好占一个页面),Hyperspace虽然在系统空间,其实际的映射却是因进程而异的所以前面也为之分配了一个物理页面Pfn[1],现在当然也要修改这个目录项。
回到前面 PspCreateProcess()的代码,下一步是对于KelnitializeProcess()的调用。这个函数需要完成许多操作,但是我们此刻所关心的只是对KPROCESS数据结构中成分DirectoryTableBase的设置。
KeInitializeProcess()
VOID
NTAPI
KeInitializeProcess(IN OUT PKPROCESS Process,
IN KPRIORITY Priority,
IN KAFFINITY Affinity,
IN PLARGE_INTEGER DirectoryTableBase,
IN BOOLEAN Enable)
{
....
Process->DirectoryTableBase = *DirectoryTableBase;
....
}
把参数 DirectoryTableBase 所指的内容复制到结构成分 DirectoryTableBase 中,如此而已。这个结构成分的类型是大整数,这是因为考虑到64位地址或36位地址的需要,我们现在则只关心32位。指针DirectoryTableBase所指的内容是什么呢?就是子进程页面映射表的起点(物理地址)。显然,这个页面映射表的系统空间部分来自内核映射表,而用户空间部分此刻尚是空白。
此外,PspCreateProcess()中还调用了一个函数MmCreateProcessAddressSpace(),这个函数处理的是用户空间的地址管理,首先是其MADDRESS_SPACE数据结构和空白的AVL树。这里就不多说了。
那么MmGlobalKernelPageDirectory[]中的信息又是从哪里来的呢?是在系统初始化的时候创建的。
MmInitGlobalKernelPageDirectory()
VOID
INIT_FUNCTION
NTAPI
MmInitGlobalKernelPageDirectory(VOID)
{
ULONG i;
DPRINT("MmInitGlobalKernelPageDirectory()\n");
if (Ke386Pae)
{
...
}
else
{
PULONG CurrentPageDirectory = (PULONG)PAGEDIRECTORY_MAP;
for (i = ADDR_TO_PDE_OFFSET(MmSystemRangeStart); i < 1024; i++)
{
if (i != ADDR_TO_PDE_OFFSET(PAGETABLE_MAP) &&
i != ADDR_TO_PDE_OFFSET(HYPERSPACE) &&
0 == MmGlobalKernelPageDirectory[i] && 0 != CurrentPageDirectory[i])
{
MmGlobalKernelPageDirectory[i] = CurrentPageDirectory[i];//copy
if (Ke386GlobalPagesEnabled)//如果支持全局页面的话
{
MmGlobalKernelPageDirectory[i] |= PA_GLOBAL;//加上全局标志
CurrentPageDirectory[i] |= PA_GLOBAL;
}
}
}
}
}
PAGEDIRECTORY_MAP就是
#define PAGEDIRECTORY_MAP (0xc0000000 + (PAGETABLE_MAP / (1024)))
#define PAGETABLE_MAP (0xc0000000)
这是初始化进程的页面映射表,其内容是系统初始化的产物。对于系统空间的页面映射,除PAGETABLE_MAP和HYPERSPACE 两个页面外全部照抄(除非初始化进程另有映射的页面)。如果要支持全局页面,则还要在用于系统空间映射的表项中加上全局页面标志位PA_GLOBAL。凡是缓存在 TLB 中的映射表项,只要这个标志位为1就不受整体的冲刷。前面讲过,进程切换时要冲刷用户空间的页面映射。但是,如果要一个表项一个表项地冲刷,效率就太低了。所以CPU提供了冲刷整个TLB的手段,每当改变控制寄存器 CR3的内容时,CPU 就会冲刷整个TLB。可是那样一来又把系统空间的映射也给冲刷掉了,这也不好。PA_GLOBAL标志位就是为解决这个问题而设的,有了这个标志位就可以两全其美了。
#define PA_BIT_GLOBAL (8)
#define PA_GLOBAL (1 << PA_BIT_GLOBAL)