📄
字号:
NtOpenThreadToken()
NtCreateProfile()
NtDuplicateObject()[/code]
我们当然不可能在这里逐一考察所有这些系统调用,而只是顺着前面所说Jeffrey Richter的实验考察几个关键的系统调用。首先是在别的进程的用户空间分配一个内存区间,这是由NtAllocateVirtualMemory()实现的。
[code]NTSTATUS STDCALL
NtAllocateVirtualMemory(IN HANDLE ProcessHandle,
IN OUT PVOID* UBaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG URegionSize,
IN ULONG AllocationType,
IN ULONG Protect)
{
PEPROCESS Process;
MEMORY_AREA* MemoryArea;
. . . . . .
/*
* Check the validity of the parameters
*/
if ((Protect & PAGE_FLAGS_VALID_FROM_USER_MODE) != Protect)
{
return(STATUS_INVALID_PAGE_PROTECTION);
}
if ((AllocationType & (MEM_COMMIT | MEM_RESERVE)) == 0)
{
return(STATUS_INVALID_PARAMETER);
}
PBaseAddress = *UBaseAddress;
PRegionSize = *URegionSize;
BoundaryAddressMultiple.QuadPart = 0;
BaseAddress = (PVOID)PAGE_ROUND_DOWN(PBaseAddress);
RegionSize = PAGE_ROUND_UP(PBaseAddress + PRegionSize) -
PAGE_ROUND_DOWN(PBaseAddress);
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
NULL,
UserMode,
(PVOID*)(&Process),
NULL);
. . . . . .
Type = (AllocationType & MEM_COMMIT) ? MEM_COMMIT : MEM_RESERVE;
DPRINT("Type %x\n", Type);
AddressSpace = &Process->AddressSpace;
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(Process, AddressSpace,
MEMORY_AREA_VIRTUAL_MEMORY,
&BaseAddress, RegionSize, Protect,
&MemoryArea,
PBaseAddress != 0,
(AllocationType & MEM_TOP_DOWN) == MEM_TOP_DOWN,
BoundaryAddressMultiple);
. . . . . .
MemoryAreaLength = (ULONG_PTR)MemoryArea->EndingAddress -
(ULONG_PTR)MemoryArea->StartingAddress;
MmInitialiseRegion(&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);
}[/code]
先要着重说一下参数AllocationType。这是一些标志位,主要有MEM_RESERVE、MEM_COMMIT、MEM_RESERVE、MEM_RESET、以及MEM_TOP_DOWN。调用者通过这些标志位说明调用NtAllocateVirtualMemory()的意图。Windows内核把虚存空间的分配与映射区分了开来:
标志位MEM_RESERVE表示要求“预订”、即分配一个虚拟地址区间。正如前一篇漫谈中所述,虚拟地址区间的分配只是“账面”上的操作,而并不涉及页面映射表的改变,所以并没有建立起有关页面的映射。要建立页面映射,就得为有关的虚存页面提供物理的存储、或者说后备。就Windows而言,这种物理的存储有两种形式。一种是物理的内存页面,另一种是磁盘上的Swap文件。这样,一旦为一个虚存页面建立了映射,这个页面就要么体现为内存中的某个物理页面,要么体现为Swap文件中的某个页面(也是物理页面),这两种形态之间的转换就是页面的换入/换出。从某种意义上说,映射的建立类似于所预订资源的兑现,为此就得投入相应的资源(Swap文件页面或内存页面)作为代价,类似于“现金交割”,这就是标志位MEM_COMMIT所表示的意思。所以,虚存区间的分配实际上分成预订和交割两项操作,这两项操作既可以分两步走,也可以一步到位。如果是分两步走,就要先后调用NtAllocateVirtualMemory()两次,第一次把MEM_RESERVE设成1,第二次把MEM_COMMIT设成1。也就是说:先预订,再交割。而若要一步到位,只调用NtAllocateVirtualMemory()一次,那就把这两个标志位都设成1。如果我们探讨这套方案的设计者的初衷,那么显然是要人们分两步走、甚至分多步走,目的是要减小Swap文件的大小。假定我们要分配一个512MB的虚存区间,如果要立即就建立映射,那么就要在Swap文件中提供512MB的空间,相当于一订货就把全部货款都付清了。但是,实际上往往并非所有这512MB的存储空间都是立即就要使用的,所以更好的办法是先预定,然后要用多少就交割多少,不用了就退掉,这样就可以少占Swap文件的空间、从而可以减小Swap文件的大小。Jeffrey Richter的书中对此有比较详细的叙述。
但是,这当然不是唯一的方法,例如Linux就不采用这样的方法。在Linux中根本就不分甚么预订和交割,分配内存区间就是分配内存区间,也并不是在分配内存区间的时候就在Swap盘区上分配页面作为类似于“保证金”那样的后备,而是在真正需要的时候才动态分配Swap页面。这一方面可能是因为Linux基本上都是用一个磁盘或盘区作为Swap空间,不像Windows那样采用Swap文件而有文件大小的压力,另一方面结构上也比较简洁。不过这两种方法应该说是各有千秋,而并无绝对的好坏或高下。按说ReactOS在各个方面都在尽力模仿Windows,但是在这方面却实际上采用了类似于Linux的方法,这一点下面就可以看到。
另一方面,在前一篇漫谈中我们看到的是映像文件的映射,而映像文件本身就起着相当于Swap文件的作用,而给定映像文件的大小本来就是固定的,所以不存在要设法减小其文件大小的问题。
明白了这些,下面就可以看NtAllocateVirtualMemory()的代码了。
首先,程序中局部量Type 的值来自AllocationType,不是MEM_COMMIT就是MEM_RESERVE,二者必居其一。不过MEM_COMMIT也可能蕴含了MEM_RESERVE,因为两步可以并为一步走。
参数UbaseAddress表示对于起点地址的要求,为0表示任意。UbaseAddress为0时参数ZeroBits表示要求所分配的起点地址前面有几位(二进制位)必须是0,实际上就是要求所分配的区间大体上落在什么位置上。如果UbaseAddress非0,这里的代码中就通过MmLocateMemoryAreaByAddress()找一下,看这地址是否落在某个已分配的区间内。如果是的话(返回的MemoryArea指针非0),此时有三种可能:
● 这个区间在此前已经通过NtAllocateVirtualMemory()预订或交割,因而其类型为MEMORY_AREA_VIRTUAL_MEMORY,区间也够大。现在要做的是改变其一部或全部的状态,例如设置成MEM_COMMIT、以及所要求的访问访问模式(例如可读写或可执行等等)。
● 这个区间是通过别的手段、而不是NtAllocateVirtualMemory()分配的。例如Section的映射也会导致空间的分配,但是此时的类型不是MEMORY_AREA_VIRTUAL_MEMORY(而是。。。。。。。。。)。只要区间够大,也允许通过NtAllocateVirtualMemory()改变其一部或全部的状态。
● 这个区间不够大,因而失败返回。
在ReactOS的实现中,数据结构MEMORY_AREA代表着内存区间,在同一个内存区间中可以存在一个或多个Region,以数据结构MM_REGION作为代表。我们既已称Area为区间,就只好称Region为“区段”了。之所以在一个区间中可以有多个区段,是因为它们的访问模式可能不同。例如可能要需要把一个区间的一部分设置成可执行,另一部分设置成只读,还有一部分设置成读写等等。此外,它们的状态也可能不同,例如在一次预订以后分好几次交割,因而可能有的区段状态为MEM_RESERVE,而有的是MEM_COMMIT。而所谓Region,是指一块连续的,“均匀”的即具有相同模式和状态的虚存区段。所以前面有个参数名是URegionSize,而不是UAreaSize。此外,MEMORY_AREA中的Type字段表示一个区间的性质和类型,例如MEMORY_AREA_VIRTUAL_MEMORY,而MM_REGION中的Type字段则表示区段的状态,例如MEM_COMMIT或MEM_RESERVE。
所以,如果找到了相应的区间,就通过MmAlterRegion()改变目标区段的模式和状态。注意调用MmAlterRegion()时的最后一个参数是个函数指针,在这里指向MmModifyAttributes()。如果MmAlterRegion()发现所要求的空间可用,就会通过函数指针调用这个函数,其作用是对页面映射表作出相应的修改,以适应可能与前不同的访问模式,例如把只读改成读写。
读者也许会问:如果把一个区段的状态从MEM_RESERVE改成MEM_COMMIT,这到底是否涉及Swap文件的页面分配呢?前面讲过,ReactOS目前实际上采用的是类似于Linux的那种方法,所以只是改变了区段的状态,而并没有涉及Swap文件的页面分配,甚至没有涉及页面映射的建立。那这套机制怎么工作呢?当第一次访问某个页面时,CPU会因为页面无映射而发生异常,而异常处理程序会根据引起异常的地址找到相应的区段并检查其状态,如果是MEM_RESERVE就作为出错,而若是MEM_COMMIT则为其分配物理页面和建立映射,并在Swap文件中也分配好后备页面。
如果并未指定起点地址,或者所指定的起点地址并不落在某个已分配的区间中,那就比较自由了,此时通过MmCreateMemoryArea()分配一个地址区间并创建其MEMORY_AREA数据结构,再通过MmInitialiseRegion()创建其第一个MM_REGION数据结构。
然后,如果参数AllocationType中的MEM_COMMIT标志位为1,就通过MmReserveSwapPages()记下一笔帐,以保留一定数量的Swap页面。不过ReactOS在这方面的程序还很粗糙,只能大致看出个意图。
由于本文的目的不在于存储管理,这里就不在这些问题上再深入下去了。
在目标进程的用户空间分配了一个虚存区间以后,就可以对其进行读写了。我们在这里特别感兴趣的是写入,因为Jeffrey Richter把一段程序拷贝到了另一个进程的用户空间。当然,由于这是在另一个进程的用户空间,不能像通常那样直接按地址指针随机写入,而需要通过另一个系统调用NtWriteVirtualMemory()来进行成块的写入(拷贝)。
[code]NTSTATUS STDCALL
NtWriteVirtualMemory(IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG NumberOfBytesToWrite,
OUT PULONG NumberOfBytesWritten OPTIONAL)
{
NTSTATUS Status;
PMDL Mdl;
PVOID SystemAddress;
PEPROCESS Process;
ULONG OldProtection = 0;
PVOID ProtectBaseAddress;
ULONG ProtectNumberOfBytes;
. . . . . .
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -