系列文章目录
文章目录
3.1.6 系统调用NtAllocateVirtualMemory()
在前面各节的基础上,现在可以用一个完整的情景加以贯穿和综合了,这个情景就是系统调用NtAllocateVirtualMemory()。
凡是用户空间的程序向内核要求分配空间,无论是只要求一个指标即保留一片区间,还是要求交割兑现已经保留的一片区间,还是二者兼有,都可以通过系统调用ZwAllocateVirtualMemory()即NtAllocateVirtualMemory()实现。实际上还有别的功能也可以通过这个系统调用实现,但是在这里我们并不关心。当然,所分配的仅限于用户空间,用户空间的程序是无法要求内核分配系统空间的。NtAllocateVirtualMemory()是个不小的系统调用,我们分段阅读其代码:
NtAllocateVirtualMemory()
/*
* @implemented
*/
NTSTATUS STDCALL
NtAllocateVirtualMemory(IN HANDLE ProcessHandle,
IN OUT PVOID* UBaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG URegionSize,
IN ULONG AllocationType,
IN ULONG Protect)
/*
* FUNCTION: Allocates a block of virtual memory in the process address space
* ARGUMENTS:
* ProcessHandle = The handle of the process which owns the virtual memory
* BaseAddress = A pointer to the virtual memory allocated. If you
* supply a non zero value the system will try to
* allocate the memory at the address supplied. It round
* it down to a multiple of the page size.
* ZeroBits = (OPTIONAL) You can specify the number of high order bits
* that must be zero, ensuring that the memory will be
* allocated at a address below a certain value.
* RegionSize = The number of bytes to allocate
* AllocationType = Indicates the type of virtual memory you like to
* allocated, can be a combination of MEM_COMMIT,
* MEM_RESERVE, MEM_RESET, MEM_TOP_DOWN.
* Protect = Indicates the protection type of the pages allocated, can be
* a combination of PAGE_READONLY, PAGE_READWRITE,
* PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_GUARD,
* PAGE_NOACCESS
* RETURNS: Status
*/
{
PEPROCESS Process;
MEMORY_AREA* MemoryArea;
ULONG_PTR MemoryAreaLength;
ULONG Type;
NTSTATUS Status;
PMADDRESS_SPACE AddressSpace;
PVOID BaseAddress;
ULONG RegionSize;
PVOID PBaseAddress;
ULONG PRegionSize;
PHYSICAL_ADDRESS BoundaryAddressMultiple;
....
/* Check for valid protection flags */
if ((Protect & PAGE_FLAGS_VALID_FROM_USER_MODE) != Protect)
{
DPRINT1("Invalid page protection\n");
return STATUS_INVALID_PAGE_PROTECTION;
}
/* Check for valid Zero bits */
if (ZeroBits > 21)
{
DPRINT1("Too many zero bits\n");
return STATUS_INVALID_PARAMETER_3;
}
/* Check for valid Allocation Types */
if ((AllocationType &~ (MEM_COMMIT | MEM_RESERVE | MEM_RESET | MEM_PHYSICAL |
MEM_TOP_DOWN | MEM_WRITE_WATCH)))
{
DPRINT1("Invalid Allocation Type\n");
return STATUS_INVALID_PARAMETER_5;
}
/* Check for at least one of these Allocation Types to be set */
if (!(AllocationType & (MEM_COMMIT | MEM_RESERVE | MEM_RESET)))
{
DPRINT1("No memory allocation base type\n");
return STATUS_INVALID_PARAMETER_5;
}
/* MEM_RESET is an exclusive flag, make sure that is valid too */
if ((AllocationType & MEM_RESET) && (AllocationType != MEM_RESET))
{
DPRINT1("MEM_RESET used illegaly\n");
return STATUS_INVALID_PARAMETER_5;
}
/* MEM_WRITE_WATCH can only be used if MEM_RESERVE is also used */
if ((AllocationType & MEM_WRITE_WATCH) && !(AllocationType & MEM_RESERVE))
{
DPRINT1("MEM_WRITE_WATCH used without MEM_RESERVE\n");
return STATUS_INVALID_PARAMETER_5;
}
/* MEM_PHYSICAL can only be used with MEM_RESERVE, and can only be R/W */
if (AllocationType & MEM_PHYSICAL)
{
/* First check for MEM_RESERVE exclusivity */
if (AllocationType != (MEM_RESERVE | MEM_PHYSICAL))
{
DPRINT1("MEM_PHYSICAL used with other flags then MEM_RESERVE or"
"MEM_RESERVE was not present at all\n");
return STATUS_INVALID_PARAMETER_5;
}
/* Then make sure PAGE_READWRITE is used */
if (Protect != PAGE_READWRITE)
{
DPRINT1("MEM_PHYSICAL used without PAGE_READWRITE\n");
return STATUS_INVALID_PAGE_PROTECTION;
}
}
PBaseAddress = *UBaseAddress;
PRegionSize = *URegionSize;
BoundaryAddressMultiple.QuadPart = 0;
BaseAddress = (PVOID)PAGE_ROUND_DOWN(PBaseAddress);
RegionSize = PAGE_ROUND_UP(PBaseAddress + PRegionSize) -
PAGE_ROUND_DOWN(PBaseAddress);
/*
* We've captured and calculated the data, now do more checks
* Yes, MmCreateMemoryArea does similar checks, but they don't return
* the right status codes that a caller of this routine would expect.
*/
if (BaseAddress >= MM_HIGHEST_USER_ADDRESS)
{
DPRINT1("Virtual allocation above User Space\n");
return STATUS_INVALID_PARAMETER_2;
}
if (!RegionSize)
{
DPRINT1("Region size is invalid\n");
return STATUS_INVALID_PARAMETER_4;
}
if (((ULONG_PTR)MM_HIGHEST_USER_ADDRESS - (ULONG_PTR)BaseAddress) < RegionSize)
{
DPRINT1("Region size would overflow into kernel-memory\n");
return STATUS_INVALID_PARAMETER_4;
}
/*
* Copy on Write is reserved for system use. This case is a certain failure
* but there may be other cases...needs more testing
*/
if ((!BaseAddress || (AllocationType & MEM_RESERVE)) &&
((Protect & PAGE_WRITECOPY) || (Protect & PAGE_EXECUTE_WRITECOPY)))
{
DPRINT1("Copy on write is not supported by VirtualAlloc\n");
return STATUS_INVALID_PAGE_PROTECTION;
}
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
NULL,
UserMode,
(PVOID*)(&Process),
NULL);
if (!NT_SUCCESS(Status))
{
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
Type = (AllocationType & MEM_COMMIT) ? MEM_COMMIT : MEM_RESERVE;
DPRINT("Type %x\n", Type);
AddressSpace = (PMADDRESS_SPACE)&Process->VadRoot;
MmLockAddressSpace(AddressSpace);
if (PBaseAddress != 0)
{
MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, BaseAddress);
if (MemoryArea != NULL)
{
MemoryAreaLength = (ULONG_PTR)MemoryArea->EndingAddress -
(ULONG_PTR)MemoryArea->StartingAddress;
if (MemoryArea->Type == MEMORY_AREA_VIRTUAL_MEMORY &&
MemoryAreaLength >= RegionSize)
{
Status =
MmAlterRegion(AddressSpace,
MemoryArea->StartingAddress,
&MemoryArea->Data.VirtualMemoryData.RegionListHead,
BaseAddress, RegionSize,
Type, Protect, MmModifyAttributes);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
else if (MemoryAreaLength >= RegionSize)
{
Status =
MmAlterRegion(AddressSpace,
MemoryArea->StartingAddress,
&MemoryArea->Data.SectionData.RegionListHead,
BaseAddress, RegionSize,
Type, Protect, MmModifyAttributes);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
else
{
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
return(STATUS_UNSUCCESSFUL);
}
}
}
Status = MmCreateMemoryArea(AddressSpace,
MEMORY_AREA_VIRTUAL_MEMORY,
&BaseAddress,
RegionSize,
Protect,
&MemoryArea,
PBaseAddress != 0,
AllocationType & MEM_TOP_DOWN,
BoundaryAddressMultiple);
if (!NT_SUCCESS(Status))
{
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
MemoryAreaLength = (ULONG_PTR)MemoryArea->EndingAddress -
(ULONG_PTR)MemoryArea->StartingAddress;
MmInitializeRegion(&MemoryArea->Data.VirtualMemoryData.RegionListHead,
MemoryAreaLength, Type, Protect);
if ((AllocationType & MEM_COMMIT) &&
((Protect & PAGE_READWRITE) ||
(Protect & PAGE_EXECUTE_READWRITE)))
{
MmReserveSwapPages(MemoryAreaLength);
}
*UBaseAddress = BaseAddress;
*URegionSize = MemoryAreaLength;
DPRINT("*UBaseAddress %x *URegionSize %x\n", BaseAddress, RegionSize);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
return(STATUS_SUCCESS);
}
先看参数。
用户空间的虚拟存储区间是进程的财产,不同的进程有不同的用户空间,为进程A分配其用户空间的某个虚存区间,不等于进程B、进程C的用户空间中的同一区间也受到了分配。一般而言,用户空间的程序只能为当前进程分配空间。也就是说,只有本进程可以为自己分配内存区间,除此以外就只有内核可以为任何进程分配内存区间了,一个进程是不能越俎代庖为别的进程分配内存的。这是一般规则,Linux就是这样做的。如果严格地遵循这个规则,这里的第一个参数 ProcessHandle就失去了意义。然而 Windows在这里表现出了它的特殊性,在Windows中一个进程也可以替别的进程分配内存。不过为此先要打开目标进程,取得代表目标进程的句柄,就是这里的第一个参数ProcessHandle。当然,在绝大多数情况下该目标进程就是当前进程本身。
第二、第三、第四个参数用于区间的起始地址和长度。用户空间的程序可以通过指针UBaseAddress要求把所需的区间分配在这个地址上,长度至少为URegionSize。如果给定的地址非0,内核就必须照办,如果不能满足要求就失败返回。如果UBaseAddress为0就表示由内核参考另一个参数 ZeroBits 办事,此时参数 UBaseAddress和 UReeionSize 用来返回实际分配的地址和长度。另一方面,此时参数 ZeroBits用于决定实际分配的位置,表示实际分配的地址必须以多少个0为前导,例如10个0就表示应该分配在最低的4MB之内,但是所要求的前导0的个数最多不能超过21个。不过,ZeroBits的作用在目前的ReactOS代码中尚未实现。
参数AllocationType是表示分配类型的一组标志位。其中最重要的是MEM_RESERVE和MEM_COMMIT。前者表示“保留”虚存区间,即只分配未经物理映射的虚存区间,或者说只分配“指标”而不分配实际的物理存储:后者则表示“交割”映射到虚存区间的物理存储。这两者可以并存,表示一次到位地分配建立了物理映射的虚存区间。此外,标志位MEM_TOP_DOWN表示:如果不指定地址而由内核自由分配,就应分配尽可能高的地址,反之则分配尽可能低的地址。我们在前面看过函数 MmFindGap()的代码,标志位MEM_TOPDOWN 就用来指示搜索的方向。最后一个参数 Protect 则表示对该区间的访问权限,例如可读、可写、可执行等。
函数开头的部分基本上都是对参数的合理性检查,有兴趣或需要的读者可以自己阅读,这里就不多作解释了。注意代码中对起始地址和区间长度都做了取整和边界对齐,起始地址是向下调整到与页面边界对齐,长度则向上调整到与页面边界对齐,一个页面的大小是4KB。
我们继续往下看:
[NtAllocateVirtualMemory()]
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
NULL,
UserMode,
(PVOID*)(&Process),
NULL);
if (!NT_SUCCESS(Status))
{
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
Type = (AllocationType & MEM_COMMIT) ? MEM_COMMIT : MEM_RESERVE;
DPRINT("Type %x\n", Type);
AddressSpace = (PMADDRESS_SPACE)&Process->VadRoot;
MmLockAddressSpace(AddressSpace);
用户空间的虚存区间属于进程,是按进程管理的,所以先要根据目标进程的句柄找到其数据结构,这是由ObReferenceObiectByHandle()完成的。这个函数通过参数Process返回指向目标进程EPROCESS 数据结构的指针。有了目标进程的EPROCESS结构之后,就可以进而找到其MADDRESS_SPACE 数据结构了,这就是代码中的指针AddressSpace 所指。
找到了描述目标进程虚存空间的数据结构,就可以根据参数按要求分配存储空间了。而分配的类型,在这里设定为MEM_COMMIT或MEM_RESERVE。换言之,如果要求对未经保留的区间执行 MEM COMMIT,则意味着既要对该区间执行MEM RESERVE又要执行MEM COMMIT。
由于具体分配内存区间的操作涉及队列操作,需要互斥进行,所以此后的操作都要由MmLockAddressSpace()和MmUnlockAddressSpace()加以保护。
[NtAllocateVirtualMemory()]
MmLockAddressSpace(AddressSpace);
if (PBaseAddress != 0)
{//指定了地址
MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, BaseAddress);
if (MemoryArea != NULL)
{ //找到了包含BaseAddress的已分配(已保留或也交割)区间
MemoryAreaLength = (ULONG_PTR)MemoryArea->EndingAddress -
(ULONG_PTR)MemoryArea->StartingAddress;
if (MemoryArea->Type == MEMORY_AREA_VIRTUAL_MEMORY &&
MemoryAreaLength >= RegionSize)
{ //类型为普通虚存区间且长度符合要求,改变目标区块的类型(状态)和属性
Status =
MmAlterRegion(AddressSpace,
MemoryArea->StartingAddress,
&MemoryArea->Data.VirtualMemoryData.RegionListHead,
BaseAddress, RegionSize,
Type, Protect, MmModifyAttributes);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
else if (MemoryAreaLength >= RegionSize)
{//长度也符合,但是区间类型是用于Section
Status =
MmAlterRegion(AddressSpace,
MemoryArea->StartingAddress,
&MemoryArea->Data.SectionData.RegionListHead,
BaseAddress, RegionSize,
Type, Protect, MmModifyAttributes);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
else
{//区间长度不够,失败
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
return(STATUS_UNSUCCESSFUL);
}
}
}
如果指定了起始地址,那就先试着通过MmLocateMemoryAreaByAddress()找到该地址所在的节点,即已经分配的区间。如果找到就说明以前就已经分配了包含该地址的区间,所谓已经分配是已经保留或已经交割。如果这个区间的长度符合要求,就通过MmlterRegion0)改变其中所要求区段的类型和保护模式。我们已在前面读过这两个函数的代码,其中比较复杂的是MmAlterRegion0)。这个函数设置已分配区间内某个区段的类型(其实是状态)与属性,例如将一个“已保留”区段的类型设置成“已交割”,并设置其页面保护模式,这里面可能涉及原有区段的分割和合并。如果在此过程中做出了一些改变,就会调用作为参数传下去的一个“改变函数”AlterFunc,在这里就是MmModifyAttributes().
MmModifyAttributes()
[NtAllocateVirtualMemory()>MmAlterRegion()>MmModifyAttributes()]
VOID static
MmModifyAttributes(PMADDRESS_SPACE AddressSpace,
PVOID BaseAddress,
ULONG RegionSize,
ULONG OldType,
ULONG OldProtect,
ULONG NewType,
ULONG NewProtect)
/*
* FUNCTION: Modify the attributes of a memory region
*/
{
/*
* If we are switching a previously committed region to reserved then
* free any allocated pages within the region
*/
if (NewType == MEM_RESERVE && OldType == MEM_COMMIT)
{
...//从已交割返回已保留,释放已分配的物理页面
}
/*
* If we are changing the protection attributes of a committed region then
* alter the attributes for any allocated pages within the region
*/
if (NewType == MEM_COMMIT && OldType == MEM_COMMIT &&
OldProtect != NewProtect)
{
ULONG i;
for (i=0; i < PAGE_ROUND_UP(RegionSize)/PAGE_SIZE; i++)
{
if (MmIsPagePresent(AddressSpace->Process,
(char*)BaseAddress + (i*PAGE_SIZE)))
{//改变目标区段中各在位页面的保护模式
MmSetPageProtect(AddressSpace->Process,
(char*)BaseAddress + (i*PAGE_SIZE),
NewProtect);
}
}
}
}
这个函数的代码表明,在两种情况下需要改变页面的映射和保护模式。一种是原来是MEM_COMMIT 而现在变成了MEM_RESERVE,这种情况发生于要拆除物理映射而不是要建立物理映射的时候,此时要释放目标区段中已经在使用的物理页面,但是在这里我们不感兴趣。第二种是原来就是MEM_COMMIT,现在也仍是MEM_COMMIT,但是保护模式发生了改变,例如从只读变成了可读可写。此时要通过MmSetPageProtect()修改页面映射表中相应页面的保护模式,但是这只适用于当时在内存的页面。
读者可能感到困惑,分配存储区间时最典型的变化应该是把一个原来是MEM_RESERVE的区块变成 MEM_COMMIT,这时最需要把区块的类型变化落实到页面映射的变化,怎么这里倒反而不理不睬呢?这个问题后面还要细讲,这里先简单提一下:那要到用户程序真的去访问这个区间,从而引起页面访问异常时才来处理,才来分配物理页面并建立页面映射。所以,现在涉及的只是改变原来已有映射的页面的保护模式
这里还有个问题,如果目标区块的类型(其实是状态)原来就是MEM_COMMIT,新的类型仍是MEM_COMMIT,保护模式也没有改变,这时会发生什么呢?什么也不会发生,此时的MmAlterRegion()相当于一次空操作,但是会成功返回。
回到 NtAllocateVirtualMemory()的代码,如果用户并未指定区间的起始地址,即参数UBaseAddress为0,或者用户指定的地址尚未被分配,即在目标空间中找不到包含这个地址的已分配区间,那就要由内核为之分配一块大小合适的区间。我们看下面的代码:
[NtAllocateVirtualMemory()]
Status = MmCreateMemoryArea(AddressSpace,
MEMORY_AREA_VIRTUAL_MEMORY,
&BaseAddress,
RegionSize,
Protect,
&MemoryArea,
PBaseAddress != 0,
AllocationType & MEM_TOP_DOWN,
BoundaryAddressMultiple);
if (!NT_SUCCESS(Status))
{
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
DPRINT("NtAllocateVirtualMemory() = %x\n",Status);
return(Status);
}
MemoryAreaLength = (ULONG_PTR)MemoryArea->EndingAddress -
(ULONG_PTR)MemoryArea->StartingAddress;
MmInitializeRegion(&MemoryArea->Data.VirtualMemoryData.RegionListHead,
MemoryAreaLength, Type, Protect);
if ((AllocationType & MEM_COMMIT) &&
((Protect & PAGE_READWRITE) ||
(Protect & PAGE_EXECUTE_READWRITE)))
{//预留倒换页面
MmReserveSwapPages(MemoryAreaLength);
}
*UBaseAddress = BaseAddress;//返回实际分配的地址
*URegionSize = MemoryAreaLength;//返回实际分配的长度
DPRINT("*UBaseAddress %x *URegionSize %x\n", BaseAddress, RegionSize);
MmUnlockAddressSpace(AddressSpace);
ObDereferenceObject(Process);
return(STATUS_SUCCESS);
先通过 MmCreateMemoryArea()在目标空间中分配创建一个区间。注意这里的参数 BaseAddress可能是0,也可能非0。如果是0就说明由内核自由分配,此时标志位MEM_TOP_DOWN仍旧决定着搜索的方向。如果非0就表示必须分配在指定的地址上。
由于是用于普通的用户空间虚存,其类型为MEMORY_AREA_VIRTUAL_MEMORY。再由MmInitializeRegion()将该区间中唯一的区块设置成所要求的类型(其实是状态,例如MEM_COMMIT)和保护模式。然后,如果是MEM_COMMIT,并且是可写的区块,就要在页面倒换文件中预留(并非分配)所需的倒换页面,因为已交割的区间是需要以倒换文件上的页面作为后盾的。
我们已经在前面看过 MmCreateMemoryArea()的代码,这个函数在目标空间的二叉树中插入了一个MEMORY_AREA 节点,表示地址区间的占用。一个区间里面可以有属性不同的多个区块,但是刚建立的区间只有一个区块,是由MmInitializeRegion()创建并设置成给定的类型和保护模式的。
MmInitializeRegion()
[NtAllocateVirtualMemory()>MmAlterRegion()]
VOID
NTAPI
MmInitializeRegion(PLIST_ENTRY RegionListHead, ULONG Length, ULONG Type,
ULONG Protect)
{
PMM_REGION Region;
Region = ExAllocatePoolWithTag(NonPagedPool, sizeof(MM_REGION),
TAG_MM_REGION);
Region->Type = Type;
Region->Protect = Protect;
Region->Length = Length;
InitializeListHead(RegionListHead);
InsertHeadList(RegionListHead, &Region->RegionListEntry);
}
这段代码就无须解释了。
前面说过,新建的区间可以是只保留不交割,也可以是既保留又交割。如果是后者,则这里的TyPe为MEM_COMMIT。凡是要交割而又是可写的区块,就应该要有倒换文件中的页面作为后盾所以这里先通过 MmReserveSwapPages()预留所需的倒换页面。不过倒换页面的预留并不等于分配,实际的分配要到真正需要的时候才进行。