系列文章目录
文章目录
- 系列文章目录
- 3.2 页面异常
- MmAccessFault()
- MmNotPresentFault()
- MmNotPresentFaultVirtualMemory()
- MmGetPageOp()
- MmReadFromSwapPage()
3.2 页面异常
前面曾经提到,在为交割而分配一个区间时,区块的类型变成了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);
}
}
这个函数是由底层的异常响应程序调用的,异常响应程序的作用类似于中断响应程序。当发生某种异常时(页面异常是14号常),CPU首先进入相应的异常响应程序,在那里再根据具体的情况调用不同的处理程序。
参数 Storelnstruction是个表示异常原因的布尔量,为0表示缺页,非0表示越权。该信息来自发生异常时CPU自动压入堆栈的出错代码。发生异常时,CPU一方面根据异常的种类产生中断1异常向量,使控制转入相应的异常响应程序入口,另一方面在保存返回断点的同时将一个进一步说明异常原因的出错代码压入堆栈。
般而言,页面异常多半发生于用户空间,发生在系统空间的可能性也有,因为Windows允许倒出系统空间的部分页面。其中有一种情况,表面看来是个问题,实际上却不是。我们知道,当内核的映射出现变动时,该变动首先反映在全局的内核映射表中,然后才反映到当前进程的映射表中。如果在这中间访问了刚发生变动而尚未来得及反映到当前进程的页面映射表中的页面,就可能发生异常。但是此时只要依据全局的内核映射表更新当前进程的映射表,就可以重试引起异常的访问了这里的 Mmi386MakeKernelPageTableGlobal()就用来检查是否属于这种情况,并(就所涉及的页面)更新当前进程的映射表。这个函数返回TRUE表示天下本无事,所以这里就直接成功返回了,这是一种优化。
发生页面异常的原因大体上可以分成两大类。一类是由于违反了页面的保护模式,这是因访问权限不足而引起的:另一类是因缺页而引起的。这里分别提供了两个函数加以处理,我们只看其中之一MmNotPresentFault(),这是对于缺页的处理。
MmNotPresentFault()
[MmAccessFault()>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;
/*
* Find the memory area for the faulting address
*/
if (Address >= (ULONG_PTR)MmSystemRangeStart)//页面异常发生于系统空间
{
/*
* Check permissions
*/
if (Mode != KernelMode)
{
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.QuadPart >> 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);
}
参数Address表明引起异常的内存单元地址(而不是引起异常的指令所在的地址),如果这个(虚存)地址在系统空间,就通过MmGetKernelAddressSpace()获取代表系统空间的数据结构(指针)在用户空间则从当前进程的数据结构获取其用户空间的结构指针。
然后,通过 MmLocateMemoryAreaByAddress()在该空间中寻找所属的虚存区间。如果找不到,就说明这个地址根本就不在任何已经分配的区间之内,所以这是一次属于越界访问的“硬伤”。
而如果找到了这个地址所属的区间,那就要看该区间的类型了。对于一般的虚存区间,即类型为MEMORY_AREA_VIRTUAL_MEMORY的区间,所做的反应是通过MmNotPresentFaultVirtualMemory()加以处理。
MmNotPresentFaultVirtualMemory()
[MmNotPresentFault()>MmNotPresentFaultVirtualMemory()]
NTSTATUS
NTAPI
MmNotPresentFaultVirtualMemory(PMADDRESS_SPACE AddressSpace,
MEMORY_AREA* MemoryArea,
PVOID Address,
BOOLEAN Locked)
/*
* FUNCTION: Move data into memory to satisfy a page not present fault
* ARGUMENTS:
* AddressSpace = Address space within which the fault occurred
* MemoryArea = The memory area within which the fault occurred
* Address = The absolute address of fault
* RETURNS: Status
* NOTES: This function is called with the address space lock held.
*/
{
PFN_TYPE Page;
NTSTATUS Status;
PMM_REGION Region;
PMM_PAGEOP PageOp;
/*
* 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(NULL, Address))//页面已经在位
{
if (Locked)
{
MmLockPage(MmGetPfnForProcess(NULL, Address));
}
return(STATUS_SUCCESS);
}
/*
* Check for the virtual memory area being deleted.
*/
if (MemoryArea->DeleteInProgress)
{
return(STATUS_UNSUCCESSFUL);
}
/*
* Get the segment corresponding to the virtual address //找到所属的区块
*/
Region = MmFindRegion(MemoryArea->StartingAddress,
&MemoryArea->Data.VirtualMemoryData.RegionListHead,
Address, NULL);
if (Region->Type == MEM_RESERVE || Region->Protect == PAGE_NOACCESS)
{//尚未COMMIT或不让访问的页面当然不应该被访问
return(STATUS_ACCESS_VIOLATION);
}
/*
* Get or create a page operation
* //创建或者获取一个已由别的线程创建的PageOp
*/
PageOp = MmGetPageOp(MemoryArea, AddressSpace->Process->UniqueProcessId,
(PVOID)PAGE_ROUND_DOWN(Address), NULL, 0,
MM_PAGEOP_PAGEIN, FALSE);
if (PageOp == NULL)
{
DPRINT1("MmGetPageOp failed");
KEBUGCHECK(0);
}
/*
* Check if someone else is already handling this fault, if so wait
* for them
*/
if (PageOp->Thread != PsGetCurrentThread())
{//别的线程已在进行同样的页面操作
MmUnlockAddressSpace(AddressSpace);
Status = KeWaitForSingleObject(&PageOp->CompletionEvent,
0,
KernelMode,
FALSE,
NULL);//等待其完成
/*
* Check for various strange conditions
*/
if (Status != STATUS_SUCCESS)
{
DPRINT1("Failed to wait for page op\n");
KEBUGCHECK(0);
}
if (PageOp->Status == STATUS_PENDING)
{
DPRINT1("Woke for page op before completion\n");
KEBUGCHECK(0);
}
/*
* If this wasn't a pagein then we need to restart the handling
*/
if (PageOp->OpType != MM_PAGEOP_PAGEIN)
{//当前操作不是MM_PAGEOP_PAGEIN,重新加以处理
MmLockAddressSpace(AddressSpace);
KeSetEvent(&PageOp->CompletionEvent, IO_NO_INCREMENT, FALSE);
MmReleasePageOp(PageOp);
return(STATUS_MM_RESTART_OPERATION);
}
/*
* If the thread handling this fault has failed then we don't retry
*/
if (!NT_SUCCESS(PageOp->Status))
{
MmLockAddressSpace(AddressSpace);
KeSetEvent(&PageOp->CompletionEvent, IO_NO_INCREMENT, FALSE);
Status = PageOp->Status;
MmReleasePageOp(PageOp);
return(Status);
}
MmLockAddressSpace(AddressSpace);
if (Locked)
{
MmLockPage(MmGetPfnForProcess(NULL, Address));
}
KeSetEvent(&PageOp->CompletionEvent, IO_NO_INCREMENT, FALSE);
MmReleasePageOp(PageOp);//别的线程已经完成了同样的操作
return(STATUS_SUCCESS);
}
}
在同一个内存区间之内,还可以有多个不同的区块,其中有的可能已经交割兑现了,有的可能还只是保留了但未交割,所以这里还要通过 MmFindRegion()从中找到具体的区块。显然,只有已经交割的区块才可以被访问。所以,如果具体区块的类型(其实是状态)是MEM_RESERVE或者PAGE_NOACCESS,就没有什么办法可以补救了,所以直接失败返回,由系统的出错处理机制即“结构化异常处理”机制去采取进一步的措施。
确认了地址 Address所在的区块可以被访问之后,就通过 MmGetPageOp()获取或登记一个类型为MM_PAGEOP_PAGEIN的MM_PAGEOP数据结构PageOp。之所以要使用这样的数据结构,是因为不同的线程有可能要启动针对同一页面的操作(例如两个线程先后访问同一个不在位的页面),采用 MM_PAGEOP数据结构及其队列可以使后来的线程发现已经有别的线程在进行针对同一页面的操作。这样一来可以把相同的操作合并在一起,二来也可以避免互相干扰。所以,如果发现已经有别的线程在进行针对同一页面的操作,就需要先通过 KeWaitForSingleObiect()等待其完成。
我们暂时岔开来看一下MmGetPageOp()的代码:
MmGetPageOp()
[MmNotPresentFault() > MmNotPresentFaultVirtualMemory() > MmGetPageOp()]
PMM_PAGEOP
NTAPI
MmGetPageOp(PMEMORY_AREA MArea, HANDLE Pid, PVOID Address,
PMM_SECTION_SEGMENT Segment, ULONG Offset, ULONG OpType, BOOLEAN First)
.....................
/*
* Calcuate the hash value for pageop structure //计算一个Hash值
*/
if (MArea->Type == MEMORY_AREA_SECTION_VIEW)
{
Hash = (((ULONG_PTR)Segment) | (((ULONG_PTR)Offset) / PAGE_SIZE));
}
else
{
Hash = (((ULONG_PTR)Pid) | (((ULONG_PTR)Address) / PAGE_SIZE));
}
Hash = Hash % PAGEOP_HASH_TABLE_SIZE;
KeAcquireSpinLock(&MmPageOpHashTableLock, &oldIrql);
/*
* Check for an existing pageop structure
*/
PageOp = MmPageOpHashTable[Hash];
while (PageOp != NULL)
{
if (MArea->Type == MEMORY_AREA_SECTION_VIEW)
{
if (PageOp->Segment == Segment &&
PageOp->Offset == Offset)
{
break;
}
}
else
{
if (PageOp->Pid == Pid &&
PageOp->Address == Address)
{
break;
}
}
PageOp = PageOp->Next;
}
/*
* If we found an existing pageop then increment the reference count
* and return it.
*/
if (PageOp != NULL)
{
if (First)
{
PageOp = NULL;
}
else
{
PageOp->ReferenceCount++;
}
KeReleaseSpinLock(&MmPageOpHashTableLock, oldIrql);
return(PageOp);
}
/*
* Otherwise add a new pageop.
*/
PageOp = ExAllocateFromNPagedLookasideList(&MmPageOpLookasideList);
if (PageOp == NULL)
{
KeReleaseSpinLock(&MmPageOpHashTableLock, oldIrql);
KEBUGCHECK(0);
return(NULL);
}
if (MArea->Type != MEMORY_AREA_SECTION_VIEW)
{
PageOp->Pid = Pid;
PageOp->Address = Address;
}
else
{
PageOp->Segment = Segment;
PageOp->Offset = Offset;
}
PageOp->ReferenceCount = 1;
PageOp->Next = MmPageOpHashTable[Hash];
PageOp->Hash = Hash;
PageOp->Thread = PsGetCurrentThread();
PageOp->Abandoned = FALSE;
PageOp->Status = STATUS_PENDING;
PageOp->OpType = OpType;
PageOp->MArea = MArea;
KeInitializeEvent(&PageOp->CompletionEvent, NotificationEvent, FALSE);
MmPageOpHashTable[Hash] = PageOp;
(void)InterlockedIncrementUL(&MArea->PageOpCount);
KeReleaseSpinLock(&MmPageOpHashTableLock, oldIrql);
return(PageOp);
}
参数 Address已与页面边界对齐,因为前面的实参是PAGE_ROUND_DOWN(Address)。所以这儿的参数 Address 是页面的起始地址。
内核中有一组杂凑(Hash)的页面操作(请求)队列,每当要进行页面操作时就创建一个MM_PAGEOP数据结构并将其挂入某个杂凑队列,表示此项操作正在进行之中。所以,如果根据杂凑值在队列中找到了特征相符的数据结构,就说明己有针对同一个页面的操作在进行中,需要等待其完成,所以返回指向这个数据结构的指针。要是找不到,就分配一个MM_PAGEOP数据结构,加以初始化之后将其挂入杂凑队列,同样也返回指向这个数据结构的指针。
回到 MmNotPresentFaultVirtualMemory()的代码。如果从 MmGetPageOp()返回的结构指针PageOp表明该页面操作的启动者并非当前线程,就说明别的线程已经启动了针对同一个页面的操作,所以通过KeWaitForSingleObject()等待该页面操作完成。但是,针对同一页面的操作未必就是同样的操作。如果所完成的操作不是MM_PAGEOP_PAGEIN,就说明这里所需的操作尚未完成,同志仍需努力。怎么努力呢?就是出错返回,返回出错代码STATUS_MM_RESTART_OPERATION。这样,上一层函数MmNotPresentFault()中的 do{}while()循环就会以相同的参数再次调用MmNotPresentFaultVirtualMemory(),这一次可能就不存在由别的线程启动的针对同一个页面的操作了。如果还存在也不要紧,只不过是又一轮循环而己。反之,如果刚完成的这个操作恰好也是MM_PAGEOP_PAGEIN,那就实际上合二为一了,目的已经达到,可以成功返回了。
如果没有别的线程在进行针对同一页面的操作,则当前线程就是该页面操作的启动者,所以当前线程承担着完成此项操作的责任,我们继续往下看:
[MmNotPresentFault()>MmNotPresentFaultVirtualMemory()]
/*
* Try to allocate a page//分配物理内存页面,不等待
*/
Status = MmRequestPageMemoryConsumer(MC_USER, FALSE, &Page);
if (Status == STATUS_NO_MEMORY)
{//分配物理内存页面,等行
MmUnlockAddressSpace(AddressSpace);
Status = MmRequestPageMemoryConsumer(MC_USER, TRUE, &Page);
MmLockAddressSpace(AddressSpace);
}
if (!NT_SUCCESS(Status))
{
DPRINT1("MmRequestPageMemoryConsumer failed, status = %x\n", Status);
KEBUGCHECK(0);
}
/*
* Handle swapped out pages.
*/
if (MmIsPageSwapEntry(NULL, Address))
{
SWAPENTRY SwapEntry;
//从页面映射表中读出相应的PTE,将其(右移一位)转换成SwapEntry,并将PTE清0
MmDeletePageFileMapping(AddressSpace->Process, Address, &SwapEntry);
Status = MmReadFromSwapPage(SwapEntry, Page);//读入倒换页面
if (!NT_SUCCESS(Status))
{
KEBUGCHECK(0);
}
MmSetSavedSwapEntryPage(Page, SwapEntry);
}
/*
* Set the page. If we fail because we are out of memory then
* try again
*/
Status = MmCreateVirtualMapping(AddressSpace->Process,
(PVOID)PAGE_ROUND_DOWN(Address),
Region->Protect,
&Page,
1);//建立物理页面的映射
while (Status == STATUS_NO_MEMORY)
{
MmUnlockAddressSpace(AddressSpace);
Status = MmCreateVirtualMapping(AddressSpace->Process,
Address,
Region->Protect,
&Page,
1);
MmLockAddressSpace(AddressSpace);
}
if (!NT_SUCCESS(Status))
{
DPRINT1("MmCreateVirtualMapping failed, not out of memory\n");
KEBUGCHECK(0);
return(Status);
}
/*
* Add the page to the process's working set
*/
MmInsertRmap(Page, AddressSpace->Process, (PVOID)PAGE_ROUND_DOWN(Address));
/*
* Finish the operation
*/
if (Locked)
{
MmLockPage(Page);
}
PageOp->Status = STATUS_SUCCESS;
KeSetEvent(&PageOp->CompletionEvent, IO_NO_INCREMENT, FALSE);
MmReleasePageOp(PageOp);
return(STATUS_SUCCESS);
内核函数 MmRequestPageMemoryConsumer()的作用是分配一个物理页面,如果内核中已经没有空闲的物理页面,就得把已经有较长时间没有得到访问的页面换出到页面倒换文件,腾出一些物理页面,然后再来分配。这个函数的第二个参数为真表示可以等待、即等待换出页面以腾出一些空闲的物理页面。这里的第一次 MmRequestPageMemoryConsumer()是不等待的,如果失败就再来一次,但这一次只好等待了。
获得了空闲的物理页面之后,当然需要在虚存页面与物理页面之间建立起映射,但是这里又有两种不同的情况。一种情况是,在发生本次页面异常之前相应的页面映射表项是0,说明这是全新的映射,此时所需的是一个空白的物理页面,所以只需建立映射就行了。另一种情况是相应的页面映射表项非0,说明原来是有物理页面的,只是其内容已经倒换出去,此时需要先从倒换文件中读入该页面的映像,然后才能建立映射。函数MmIsPageSwapEntry()就是用于这个判断的,这个函数返回非0表示已经有页面映像在倒换文件中
对于已被倒换出去的页面,代码中先通过 MmDeletePageFileMapping()从相应的页面映射表项中获取“倒换描述项”,即倒换页面所在的文件和位置。如前所述,当页面不在内存中时,相应页面表项 PTE的最低位为0,而其余31位就用来描述作为后备的倒换页面所在的文件和位置。获得了这些信息以后,就通过MmReadFromSwapPage()从页面倒换文件读入相应的页面映像。
MmReadFromSwapPage()
[MmNotPresentFault() > MmNotPresentFaultVirtualMemory()>MmReadFromSwapPage()]
NTSTATUS
NTAPI
MmReadFromSwapPage(SWAPENTRY SwapEntry, PFN_TYPE Page)
{
ULONG i, offset;
LARGE_INTEGER file_offset;
IO_STATUS_BLOCK Iosb;
NTSTATUS Status;
KEVENT Event;
UCHAR MdlBase[sizeof(MDL) + sizeof(ULONG)];
PMDL Mdl = (PMDL)MdlBase;
DPRINT("MmReadFromSwapPage\n");
if (SwapEntry == 0)
{
KEBUGCHECK(0);
return(STATUS_UNSUCCESSFUL);
}
i = FILE_FROM_ENTRY(SwapEntry);//高8位是文件号
offset = OFFSET_FROM_ENTRY(SwapEntry);//低24位是文件内部位移
if (i >= MAX_PAGING_FILES)//倒换文件的数量(32)
{
DPRINT1("Bad swap entry 0x%.8X\n", SwapEntry);
KEBUGCHECK(0);
}
if (PagingFileList[i]->FileObject == NULL ||
PagingFileList[i]->FileObject->DeviceObject == NULL)
{
DPRINT1("Bad paging file 0x%.8X\n", SwapEntry);
KEBUGCHECK(0);
}
MmInitializeMdl(Mdl, NULL, PAGE_SIZE);
MmBuildMdlFromPages(Mdl, &Page);
file_offset.QuadPart = offset * PAGE_SIZE;//实际的字节位移要乘上PAGESIZE
file_offset = MmGetOffsetPageFile(PagingFileList[i]->RetrievalPointers, file_offset);
KeInitializeEvent(&Event, NotificationEvent, FALSE);
Status = IoPageRead(PagingFileList[i]->FileObject,
Mdl,
&file_offset,
&Event,
&Iosb);
if (Status == STATUS_PENDING)
{
KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, NULL);
Status = Iosb.Status;
}
MmUnmapLockedPages(Mdl->MappedSystemVa, Mdl);
return(Status);
}
倒换描述项的高8位(实际上是高7位)是倒换文件号,低24位则是页面在文件内部的位移不过这个位移是以页面为单位的位移,所以实际上是页面号,实际的字节位移则还要乘上页面的大小。下面的MmGetOffsetPageFile()和IoPageRead()就是具体的文件操作了,在这里我们并不关心。
回到 MmNotPresentFaultVirtualMemory()的代码中,现在要在虚存页面与物理页面之间建立映射了,这是由 MmCreateVirtualMappingO完成的。所谓建立映射,就是要将物理页面号和页面保护模式(读/写/执行),以及访问权限DPL(0环或3环)等信息组合起来,形成一个最低位为1(表示页面在位)的页面映射表项 PTE(的值):再把这个值写到页面映射表中与给定虚存地址相对应的表项中(如果所属的目录项为0,则还要分配相应的二级映射表,并修改目录项)。这个操作的原理并不复杂,但是代码却颇为烦琐,限于篇幅这里就不深入下去了,有兴趣或需要的读者可以自行钻研。