页面异常
在为交割而分配一个区间时,区块的类型是MEM_COMMIT,此后这个区间就可以被访问了。但是,所谓“交割”也只是逻辑上的操作,并未落实到实际的物理页面的映射。于是,用户空间的程序在系统调用成功返回后就可能访问这个区间,然而一访问就因缺页异常而发生页面异常,因为此时实际上尚未建立起物理页面的映射。
发生页面异常时,内核走过类似于中断的过程,因异常的原因为页面异常而进入页面异常处理程序MmAccessFault(),这个函数的地位类似于中断处理程序。
MmAccessFault
NTSTATUS
NTAPI
MmAccessFault(IN BOOLEAN StoreInstruction,
IN PVOID Address,
IN KPROCESSOR_MODE Mode,
IN PVOID TrapInformation)
{
/* Cute little hack for ROS */
if ((ULONG_PTR)Address >= (ULONG_PTR)MmSystemRangeStart)//地址落在系统空间
{
/* Check for an invalid page directory in kernel mode */
if (Mmi386MakeKernelPageTableGlobal(Address))//更新页目录表
{
/* All is well with the world */
return STATUS_SUCCESS;
}
}
/* Keep same old ReactOS Behaviour */
if (StoreInstruction)
{
/* Call access fault */ //如果是因为越权,那么发往这个处理例程
return MmpAccessFault(Mode, (ULONG_PTR)Address, TrapInformation ? FALSE : TRUE);
}
else
{
/* Call not present *///如果因为该页面不存在,那么会发往这个例程
return MmNotPresentFault(Mode, (ULONG_PTR)Address, TrapInformation ? FALSE : TRUE);
}
}
参数StoreInstruction是表示异常原因的布尔量,为0表示是缺页,否则表示是越权导致的页面异常,页面异常代码是由CPU自动压入堆栈的。发生异常时,CPU一方面根据异常种类产生中断向量,使控制转向相应的异常响应程序入口。
当内核的映射出现变动时,该变动首先反映在全局的内核映射表上,然后才是反映到当前进程的映射表中。如果在这中间发生了访问,而映射在全局的内核映射表上的内容还未来得及反映到当前进程的映射表,那么就可能发生异常。所以此时要通过内核映射表来更新当前进程的映射表,就可以重试引起异常的访问了,这里的Mmi386MakeKernelPageTableGlobal就是来检查是否属于这种情况,并更新当前进程的映射表。这个函数返回TRUE自然万事大吉。
MmNotPresentFault
发生页面异常的原因大体分为两大类:一类由于违反了页面的保护模式,这是因访问权限不足引起的;另一类是因缺页引起的。
看下MmNotPresentFault
NTSTATUS
NTAPI
MmNotPresentFault(KPROCESSOR_MODE Mode,
ULONG_PTR Address,
BOOLEAN FromMdl)
{
PMADDRESS_SPACE AddressSpace;
MEMORY_AREA* MemoryArea;
NTSTATUS Status;
BOOLEAN Locked = FromMdl;
PFN_TYPE Pfn;
DPRINT("MmNotPresentFault(Mode %d, Address %x)\n", Mode, Address);
if (KeGetCurrentIrql() >= DISPATCH_LEVEL)//如果中断级别大于dispatch_level说明中断有问题,因为这种中断级别下是不会产生异常的
{
CPRINT("Page fault at high IRQL was %d, address %x\n", KeGetCurrentIrql(), Address);
return(STATUS_UNSUCCESSFUL);
}
if (PsGetCurrentProcess() == NULL)//若是当前进程不存在 允许这种情况
{
/* Allow this! It lets us page alloc much earlier! It won't be needed
* after my init patch anyways
*/
DPRINT("No current process\n");
if (Address < (ULONG_PTR)MmSystemRangeStart)
{
return(STATUS_ACCESS_VIOLATION);
}
}
/*
* Find the memory area for the faulting address
*/
if (Address >= (ULONG_PTR)MmSystemRangeStart)//若发生在系统空间
{
/*
* Check permissions
*/
if (Mode != KernelMode)//check先前模式
{
CPRINT("Address: %x\n", Address);
return(STATUS_ACCESS_VIOLATION);
}
AddressSpace = MmGetKernelAddressSpace();//获得系统空间的数据结构
}
else//说明发生在用户空间
{
AddressSpace = (PMADDRESS_SPACE)&(PsGetCurrentProcess())->VadRoot;//获得用户空间的数据结构
}
if (!FromMdl)
{
MmLockAddressSpace(AddressSpace);
}
/*
* Call the memory area specific fault handler
*/
do
{
MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, (PVOID)Address);//获得该空间发生异常位置的区间
if (MemoryArea == NULL || MemoryArea->DeleteInProgress)//区间尚未分配,或被删除
{
if (!FromMdl)
{
MmUnlockAddressSpace(AddressSpace);
}
return (STATUS_ACCESS_VIOLATION);
}
switch (MemoryArea->Type)//根据区间的类型采取不同措施
{
case MEMORY_AREA_PAGED_POOL://可置换池区间
{
Status = MmCommitPagedPoolAddress((PVOID)Address, Locked);
break;
}
case MEMORY_AREA_SYSTEM://系统区间
Status = STATUS_ACCESS_VIOLATION;
break;
case MEMORY_AREA_SECTION_VIEW:
Status = MmNotPresentFaultSectionView(AddressSpace,
MemoryArea,
(PVOID)Address,
Locked);
break;
case MEMORY_AREA_VIRTUAL_MEMORY:
case MEMORY_AREA_PEB_OR_TEB:
Status = MmNotPresentFaultVirtualMemory(AddressSpace,
MemoryArea,
(PVOID)Address,
Locked);
break;
case MEMORY_AREA_SHARED_DATA:
Pfn = MmSharedDataPagePhysicalAddress.LowPart >> PAGE_SHIFT;
Status =
MmCreateVirtualMapping(PsGetCurrentProcess(),
(PVOID)PAGE_ROUND_DOWN(Address),
PAGE_READONLY,
&Pfn,
1);
break;
default:
Status = STATUS_ACCESS_VIOLATION;
break;
}
}
while (Status == STATUS_MM_RESTART_OPERATION);
DPRINT("Completed page fault handling\n");
if (!FromMdl)
{
MmUnlockAddressSpace(AddressSpace);
}
return(Status);
}
函数流程很简单,首先定位发生异常的区间,然后若该区间被分配并且不处于删除阶段,则根据区间的类型发往不同的handler去。直到状态码为STATUS_MM_RESTART_OPERATION为止。
MmNotPresentFaultVirtualMemory
对于一般的虚存区间,其类型是MEMORY_AREA_VIRTUAL_MEMORY。所做的反应是MmNotPresentFaultVirtualMemory。
//MmNotPresentFault->MmNotPresentFaultVirtualMemory
函数流程是
先判断其页面对应的present位是否为1,若为1则不需要再干别的事了。如果该页处于删除阶段,则表明这个缺页问题是正常的,无法恢复。接着寻找其所属区块。得到返回的区块后,若区块的类型还未commit,或者其保护模式设置为不可访问,那么自然不可访问。接着调用MmGetOp,用来创建或者获取一个PageOp,用来处理页面的某些操作。比对该PageOp的线程是否是本线程,若不是则要等待别的线程先完成他们在执行的操作,即通过KeWaitForSingleObject来等待完成事件被设置。然后假若出现了某种奇怪的BUG,则直接蓝屏(该线程:喵喵喵???)。还要比较OpType是否是MM_PAGEOP_PAGEIN,若不是则发回重来。
MmGetPageOp
看下得到页面操作结构的过程
根据这块区间的类型是否是section_view类型,计算出一个Hash值。然后根据Hash值找到相应的队列,接着在队列中搜索,根据之前的类型判断是否有一样的PageOp数据结构。
如果发现了针对同一页面的操作即PageOp是同一个,那么我们将这个针对于同一页面的PageOp的引用计数加1,接着返回。若并没有针对同一个页面的Op,那么调用ExAllocateFromNPagedLookasideList创建一个PageOp,如果不是section_view类型,那么就将用传进来的pid和address赋予新创建的PageOp,否则就是segment和offset赋值给PageOp。接着为新创建的PageOp初始化的数据,并将PageOp入MmPageOpHashTable,插入到队列最开头的位置。然后返回该PageOp结构。
回到MmNotPresentFaultVirtualMemory的代码,如果从MmGetPageOp返回的PageOp->Thead不是本身,那说明是其他线程在对同一界面进行操作,我们自然是不能破坏的,但是可能针对这一页面的不是同一操作,所以我们就得返回一个RESTART_OPERATION操作循环来等待其他线程完成他们的工作。
如果没有别的线程在进行同一页面的操作,则当前线程就是该页面操作的启动者,所以当前线程承担着完成此项操作的责任,所以继续看下面的MmNotPresentFaultVirtualMemory。、
MmNotPresentFaultVirtualMemory第二部分
PageOp的工作如下:
- 轮到PageOp处理NotPresent,首先会尝试分配一个物理页面,若物理页面不足,自然要等待分配。
- 若该地址对应的pte项是一个SwapEntry,说明缺页是因为将该虚存页面置换到外存上了,那么如果设这样的话,那么会先解除虚存页面跟置换文件的映射,从而获得置换文件号,然后调用MmReadFromSwapPage将置换文件读入我们刚分配的物理页面上。最后将置换文件号保存在物理页面里,供以后使用,接着返回。
- 若不是因为虚存页面对应的pte是个SwapEntry,则说明发生本次页面异常之前相应的页面映射表项为0,说明这是一个全新的页面,所以只要建立一个映射即可,若因为空间不足的原因无法有空闲物理页可供建立映射,则一直等待。最后成功建立映射后调用MmInsertRmap将该页面加入进程的working set,然后设置事件完成,重新执行本条指令。
MmReadFromSwapPage
看看对于被倒换到外存的页面,MmReadFromSwapEntry是如何处理的
函数流程还是很清晰的
- 首先从SwapEntry中获取文件号和文件的页面号(是以页面为单位的位移)。
- 接着检测文件号和PagingFile队列对应的文件号是否合法,若不合法说明出现了严重错误。
- 实际的文件偏移要乘上PAGE_SIZE才是真正的文件偏移,然后调用MmGetOffsetPageFile获取在交换文件中的偏移。
- 最后设置一个事件用于等待真正的文件读取,通过IoPageRead,将对应的页面文件对象和读取的文件偏移传进去等待IoReadPage完成。
页面的换出
对有映射的页面已经被倒换到磁盘上即倒换文件中,那么对这个页面的访问就会引起一次缺页异常,相应的异常处理程序负责将页面读入到物理页面中,然后建立跟物理页面的映射。这自然会产生一个问题:这个页面是什么时候到磁盘上的。我们之前虽然讲过跟外存文件建立映射,但是对于虚存页面的数据置换到外存的操作是没讲到的。这个置换的行为主体其实也就是MiBalancerThread
VOID STDCALL
MiBalancerThread(PVOID Unused)
{
PVOID WaitObjects[2];
NTSTATUS Status;
ULONG i;
ULONG NrFreedPages;
ULONG NrPagesUsed;
ULONG Target;
BOOLEAN ShouldRun;
WaitObjects[0] = &MiBalancerEvent;
WaitObjects[1] = &MiBalancerTimer;
while (1)
{
Status = KeWaitForMultipleObjects(2, //这个线程一般都会等待其他线程唤醒它,这里等待多个线程,在这之前它是处于睡眠状态的
WaitObjects,
WaitAny,
Executive,
KernelMode,
FALSE,
NULL,
NULL);
if (Status == STATUS_SUCCESS)//若因为空闲页面不足调用balance函数
{
/* MiBalancerEvent */
CHECKPOINT;
while (MmStats.NrFreePages < MiMinimumAvailablePages + 5)//如果总空闲页面的数量接近最低阈值了 则需要调用相应的修剪函数来对可置换出去的物理页面进行置换操作 一直到能空出五个空闲物理页面为止
{
for (i = 0; i < MC_MAXIMUM; i++)//对于每个用途的Trim函数,都调用一次
{
if (MiMemoryConsumers[i].Trim != NULL)
{
NrFreedPages = 0;
Status = MiMemoryConsumers[i].Trim(MiMinimumPagesPerRun, 0, &NrFreedPages);
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
}
}
}
InterlockedExchange(&MiBalancerWork, 0);
CHECKPOINT;
}
else if (Status == STATUS_SUCCESS + 1)//因超时而被唤醒
{
/* MiBalancerTimer */
ShouldRun = MmStats.NrFreePages < MiMinimumAvailablePages + 5 ? TRUE : FALSE;//判断是否修剪
for (i = 0; i < MC_MAXIMUM; i++)
{
if (MiMemoryConsumers[i].Trim != NULL)//若修剪函数存在
{
NrPagesUsed = MiMemoryConsumers[i].PagesUsed;
if (NrPagesUsed > MiMemoryConsumers[i].PagesTarget || ShouldRun)//若所使用的页面超出了最大阈值,或是空闲页面过少
{
if (NrPagesUsed > MiMemoryConsumers[i].PagesTarget)//选择修剪的次数
{
Target = max (NrPagesUsed - MiMemoryConsumers[i].PagesTarget,
MiMinimumPagesPerRun);
}
else
{
Target = MiMinimumPagesPerRun;
}
NrFreedPages = 0;
Status = MiMemoryConsumers[i].Trim(Target, 0, &NrFreedPages);
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
}
}
}
}
else
{
DPRINT1("KeWaitForMultipleObjects failed, status = %x\n", Status);
KEBUGCHECK(0);
}
}
}
这个线程平时会睡眠,但是会周期性的通过MiBalancerTimer被唤醒。此外,这个线程也可以通过事件MiBalancerEvent,这发生在空闲页面库存不足的情况下。
每当这个线程被唤醒的时候,它就借助若干个“消费者”的“修剪函数”。所谓修剪,也就是将暂时不会被访问的内容倒换出去,腾出所占据的物理页面另行分配。
复习下Consumer都有哪几种类型:
#define MC_CACHE 0
#define MC_USER 1
#define MC_PPOOL 2
#define MC_NPPOOL 3
#define MC_MAXIMUM 4
显然,每种Consumer都应该提供自己的修剪函数;但目前只有MC_CACHE和MC_USER有修剪函数。换言之,另外两种用途的是不让修剪的。用户空间页面所映射的物理页面的修剪函数为MmTrimUserMemory
NTSTATUS
NTSTATUS
MmTrimUserMemory(ULONG Target, ULONG Priority, PULONG NrFreedPages)
{
PFN_TYPE CurrentPage;
PFN_TYPE NextPage;
NTSTATUS Status;
(*NrFreedPages) = 0;
CurrentPage = MmGetLRUFirstUserPage();//获得第一个要修剪的物理页面
while (CurrentPage != 0 && Target > 0)//Target为要修剪的物理页面数量
{
NextPage = MmGetLRUNextUserPage(CurrentPage);//获得下一个要修剪的物理页面
Status = MmPageOutPhysicalAddress(CurrentPage);//将当前页面置换出去
if (NT_SUCCESS(Status))//成功Target--,空闲页面自然也加1
{
DPRINT("Succeeded\n");
Target--;
(*NrFreedPages)++;
}
else if (Status == STATUS_PAGEFILE_QUOTA)//已超过倒换文件配额
{
MmSetLRULastPage(CurrentPage);//下次再来
}
CurrentPage = NextPage;
}
return(STATUS_SUCCESS);
}
修剪的对象自然是最近没有访问的物理页面,相应的算法是LRU(Least Recently Used)。与MiMemoryConsumer[]平行内核中有个UsedPageListHeads[],也是以MC_XX等为下标,凡是分配用于某个消费者的物理页面,其数据结构是按LRU的次序挂在其队列中。
这个修剪函数会按照LRU顺序从UsedPageListHeads队列里每次取一个页面调用MmPageOutPhysicalAddress从而将页面置换出去。
MmPageOutPhysicalAddress
//MiBalanceThread() > MmTrimUserMemory > MmPageOutPhysicalAddress
NTSTATUS
NTAPI
MmPageOutPhysicalAddress(PFN_TYPE Page)//给定一个物理页 置换到外存
{
PMM_RMAP_ENTRY entry;
PMEMORY_AREA MemoryArea;
PMADDRESS_SPACE AddressSpace;
ULONG Type;
PVOID Address;
PEPROCESS Process;
PMM_PAGEOP PageOp;
ULONG Offset;
NTSTATUS Status = STATUS_SUCCESS;
ExAcquireFastMutex(&RmapListLock);
entry = MmGetRmapListHeadPage(Page); //该物理页第一个MM_RMAP_ENTRY结构 用来表示本页面属于哪个进程,以及在这个进程的地址,因为需要将变化告诉该进程的页面映射表
if (entry == NULL || MmGetLockCountPage(Page) != 0)
{
ExReleaseFastMutex(&RmapListLock);
return(STATUS_UNSUCCESSFUL);
}
Process = entry->Process;//获得映射到该页面的进程
Address = entry->Address;//获得该进程的相应地址
if ((((ULONG_PTR)Address) & 0xFFF) != 0)//显然这个Address是跟页面对齐的
{
KEBUGCHECK(0);
}
if (Address < MmSystemRangeStart)//如果地址小于系统空间范围
{
Status = ObReferenceObjectByPointer(Process, PROCESS_ALL_ACCESS, NULL, KernelMode);//计数加1
ExReleaseFastMutex(&RmapListLock);
if (!NT_SUCCESS(Status))
{
return Status;
}
AddressSpace = (PMADDRESS_SPACE)&Process->VadRoot;//获得用户空间结构
}
else//说明该地址在系统空间
{
ExReleaseFastMutex(&RmapListLock);
AddressSpace = MmGetKernelAddressSpace();//获得系统空间结构
}
MmLockAddressSpace(AddressSpace);
MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, Address);//找到对应的区间
if (MemoryArea == NULL || MemoryArea->DeleteInProgress)
{
MmUnlockAddressSpace(AddressSpace);
if (Address < MmSystemRangeStart)
{
ObDereferenceObject(Process);
}
return(STATUS_UNSUCCESSFUL);
}
Type = MemoryArea->Type;//获得区间类型
if (Type == MEMORY_AREA_SECTION_VIEW)//对于共享映射区间
{
Offset = (ULONG_PTR)Address - (ULONG_PTR)MemoryArea->StartingAddress
+ MemoryArea->Data.SectionData.ViewOffset;;
/*
* Get or create a pageop
*/
PageOp = MmGetPageOp(MemoryArea, NULL, 0, //若是共享映射区间 则需要先准备一个操作该物理页面的请求 这次传入的segment和offset
MemoryArea->Data.SectionData.Segment,
Offset, MM_PAGEOP_PAGEOUT, TRUE);
if (PageOp == NULL)
{
MmUnlockAddressSpace(AddressSpace);
if (Address < MmSystemRangeStart)
{
ObDereferenceObject(Process);
}
return(STATUS_UNSUCCESSFUL);
}
/*
* Release locks now we have a page op.
*/
MmUnlockAddressSpace(AddressSpace);
/*
* Do the actual page out work.
*/
Status = MmPageOutSectionView(AddressSpace, MemoryArea,
Address, PageOp);
}
else if ((Type == MEMORY_AREA_VIRTUAL_MEMORY) || (Type == MEMORY_AREA_PEB_OR_TEB))//对于普通的虚存空间而言
{
PageOp = MmGetPageOp(MemoryArea, Address < MmSystemRangeStart ? Process->UniqueProcessId : NULL,
Address, NULL, 0, MM_PAGEOP_PAGEOUT, TRUE);//也需要获得一个将页面置换出去的PageOp操作 但这里传入的是pid和address
if (PageOp == NULL)
{
MmUnlockAddressSpace(AddressSpace);
if (Address < MmSystemRangeStart)
{
ObDereferenceObject(Process);
}
return(STATUS_UNSUCCESSFUL);
}
/*
* Release locks now we have a page op.
*/
MmUnlockAddressSpace(AddressSpace);
/*
* Do the actual page out work.
*/
Status = MmPageOutVirtualMemory(AddressSpace, MemoryArea,//执行页面置换操作
Address, PageOp);
}
else
{
KEBUGCHECK(0);
}
if (Address < MmSystemRangeStart)//对于地址在用户空间内的,需要将引用计数减1
{
ObDereferenceObject(Process);
}
return(Status);
}
可以看到MmPageOutPhysicalAddress要先确定置换页面所在的进程,要通知该进程的页面映射表,然后根据是共享区还是普通虚存空间来决定具体的置换操作,不过两者都有共同点,两者都需要先获得PAGE_OUT的PageOp数据结构才能操纵相应的物理界面,真正的置换函数是MmPageOutVirtualMemory。这个之前我们已经很详细的阐述过了,里面对于已被写入污染和未被写入污染的物理页面采取不同的措施。
共享映射区
注意,这里的共享映射区是针对于用户空间而言的,对于系统空间,自然是所有进程所共享的。对于每一个物理页而言,通常属于一个进程,即只被映射到一个进程的空间里,因此其页面号是进程所“独有”的。但是一个物理页面也可以被映射到多个进程中。由这样的物理页面映射在虚存空间形成的连续区间,称为共享映射区,也称作Section,这里的Section跟我们在pe中说的Section含义是不一样的,那里指的是pe文件中划分不同用途的组块,称为区块,也是Section。
共享映射区的作用有如下:
- 作为一种进程通信的手段,因为不同进程中的某段区间映射到同一物理页面,那么这两个进程可以通过共享映射区进行信息的一种交换
- 第二种也是最普遍,最重要的一个用途就是共享的页面我们可以将它映射到一个磁盘文件,即以该文件为倒换文件!!!这样就可以很方便的将文件或文件的一部分映射进物理内存,当用户需要访问的时候,就可以像访问内存一样方便的直接读写文件,而不再使用“读文件”,“写文件”等系统调用。由于我们可以利用这种用途使得多个进程通过共享映射区的形式来使得多个进程共享一个文件,该文件从而可以同时被多个进程打开和操作。实际应用中,其方便性和重要性远远超过了前者,以至于与其说“共享映射区”,不如说“文件映射区”更为贴切。在实践中,哪怕单个进程独享的文件操作也倾向于采用文件映射区!!!,实际上现在Windows应用软件中对于磁盘文件**已经不太使用“读文件”,“写文件”**等系统调用了。
NtCreateSection
要创建一个文件映射区,自然要**“打开”文件**,获取该文件的“句柄”,再以该句柄作为参数之一调用NtCreateSection。但是句柄不是必需的,当作为NULL传进来时自然就用作进程间的共享内存区。其实,所谓目标文件只是概念上的,只要是可倒换的页面总要有个去处,区别只在于用户是否可见而已。
NTSTATUS STDCALL
NtCreateSection (OUT PHANDLE SectionHandle, //返回的正是得到的共享内存区句柄
IN ACCESS_MASK DesiredAccess, //该块Section所具备的属性
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, //不为NULL时用来作为共享映射区的文件,为NULL时表示用于进程间通信
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection OPTIONAL,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL)
{
LARGE_INTEGER SafeMaximumSize;
PVOID SectionObject;
KPROCESSOR_MODE PreviousMode;
NTSTATUS Status = STATUS_SUCCESS;
PreviousMode = ExGetPreviousMode();//获取先前CPU运行模式
if(MaximumSize != NULL && PreviousMode != KernelMode)//若从用户空间发起的系统调用
{
_SEH_TRY
{
/* make a copy on the stack */
SafeMaximumSize = ProbeForReadLargeInteger(MaximumSize);//参数复制到系统空间
MaximumSize = &SafeMaximumSize;
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
if(!NT_SUCCESS(Status))
{
return Status;
}
}
Status = MmCreateSection(&SectionObject, //它最后调用的是MmCreateSection创建共享内存区
DesiredAccess,
ObjectAttributes,
MaximumSize,
SectionPageProtection,
AllocationAttributes,
FileHandle,
NULL);
if (NT_SUCCESS(Status))//若创建成功
{
Status = ObInsertObject ((PVOID)SectionObject,//将创建的Section对象插入到对象目录和本进程的句柄表
NULL,
DesiredAccess,
0,
NULL,
SectionHandle);
}
return Status;
}
显然这个函数的主体是MmCreateSection来实现最终的映射区的创建
MmCreateSection
调用关系:NtCreateSection->MmCreateSection
/**********************************************************************
* NAME EXPORTED
* MmCreateSection@
*
* DESCRIPTION
* Creates a section object.
*
* ARGUMENTS
* SectionObject (OUT)
* Caller supplied storage for the resulting pointer
* to a SECTION_OBJECT instance;
*
* DesiredAccess
* Specifies the desired access to the section can be a
* combination of:
* STANDARD_RIGHTS_REQUIRED |
* SECTION_QUERY |
* SECTION_MAP_WRITE |
* SECTION_MAP_READ |
* SECTION_MAP_EXECUTE
*
* ObjectAttributes [OPTIONAL]
* Initialized attributes for the object can be used
* to create a named section;
*
* MaximumSize
* Maximizes the size of the memory section. Must be
* non-NULL for a page-file backed section.
* If value specified for a mapped file and the file is
* not large enough, file will be extended.
*
* SectionPageProtection
* Can be a combination of:
* PAGE_READONLY |
* PAGE_READWRITE |
* PAGE_WRITEONLY |
* PAGE_WRITECOPY
*
* AllocationAttributes
* Can be a combination of:
* SEC_IMAGE |
* SEC_RESERVE
*
* FileHandle
* Handle to a file to create a section mapped to a file
* instead of a memory backed section;
*
* File
* Unknown.
*
* RETURN VALUE
* Status.
*
* @implemented
*/
NTSTATUS STDCALL
MmCreateSection (OUT PVOID * Section,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL,
IN PFILE_OBJECT File OPTIONAL)
{
ULONG Protection;
PROS_SECTION_OBJECT *SectionObject = (PROS_SECTION_OBJECT *)Section;
/*
* Check the protection
*/
Protection = SectionPageProtection & ~(PAGE_GUARD|PAGE_NOCACHE);
if (Protection != PAGE_NOACCESS &&//首先检测了共享内存区的Protection是否合法 不合法返回无效的区域保护状态码
Protection != PAGE_READONLY &&
Protection != PAGE_READWRITE &&
Protection != PAGE_WRITECOPY &&
Protection != PAGE_EXECUTE &&
Protection != PAGE_EXECUTE_READ &&
Protection != PAGE_EXECUTE_READWRITE &&
Protection != PAGE_EXECUTE_WRITECOPY)
{
CHECKPOINT1;
return STATUS_INVALID_PAGE_PROTECTION;
}
if (AllocationAttributes & SEC_IMAGE)//对于AllocType为可执行映像文件而言,它是特殊的,因为一个可执行映像会有数量不等的段,所以特殊情形 特殊对待
{
return(MmCreateImageSection(SectionObject,//对于可执行过映像文件 调用MmCreateImageSection来创建共享内存区
DesiredAccess,
ObjectAttributes,
MaximumSize,
SectionPageProtection,
AllocationAttributes,
FileHandle));
}
if (FileHandle != NULL)// 当指定了某个普通文件后 说明这时候创建的是 共享文件区域
{
return(MmCreateDataFileSection(SectionObject, //此时调用MmcreateDataFileSection来为普通文件创建一个共享文件区
DesiredAccess,
ObjectAttributes,
MaximumSize,
SectionPageProtection,
AllocationAttributes,
FileHandle));
}
return(MmCreatePageFileSection(SectionObject,//此时FileHandle == NULL 也就是说此时创建的是供多个进程通信用的内存共享区
DesiredAccess,
ObjectAttributes,
MaximumSize,
SectionPageProtection,
AllocationAttributes));
}
这个函数其实也相当于一个stub,其流程非常简单,就是检测了共享内存区的保护属性是否合法,不合法自然是不给创建的,然后根据AllocType和FileHandle来将创建共享内存区的任务分发给不同的函数。
MmCreateDataFileSection
这个函数是创建的是普通文件的文件映射区,我们这里详细看下。
NTSTATUS
NTAPI
MmCreateDataFileSection(PROS_SECTION_OBJECT *SectionObject,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PLARGE_INTEGER UMaximumSize,
ULONG SectionPageProtection,
ULONG AllocationAttributes,
HANDLE FileHandle)
/*
* Create a section backed by a data file
*/
{
PROS_SECTION_OBJECT Section;
NTSTATUS Status;
LARGE_INTEGER MaximumSize;
PFILE_OBJECT FileObject;
PMM_SECTION_SEGMENT Segment;
ULONG FileAccess;
IO_STATUS_BLOCK Iosb;
LARGE_INTEGER Offset;
CHAR Buffer;
FILE_STANDARD_INFORMATION FileInfo;
/*
* Create the section
*/
Status = ObCreateObject(ExGetPreviousMode(), //创建一个Section对象
MmSectionObjectType,
ObjectAttributes,
ExGetPreviousMode(),
NULL,
sizeof(ROS_SECTION_OBJECT),
0,
0,
(PVOID*)(PVOID)&Section);
if (!NT_SUCCESS(Status))
{
return(Status);
}
/*
* Initialize it
*/
Section->SectionPageProtection = SectionPageProtection; //对Section对象进行一个合理的初始化
Section->AllocationAttributes = AllocationAttributes;
Section->Segment = NULL; //Section的这个Segment域很重要
/*
* Check file access required
*/
if (SectionPageProtection & PAGE_READWRITE || //将Section的保护属性转变成File的属性
SectionPageProtection & PAGE_EXECUTE_READWRITE)
{
FileAccess = FILE_READ_DATA | FILE_WRITE_DATA;
}
else
{
FileAccess = FILE_READ_DATA;
}
/*
* Reference the file handle
*/
Status = ObReferenceObjectByHandle(FileHandle, //根据文件句柄获取文件对象
FileAccess,
IoFileObjectType,
UserMode,
(PVOID*)(PVOID)&FileObject,
NULL);
if (!NT_SUCCESS(Status))
{
ObDereferenceObject(Section);
return(Status);
}
/*
* FIXME: This is propably not entirely correct. We can't look into
* the standard FCB header because it might not be initialized yet
* (as in case of the EXT2FS driver by Manoj Paul Joseph where the
* standard file information is filled on first request).
*/
Status = IoQueryFileInformation(FileObject, //根据文件对象获取文件的基本信息
FileStandardInformation,
sizeof(FILE_STANDARD_INFORMATION),
&FileInfo,
&Iosb.Information);
if (!NT_SUCCESS(Status))
{
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return Status;
}
/*
* FIXME: Revise this once a locking order for file size changes is
* decided
*/
if (UMaximumSize != NULL)//若指定了文件共享区的尺寸大小
{
MaximumSize = *UMaximumSize;
}
else//若未指定 则从刚才获取到的文件的基本信息中摘取它的尺寸
{
MaximumSize = FileInfo.EndOfFile; //从IoQueryFileInformation中获取尺寸 这才是调用这个API的真实目的
/* Mapping zero-sized files isn't allowed. */
if (MaximumSize.QuadPart == 0) //若获取到的文件大小为0,自然是不需要创建共享区的了
{
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return STATUS_FILE_INVALID;
}
}
if (MaximumSize.QuadPart > FileInfo.EndOfFile.QuadPart)//如果指定的文件共享区的大小>文件的真实大小 则更新文件信息
{
Status = IoSetInformation(FileObject, //将我们的指定的尺寸更新到文件的基本信息中
FileAllocationInformation,
sizeof(LARGE_INTEGER),
&MaximumSize);
if (!NT_SUCCESS(Status))
{
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return(STATUS_SECTION_NOT_EXTENDED);
}
}
if (FileObject->SectionObjectPointer == NULL || //如果该文件没有被用于其他的共享映射区
FileObject->SectionObjectPointer->SharedCacheMap == NULL)
{
/*
* Read a bit so caching is initiated for the file object.
* This is only needed because MiReadPage currently cannot
* handle non-cached streams.
*/
Offset.QuadPart = 0;
Status = ZwReadFile(FileHandle, //读一次开启缓冲机制,具体的看ReactOS留下的注释
NULL,
NULL,
NULL,
&Iosb,
&Buffer,
sizeof (Buffer),
&Offset,
0);
if (!NT_SUCCESS(Status) && (Status != STATUS_END_OF_FILE))
{
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return(Status);
}
if (FileObject->SectionObjectPointer == NULL ||
FileObject->SectionObjectPointer->SharedCacheMap == NULL)
{
/* FIXME: handle this situation */
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return STATUS_INVALID_PARAMETER;
}
}
/*
* Lock the file
*/
Status = MmspWaitForFileLock(FileObject);
if (Status != STATUS_SUCCESS)
{
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return(Status);
}
/*
* If this file hasn't been mapped as a data file before then allocate a
* section segment to describe the data file mapping
*/
if (FileObject->SectionObjectPointer->DataSectionObject == NULL) //如果该文件之前没有被用于共享内存区
{
Segment = ExAllocatePoolWithTag(NonPagedPool, sizeof(MM_SECTION_SEGMENT), //分配一个MM_SECTION_SEGMENT大小的空间用于存放Segment信息
TAG_MM_SECTION_SEGMENT);
if (Segment == NULL)//若分配失败 则自然释放文件对象和共享内存区对象
{
//KeSetEvent((PVOID)&FileObject->Lock, IO_NO_INCREMENT, FALSE);
ObDereferenceObject(Section);
ObDereferenceObject(FileObject);
return(STATUS_NO_MEMORY);
}
Section->Segment = Segment; //将该共享内存的Segment指定为我们刚才生成的这个结构
Segment->ReferenceCount = 1;
ExInitializeFastMutex(&Segment->Lock);
/*
* Set the lock before assigning the segment to the file object
*/
ExAcquireFastMutex(&Segment->Lock);
FileObject->SectionObjectPointer->DataSectionObject = (PVOID)Segment; //文件对象自然也要更新这个指针
Segment->FileOffset = 0; //初始化Segment的一些信息
Segment->Protection = SectionPageProtection;
Segment->Flags = MM_DATAFILE_SEGMENT;
Segment->Characteristics = 0;
Segment->WriteCopy = FALSE;
if (AllocationAttributes & SEC_RESERVE) //如果之前分配的属性是“保留”的共享内存区 则原始长度赋值为0,先保留在那 相当于一个stub
{
Segment->Length = Segment->RawLength = 0;
}
else
{
Segment->RawLength = MaximumSize.u.LowPart; //若是提交过的,则段的长度信息自然是要更新的 RawLength是原始长度
Segment->Length = PAGE_ROUND_UP(Segment->RawLength); //这个Length是与内存对齐的长度,因为映射以页单位,所以这才是真实的段长度
}
Segment->VirtualAddress = 0; //对于普通文件而言,自然是没有VA这一说的
RtlZeroMemory(&Segment->PageDirectory, sizeof(SECTION_PAGE_DIRECTORY)); //共享内存区的页目录表
}
else
{
/*
* If the file is already mapped as a data file then we may need
* to extend it
*/
...
}
MmUnlockSectionSegment(Segment);
Section->FileObject = FileObject; //共享内存区所共享的文件自然也是要指定的 这里指定的是该文件对象
Section->MaximumSize = MaximumSize; //对于普通文件而言 只有一个段 所以其长度其实就是 Segment->RawLength
CcRosReferenceCache(FileObject);
//KeSetEvent((PVOID)&FileObject->Lock, IO_NO_INCREMENT, FALSE);
*SectionObject = Section;
return(STATUS_SUCCESS);
}
看下Section 和 Segment的结构
typedef struct _ROS_SECTION_OBJECT
{
CSHORT Type;
CSHORT Size;
LARGE_INTEGER MaximumSize;
ULONG SectionPageProtection; //共享内存区的保护模式
ULONG AllocationAttributes; //分配属性
PFILE_OBJECT FileObject; //该共享文件区所指向的文件对象
union
{
PMM_IMAGE_SECTION_OBJECT ImageSection; //对于Image映像文件而言 它是特殊的 因为它可能有多个段
PMM_SECTION_SEGMENT Segment; //对于普通文件而言 它指定一个段 普通数据文件有且仅有一个段
};
} ROS_SECTION_OBJECT, *PROS_SECTION_OBJECT;
typedef struct _MM_SECTION_SEGMENT
{
PFILE_OBJECT FileObject;
ULONG_PTR VirtualAddress; /* start offset into the address range for image sections */
LARGE_INTEGER RawLength; /* length of the segment which is part of the mapped file */
LARGE_INTEGER Length; /* absolute length of the segment */
ULONG Protection;
FAST_MUTEX Lock; /* lock which protects the page directory */
ULONG ReferenceCount;
SECTION_PAGE_DIRECTORY PageDirectory;
ULONG Flags;
BOOLEAN WriteCopy;
ULONG Characteristics;
} MM_SECTION_SEGMENT, *PMM_SECTION_SEGMENT;
这里ReactOS用了它自己独有的名字 ROS_SECTION_OBJECT 这是通过ObCreateObject生成的一个Section对象,其数据结构就如上面所示。
下面说下函数流程,其实主要分两步。
- 首先对于Section对象而言,我们要创建它,怎么创建呢,因为Section是对象,所以我们通过调用ObCreateObject生成一个 MmSectionObjectType类型即共享内存区类型的对象。创建完后进行基本的一个初始化。
- 对于文件而言,现在我们只有个句柄,但是我们需要获得文件对象获得它的基本信息,所以我们得先调用ObReferenceObjectByHandle获得这个普通文件对应的文件对象,从而能调用IoQueryFileInformation来获取它的FileInfo.EndOfFile即文件真实大小,同时若是指定了UMaximumSize即给定的共享文件区大小,将真实文件大小与它进行一个比较,如果所指定的共享文件区大小比它大,那么就要通过IoSetFileInformation来更新它,因为对我们而言,最重要的莫过于获取创建的Section大小!!!
- 接着由于ReactOS不完善的缘故,要先通过ZwReadFile来读取1bit来启动缓存机制
- 对于我们而言,共享文件区的结构是搭建好了,但是对于特殊的Image映像而言,它是多个段的,每个段的属性不同,从而使得共享文件区间也会分成不同的Segment(这个就类似于一个区间中有多个区块一样,相同属性的放在一起),这里也就是段喽,不同的段有不同的作用,对于Image可执行映像而言,它非常特殊,它的段数量是不定的,所以ROS_SECTION_OBJECT中的PMM_IMAGE_SECTION_OBJECT其实是个指针数组,它的大小也就与段的数量有密切关联了。也就衍生了段的概念,但是对于普通的数据文件而言,只有一个段,所以我们只需要动态分配一个能盛放我们的Segment信息的空间即可。段的数据结构是MM_SECTION_SEGMENT,其中RawLength和Length是比较重要的,记录着该段的长度,从而当映射的时候方便知道映射的区间是多少。对于SEC_RESERVE自然是不需要分配长度的,因为现在只是“保留”状态,而只有当我们真正的“提交”才会真正的给出这个段的大小是多少。最后初始化一些必要的域,这里面最重要的还有PageDirectory,它其实形式上与页映射机制非常相似。PageDir就是DirBase喽,它的大小是1024,其实是个指针数组,每个指针指向一个二级页表。当然这里我们只是为普通文件创建了这么一个Section和一个Segment,还没有真正的实现映射。
NtMapViewOfSection
上面我们只是创建了一个共享文件区,还没有创建真正的映射。看函数名字其实可以猜到它map的是viewOfSection,部分的文件,但是这个系统调用自然也能将文件全部映射到某一进程的用户空间,这个进程自然也不必是当前进程,因为很显然我们操作进程只需要一个Handle即可。
/**********************************************************************
* NAME EXPORTED
* NtMapViewOfSection
*
* DESCRIPTION
* Maps a view of a section into the virtual address space of a
* process.
*
* ARGUMENTS
* SectionHandle 共享文件区句柄
* Handle of the section.
*
* ProcessHandle 进程句柄
* Handle of the process.
*
* BaseAddress 可选的映射到的虚拟地址处
* Desired base address (or NULL) on entry;
* Actual base address of the view on exit.
*
* ZeroBits 在BaseAddress未给出的情况下ZeroBits就指定了范围
* Number of high order address bits that must be zero.
*
* CommitSize 提交大小,即Section对象实际提交的尺寸
* Size in bytes of the initially committed section of
* the view.
*
* SectionOffset 共享文件的偏移
* Offset in bytes from the beginning of the section
* to the beginning of the view.
*
* ViewSize 映射的共享文件大小
* Desired length of map (or zero to map all) on entry
* Actual length mapped on exit.
*
* InheritDisposition
* Specified how the view is to be shared with
* child processes.
*
* AllocateType 共享文件区域的分配类型 普通数据文件还是可执行映像文件
* Type of allocation for the pages.
*
* Protect 该共享文件区的保护模式
* Protection for the committed region of the view.
*
* RETURN VALUE
* Status.
*
* @implemented
*/
NTSTATUS STDCALL
NtMapViewOfSection(IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect)
{
PVOID SafeBaseAddress;
LARGE_INTEGER SafeSectionOffset;
SIZE_T SafeViewSize;
PROS_SECTION_OBJECT Section;
PEPROCESS Process;
KPROCESSOR_MODE PreviousMode;
PMADDRESS_SPACE AddressSpace;
NTSTATUS Status = STATUS_SUCCESS;
ULONG tmpProtect;
/*
* Check the protection
*/
if (Protect & ~PAGE_FLAGS_VALID_FROM_USER_MODE)
{
CHECKPOINT1;
return STATUS_INVALID_PARAMETER_10;
}
tmpProtect = Protect & ~(PAGE_GUARD|PAGE_NOCACHE); //检测保护选项是否合法
if (tmpProtect != PAGE_NOACCESS &&
tmpProtect != PAGE_READONLY &&
tmpProtect != PAGE_READWRITE &&
tmpProtect != PAGE_WRITECOPY &&
tmpProtect != PAGE_EXECUTE &&
tmpProtect != PAGE_EXECUTE_READ &&
tmpProtect != PAGE_EXECUTE_READWRITE &&
tmpProtect != PAGE_EXECUTE_WRITECOPY)
{
CHECKPOINT1;
return STATUS_INVALID_PAGE_PROTECTION;
}
PreviousMode = ExGetPreviousMode();
if(PreviousMode != KernelMode) //通过safe方式取出参数值
{
SafeBaseAddress = NULL;
SafeSectionOffset.QuadPart = 0;
SafeViewSize = 0;
_SEH_TRY
{
if(BaseAddress != NULL)
{
ProbeForWritePointer(BaseAddress);
SafeBaseAddress = *BaseAddress;
}
if(SectionOffset != NULL)
{
ProbeForWriteLargeInteger(SectionOffset);
SafeSectionOffset = *SectionOffset;
}
ProbeForWriteSize_t(ViewSize);
SafeViewSize = *ViewSize;
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
if(!NT_SUCCESS(Status))
{
return Status;
}
}
else
{
SafeBaseAddress = (BaseAddress != NULL ? *BaseAddress : NULL);
SafeSectionOffset.QuadPart = (SectionOffset != NULL ? SectionOffset->QuadPart : 0);
SafeViewSize = (ViewSize != NULL ? *ViewSize : 0);
}
SafeSectionOffset.LowPart = PAGE_ROUND_DOWN(SafeSectionOffset.LowPart);
Status = ObReferenceObjectByHandle(ProcessHandle, //获取进程对象
PROCESS_VM_OPERATION,
PsProcessType,
PreviousMode,
(PVOID*)(PVOID)&Process,
NULL);
if (!NT_SUCCESS(Status))
{
return(Status);
}
AddressSpace = (PMADDRESS_SPACE)&Process->VadRoot; //获取用户空间
Status = ObReferenceObjectByHandle(SectionHandle, //获取共享文件区对象
SECTION_MAP_READ,
MmSectionObjectType,
PreviousMode,
(PVOID*)(PVOID)&Section,
NULL);
if (!(NT_SUCCESS(Status)))
{
DPRINT("ObReference failed rc=%x\n",Status);
ObDereferenceObject(Process);
return(Status);
}
Status = MmMapViewOfSection(Section, //下发给MmMapViewOfSection来建立共享文件区映射
(PEPROCESS)Process,
(BaseAddress != NULL ? &SafeBaseAddress : NULL),
ZeroBits,
CommitSize,
(SectionOffset != NULL ? &SafeSectionOffset : NULL),
(ViewSize != NULL ? &SafeViewSize : NULL),
InheritDisposition,
AllocationType,
Protect);
/* Check if this is an image for the current process */
if ((Section->AllocationAttributes & SEC_IMAGE) && //对于可执行映像文件 是要特殊对待的
(Process == PsGetCurrentProcess()) &&
(Status != STATUS_IMAGE_NOT_AT_BASE))
{
/* Notify the debugger */
DbgkMapViewOfSection(Section,
SafeBaseAddress,
SafeSectionOffset.LowPart,
SafeViewSize);
}
ObDereferenceObject(Section);
ObDereferenceObject(Process);
if(NT_SUCCESS(Status))
{
/* copy parameters back to the caller */
_SEH_TRY
{
if(BaseAddress != NULL)
{
*BaseAddress = SafeBaseAddress;
}
if(SectionOffset != NULL)
{
*SectionOffset = SafeSectionOffset;
}
if(ViewSize != NULL)
{
*ViewSize = SafeViewSize;
}
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
}
return(Status);
}
这里比较重要的参数SectionOffset是指实际映射的内容在目标映射区对象中的偏移,而ViewSize则为实际映射区间的大小。
NtMapViewOfSection跟NtCreateSection一样,都只是对传进来的参数进行了一个check进行一个分发处理,先是跟NtCreateSection一样对共享文件区的保护属性进行check1 看是否合法。由于map的是viewOfSection,也就是说映射的是一个View(视图 既然是视图就有“可见”和“不可见”),这个view可以从共享文件区域的一个指定的offset和给定的size决定映射哪一部分。
MmMapViewOfSection
分发给MmMapViewOfSection,它才是真正的从给定的一个虚拟地址处开始映射文件共享区的指定偏移和指定大小的一块区域。
/**********************************************************************
* NAME EXPORTED
* MmMapViewOfSection
*
* DESCRIPTION
* Maps a view of a section into the virtual address space of a
* process.
*
* ARGUMENTS
* Section
* Pointer to the section object.
*
* ProcessHandle
* Pointer to the process.
*
* BaseAddress
* Desired base address (or NULL) on entry;
* Actual base address of the view on exit.
*
* ZeroBits
* Number of high order address bits that must be zero.
*
* CommitSize
* Size in bytes of the initially committed section of
* the view.
*
* SectionOffset
* Offset in bytes from the beginning of the section
* to the beginning of the view.
*
* ViewSize
* Desired length of map (or zero to map all) on entry
* Actual length mapped on exit.
*
* InheritDisposition
* Specified how the view is to be shared with
* child processes.
*
* AllocationType
* Type of allocation for the pages.
*
* Protect
* Protection for the committed region of the view.
*
* RETURN VALUE
* Status.
*
* @implemented
*/
NTSTATUS STDCALL
MmMapViewOfSection(IN PVOID SectionObject,
IN PEPROCESS Process,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType,
IN ULONG Protect)
{
PROS_SECTION_OBJECT Section;
PMADDRESS_SPACE AddressSpace;
ULONG ViewOffset;
NTSTATUS Status = STATUS_SUCCESS;
ASSERT(Process);
if (Protect != PAGE_READONLY && //依旧是检测区块的保护模式,似乎有点多余?
Protect != PAGE_READWRITE &&
Protect != PAGE_WRITECOPY &&
Protect != PAGE_EXECUTE &&
Protect != PAGE_EXECUTE_READ &&
Protect != PAGE_EXECUTE_READWRITE &&
Protect != PAGE_EXECUTE_WRITECOPY)
{
CHECKPOINT1;
return STATUS_INVALID_PAGE_PROTECTION;
}
Section = (PROS_SECTION_OBJECT)SectionObject;
AddressSpace = (PMADDRESS_SPACE)&(Process)->VadRoot; //获取用户空间
AllocationType |= (Section->AllocationAttributes & SEC_NO_CHANGE);
MmLockAddressSpace(AddressSpace);
if (Section->AllocationAttributes & SEC_IMAGE) //若分配的共享内存区类型是可执行映像 这里我们先pass
{
......
}
else
{
/* check for write access 检测共享内存区写权限是否冲突*/
if ((Protect & (PAGE_READWRITE|PAGE_EXECUTE_READWRITE)) && //这里将之前Create的保护属性跟Map的保护属性进行一个校验 看是否冲突
!(Section->SectionPageProtection & (PAGE_READWRITE|PAGE_EXECUTE_READWRITE)))
{
CHECKPOINT1;
return STATUS_SECTION_PROTECTION;
}
/* check for read access 检测共享内存区读权限是否冲突*/
if ((Protect & (PAGE_READONLY|PAGE_WRITECOPY|PAGE_EXECUTE_READ|PAGE_EXECUTE_WRITECOPY)) &&
!(Section->SectionPageProtection & (PAGE_READONLY|PAGE_READWRITE|PAGE_WRITECOPY|PAGE_EXECUTE_READ|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY)))
{
CHECKPOINT1;
return STATUS_SECTION_PROTECTION;
}
/* check for execute access 检测共享内存区执行权限是否冲突*/
if ((Protect & (PAGE_EXECUTE|PAGE_EXECUTE_READ|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY)) &&
!(Section->SectionPageProtection & (PAGE_EXECUTE|PAGE_EXECUTE_READ|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY)))
{
CHECKPOINT1;
return STATUS_SECTION_PROTECTION;
}
if (ViewSize == NULL)//存储映射区域的指针都为NULL说明出现了严重错误了
{
/* Following this pointer would lead to us to the dark side */
/* What to do? Bugcheck? Return status? Do the mambo? */
KEBUGCHECK(MEMORY_MANAGEMENT);
}
if (SectionOffset == NULL)//这里将SectionOffset转成ViewOffset
{
ViewOffset = 0;
}
else
{
ViewOffset = SectionOffset->u.LowPart;
}
if ((ViewOffset % PAGE_SIZE) != 0)//不对齐自然不能映射
{
MmUnlockAddressSpace(AddressSpace);
return(STATUS_MAPPED_ALIGNMENT);
}
if ((*ViewSize) == 0)//若指定的共享内存区域的大小是0,则默认从ViewOffset开始一直映射到最后
{
(*ViewSize) = Section->MaximumSize.u.LowPart - ViewOffset;
}
else if (((*ViewSize)+ViewOffset) > Section->MaximumSize.u.LowPart)//否则以映射到ViewSize+ViewOffset为止 当然viewsize自然是不能超出的
{
(*ViewSize) = Section->MaximumSize.u.LowPart - ViewOffset;
}
MmLockSectionSegment(Section->Segment);
Status = MmMapViewOfSegment(AddressSpace, //这里从映射Section转成到映射Segment,因为Section就是由不同Segment组成的,而对于非可执行映像而言,它只有一个Segment,所以映射该段即可
Section,
Section->Segment,
BaseAddress,
*ViewSize,
Protect,
ViewOffset,
AllocationType & (MEM_TOP_DOWN|SEC_NO_CHANGE));
MmUnlockSectionSegment(Section->Segment);
if (!NT_SUCCESS(Status))
{
MmUnlockAddressSpace(AddressSpace);
return(Status);
}
}
MmUnlockAddressSpace(AddressSpace);
return(STATUS_SUCCESS);
}
Map它的Section,其实等同于细分成映射不同的段,对于可执行映像而言,它需要映射多个段,它是特殊的,每个段还有不同的属性,所以需要特殊对待,但这里我们先不看可执行映像的段映射问题。我们现在着眼的是对共享文件区的分析,共享文件区的文件,就是普通的数据文件,对于普通的数据文件而言,它只有一个段,所以又下发到了MmMapViewOfSegment。当然,MmMapViewOfSection也对保护模式进行了一次检测,并且对最重要的ViewSize进行了一个处理,之后就是MmMapViewOfSegment的事了。
MmMapViewOfSegment
NTSTATUS static
MmMapViewOfSegment(PMADDRESS_SPACE AddressSpace,
PROS_SECTION_OBJECT Section,
PMM_SECTION_SEGMENT Segment,
PVOID* BaseAddress,
SIZE_T ViewSize,
ULONG Protect,
ULONG ViewOffset,
ULONG AllocationType)
{
PMEMORY_AREA MArea;
NTSTATUS Status;
PHYSICAL_ADDRESS BoundaryAddressMultiple;
BoundaryAddressMultiple.QuadPart = 0;
Status = MmCreateMemoryArea(AddressSpace, //我们是要在这个给定的虚拟空间范围中映射到文件共享区 所以创建这样的一个区间 其类型是MEMORY_AREA_SECTION_VIEW
MEMORY_AREA_SECTION_VIEW,
BaseAddress,
ViewSize,
Protect,
&MArea,
FALSE,
AllocationType,
BoundaryAddressMultiple);
if (!NT_SUCCESS(Status))
{
DPRINT1("Mapping between 0x%.8X and 0x%.8X failed (%X).\n",
(*BaseAddress), (char*)(*BaseAddress) + ViewSize, Status);
return(Status);
}
ObReferenceObject((PVOID)Section);
MArea->Data.SectionData.Segment = Segment; //此时这里使用的就是SectionData了而不是普通的区间(VirtualMemoryData)了 指定该Area的Segment
MArea->Data.SectionData.Section = Section; //指定该区间的Section
MArea->Data.SectionData.ViewOffset = ViewOffset;//指定映射的偏移 这里的大小已经隐形的给出了 这块被分配的虚拟区间长度就是我们的ViewSize
MArea->Data.SectionData.WriteCopyView = FALSE;
MmInitializeRegion(&MArea->Data.SectionData.RegionListHead,//初始化该Area下的Region,建立该区块链表,其实对于普通数据文件而言就是这一块
ViewSize, 0, Protect);
return(STATUS_SUCCESS);
}
typedef struct _MEMORY_AREA {//每一个进程空间中的一个区间的结构
PVOID StartingAddress;
PVOID EndingAddress;
struct _MEMORY_AREA *Parent;//父节点指针
struct _MEMORY_AREA *LeftChild;//左孩子指针
struct _MEMORY_AREA *RightChild;//右孩子指针
ULONG Type; //MEM_COMMIT MEM_RESERVE
ULONG Protect; //主要是读写属性之类的 例如PAGE_READONLY等
ULONG Flags;
BOOLEAN DeleteInProgress;
ULONG PageOpCount;
union {
struct {//当是文件映射或共享内存时
ROS_SECTION_OBJEXT* Section;
ULONG ViewOffset;
PMM_SECTION_SEGMENT Segment;
BOOLEAN WriteCopyView;
LIST_ENTRY RegionListHead;
}SectionData;
struct {//大部分情况是这个,指向区间中区块所连接成的双向链表
LIST_ENTRY RegionListHead;
}VirtualMemoryData;
}Data;//该节点存储的数据区域
}MEMORY_AREA, *PMEMORY_DATA;
MmMapViewOfSegment也是很简短的,其主要的工作也就是创建一个Area来容纳这个共享文件区,这个Area的大小显然就是ViewSize了,但是值得注意的是我们仅仅是创建了类型为MEMORY_AREA_SECTION_VIEW的虚拟区间,并没有映射到实际的物理页面。这里替代映射到真实的物理页面的方法是设置了SectionData的Section和Segment。之所以Windows要这么做,是因为节省物理页面的需要,只有当程序访问到共享文件区这块虚拟内存空间的时候,才会为其建立映射,其道理跟将虚拟页面置换到外存上一个道理,而我们知道,当MMU寻找不到指定页面的时候,会引发一个页面异常,此时页面异常处理函数会被执行,然后根据原因发放到具体的handler去处理,这里就会发放到MmNotPresentFault,然后根据异常发生的地址找到其所属的区块,根据区块的分配类型再进行一次处理,此时也就会下放给MmNotPresentFaultSectionView 对共享文件区的区块做一个异常处理。
MmNotPresentFaultSectionView
NTSTATUS
NTAPI
MmNotPresentFaultSectionView(PMADDRESS_SPACE AddressSpace,
MEMORY_AREA* MemoryArea,
PVOID Address,
BOOLEAN Locked)
{
ULONG Offset;
PFN_TYPE Page;
NTSTATUS Status;
PVOID PAddress;
PROS_SECTION_OBJECT Section;
PMM_SECTION_SEGMENT Segment;
ULONG Entry;
ULONG Entry1;
ULONG Attributes;
PMM_PAGEOP PageOp;
PMM_REGION Region;
BOOLEAN HasSwapEntry;
/*
* There is a window between taking the page fault and locking the
* address space when another thread could load the page so we check
* that.
*/
if (MmIsPagePresent(AddressSpace->Process, Address))
{
if (Locked)
{
MmLockPage(MmGetPfnForProcess(AddressSpace->Process, Address));
}
return(STATUS_SUCCESS);
}
PAddress = MM_ROUND_DOWN(Address, PAGE_SIZE);
Offset = (ULONG_PTR)PAddress - (ULONG_PTR)MemoryArea->StartingAddress //换算成共享文件的偏移
+ MemoryArea->Data.SectionData.ViewOffset;
Segment = MemoryArea->Data.SectionData.Segment; //从该区间中取出该段对象
Section = MemoryArea->Data.SectionData.Section; //从该区间中取出该共享文件区对象
Region = MmFindRegion(MemoryArea->StartingAddress, //找到所属的区块
&MemoryArea->Data.SectionData.RegionListHead,
Address, NULL);
/*
* Lock the segment
*/
MmLockSectionSegment(Segment);
/*
* Check if this page needs to be mapped COW
*/
if ((Segment->WriteCopy || MemoryArea->Data.SectionData.WriteCopyView) &&
(Region->Protect == PAGE_READWRITE ||
Region->Protect == PAGE_EXECUTE_READWRITE))
{
Attributes = Region->Protect == PAGE_READWRITE ? PAGE_READONLY : PAGE_EXECUTE_READ;
}
else
{
Attributes = Region->Protect;
}
/*
* Get or create a page operation descriptor
*/
PageOp = MmGetPageOp(MemoryArea, NULL, 0, Segment, Offset, MM_PAGEOP_PAGEIN, FALSE); //获取一个页面操作描述符
if (PageOp == NULL)
{
DPRINT1("MmGetPageOp failed\n");
KEBUGCHECK(0);
}
/*
* Check if someone else is already handling this fault, if so wait
* for them
*/
if (PageOp->Thread != PsGetCurrentThread())//如果不是当前线程在对这个共享文件区进行操作
{
...
}
HasSwapEntry = MmIsPageSwapEntry(AddressSpace->Process, (PVOID)PAddress); //尝试获取置换文件号
if (HasSwapEntry) //如果存在置换文件号 说明该共享内存区域是有后备的倒换文件的
{
/*
* Must be private page we have swapped out.
*/
SWAPENTRY SwapEntry;
/*
* Sanity check 可用性
*/
if (Segment->Flags & MM_PAGEFILE_SEGMENT)
{
DPRINT1("Found a swaped out private page in a pagefile section.\n");
KEBUGCHECK(0);
}
MmUnlockSectionSegment(Segment);
MmDeletePageFileMapping(AddressSpace->Process, (PVOID)PAddress, &SwapEntry); //删除该虚存页面与置换文件的映射 并获得置换文件号 从而在后面凭借该置换文件号将相应的页面读入进来
MmUnlockAddressSpace(AddressSpace);
Status = MmRequestPageMemoryConsumer(MC_USER, TRUE, &Page); //获取一个空闲的物理页面
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
Status = MmReadFromSwapPage(SwapEntry, Page); //从该置换文件中将相应的页面读取出来放到物理页面上
if (!NT_SUCCESS(Status))
{
DPRINT1("MmReadFromSwapPage failed, status = %x\n", Status);
KEBUGCHECK(0);
}
MmLockAddressSpace(AddressSpace);
Status = MmCreateVirtualMapping(AddressSpace->Process, //该共享文件区与该物理页面建立映射
Address,
Region->Protect,
&Page,
1);
if (!NT_SUCCESS(Status))
{
DPRINT("MmCreateVirtualMapping failed, not out of memory\n");
KEBUGCHECK(0);
return(Status);
}
/*
* Store the swap entry for later use.
*/
MmSetSavedSwapEntryPage(Page, SwapEntry);//对该物理页面的后备文件号进行设置
/*
* Add the page to the process's working set
*/
MmInsertRmap(Page, AddressSpace->Process, (PVOID)PAddress);//将该页面加入到工作集中
/*
* Finish the operation
*/
if (Locked)
{
MmLockPage(Page);
}
PageOp->Status = STATUS_SUCCESS;
MmspCompleteAndReleasePageOp(PageOp);//释放该PageOp供其他页面操作描述符对该页进行其他的操作
DPRINT("Address 0x%.8X\n", Address);
return(STATUS_SUCCESS);
}
/*
* Satisfying a page fault on a map of /Device/PhysicalMemory is easy
*/
if (Section->AllocationAttributes & SEC_PHYSICALMEMORY)
{
...
}
/*
* Map anonymous memory for BSS sections
*/
if (Segment->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA)
{
...
}
/*
* Get the entry corresponding to the offset within the section
* 这里的offset是之前经过转换过的该共享文件区的view-offset
* 这里尝试根据该段寻找到对应的pte
*/
Entry = MmGetPageEntrySectionSegment(Segment, Offset);//从SectionSegment中获取入口 注意这里是从共享内存区的段中获取的
if (Entry == 0) //当入口为0时
{
/*
* If the entry is zero (and it can't change because we have
* locked the segment) then we need to load the page.
*/
/*
* Release all our locks and read in the page from disk
*/
//此时释放锁 为了能写入
MmUnlockSectionSegment(Segment);
MmUnlockAddressSpace(AddressSpace);
if ((Segment->Flags & MM_PAGEFILE_SEGMENT) || //对于可执行映像而言
(Offset >= PAGE_ROUND_UP(Segment->RawLength) && Section->AllocationAttributes & SEC_IMAGE))
{
...
}
else
{
Status = MiReadPage(MemoryArea, Offset, &Page);//将对应文件偏移的页面读入,并返回页面号
if (!NT_SUCCESS(Status))
{
DPRINT1("MiReadPage failed (Status %x)\n", Status);
}
}
if (!NT_SUCCESS(Status))
{
/*
* FIXME: What do we know in this case?
*/
/*
* Cleanup and release locks
*/
MmLockAddressSpace(AddressSpace);
PageOp->Status = Status;
MmspCompleteAndReleasePageOp(PageOp);
DPRINT("Address 0x%.8X\n", Address);
return(Status);
}
/*
* Relock the address space and segment
*/
MmLockAddressSpace(AddressSpace);
MmLockSectionSegment(Segment);
/*
* Check the entry. No one should change the status of a page
* that has a pending page-in.
*/
Entry1 = MmGetPageEntrySectionSegment(Segment, Offset);//在读取一次确认页面表项没变 因为读入页面是个耗时的工作,可能会发生线程切换
if (Entry != Entry1)
{
DPRINT1("Someone changed ppte entry while we slept\n");
KEBUGCHECK(0);
}
/*
* Mark the offset within the section as having valid, in-memory
* data
* #define MAKE_SSE(P, C) ((P) | ((C) << 1))
*/
Entry = MAKE_SSE(Page << PAGE_SHIFT, 1); //此时物理页面号仍然是20bit,但是SSE的最低位为0 表明在映射段目录表中目标页面在物理内存页面中!!!,表示SSE的内容是物理页面号
MmSetPageEntrySectionSegment(Segment, Offset, Entry);//在对应的页表项中填写该SSE 注意这里是在 Segment里的页映射表中填写相关的项
MmUnlockSectionSegment(Segment);
Status = MmCreateVirtualMapping(AddressSpace->Process, //因为该页面已经读到了相应的物理页面处,现在只要建立映射即可
Address,
Attributes,
&Page,
1);
if (!NT_SUCCESS(Status))
{
DPRINT1("Unable to create virtual mapping\n");
KEBUGCHECK(0);
}
MmInsertRmap(Page, AddressSpace->Process, (PVOID)PAddress);
if (Locked)
{
MmLockPage(Page);
}
PageOp->Status = STATUS_SUCCESS;
MmspCompleteAndReleasePageOp(PageOp);//释放 page operation descriptor
DPRINT("Address 0x%.8X\n", Address);
return(STATUS_SUCCESS);
}
//#define IS_SWAP_FROM_SSE(E) ((E) & 0x00000001) 对于SectionSegment中记录的而言 低12位自然并不需要记录其他东西 只需要能表示该页面在哪即可
else if (IS_SWAP_FROM_SSE(Entry))//如果SSE中的置换文件号存在 即最低位为1 表示该页面被置换到了外存 否则表明是在物理内存中
{
SWAPENTRY SwapEntry;
/*
当建立起映射后,该物理页面可能会被Trim函数调用balance线程来将相应的物理页面置换到外存上,此时会将SSE的内容转变为文件内部的位移
#define SWAPENTRY_FROM_SSE(E) ((E) >> 1)
#define MAKE_SWAP_SSE(S) (((S) << 1) | 0x1) 当被置换到外存时 会使用这个宏 将swapentry所以恢复的时候 要右移一位获得置换文件号
*/
SwapEntry = SWAPENTRY_FROM_SSE(Entry);//从Entry中获取置换文件号 即右移1位 因为在置换到外存的过程中sse的内容被改写为swapentry 左移1位 并且将最低位设置为1 此时说明sse的内容是文件内部页面号
/*
* Release all our locks and read in the page from disk
*/
MmUnlockSectionSegment(Segment);
MmUnlockAddressSpace(AddressSpace);
Status = MmRequestPageMemoryConsumer(MC_USER, TRUE, &Page); //获取一个物理页面 用于将swapentry指向的页面读入过来
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
Status = MmReadFromSwapPage(SwapEntry, Page); //将对应的swapentry指定的置换文件读入到该物理页面中即可
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
/*
* Relock the address space and segment
*/
MmLockAddressSpace(AddressSpace);
MmLockSectionSegment(Segment);
/*
* Check the entry. No one should change the status of a page
* that has a pending page-in.
*/
Entry1 = MmGetPageEntrySectionSegment(Segment, Offset); //仍然需要确认其表项是否正常
if (Entry != Entry1)
{
DPRINT1("Someone changed ppte entry while we slept\n");
KEBUGCHECK(0);
}
/*
* Mark the offset within the section as having valid, in-memory
* data
*/
Entry = MAKE_SSE(Page << PAGE_SHIFT, 1); //此时SSE要改回来
MmSetPageEntrySectionSegment(Segment, Offset, Entry);//设置SSE到相应的segment中
MmUnlockSectionSegment(Segment);
/*
* Save the swap entry.
*/
MmSetSavedSwapEntryPage(Page, SwapEntry); //将该物理页的后备置换文件号填写到物理页面相关域中
Status = MmCreateVirtualMapping(AddressSpace->Process, //建立虚存页面到该页面的映射
Address,
Region->Protect,
&Page,
1);
if (!NT_SUCCESS(Status))
{
DPRINT1("Unable to create virtual mapping\n");
KEBUGCHECK(0);
}
MmInsertRmap(Page, AddressSpace->Process, (PVOID)PAddress);
if (Locked)
{
MmLockPage(Page);
}
PageOp->Status = STATUS_SUCCESS;
MmspCompleteAndReleasePageOp(PageOp);
DPRINT("Address 0x%.8X\n", Address);
return(STATUS_SUCCESS);
}
else
{
...
}
}
其流程如下:
- 首先从pte中尝试检测是否是swapEntry,若是的话,说明该共享内存区存在后备置换文件,这个时候将虚存页面对相应的后备文件的映射去除,然后申请一个物理页面,通过swapentry将对应的置换文件读入到所申请的物理页面中,这是之前讲过的。
- 当对应的pte项中没有相应的swapentry,会从映射段页面表中获取独有的Entry,这里的SSE是Section->Segment的PageDirectory的相关项,这里我们只需要知道它可以获得SSE,映射段页面表中记录的SSE有其独有的特征,当最低位为0时,表明高20bit是物理页面号,当最低位为1时,此时其余的31位自然表示的是置换文件号
- 此时先判别SSE是否全空,当全空时,自然只能申请新的物理页面,然后读入,这里Windows将它集成到了一个MiReadPage中,调用此函数就将这里事情全干了,当然耗费的时间也是巨大的,所以还要调用一次MmGetPageEntrySectionSegment来判别在读页面的时候其他线程有没有篡改SSE。之后已经读入了,而SSE还是为0,所以需要根据物理页面号生成SSE,注意这里最低位是为0的,表示存储的是物理页面号,Entry填写完,最后再建立虚存页面到该物理页面的映射即可。
- 当SSE最低位为1时,表明此时物理页面已被修剪函数置换到外存上了,所以需要根据此时的SSE转换成文件页面号,然后申请一个物理页面,调用MmReadFromSwapPage从SSE中获得的swapentry指向的置换文件页面读入到申请的物理页面中。接着重新设置SSE,指定该页面的后备文件号,最后建立虚拟页面对该物理页面的映射。