3.1.6 系统调用NtAllocateVirtualMemory()1

系列文章目录


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()预留所需的倒换页面。不过倒换页面的预留并不等于分配,实际的分配要到真正需要的时候才进行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值