1.3 虚拟内存访问
每个进程都拥有自己的虚拟地址空间,那么怎样才能访问这个空间呢?这就需要用到Windows API函数。这些函数直接与编写程序相关,因而更受软件工程师的关注。有关这方面的函数较多,这里介绍几个重要的函数。
1.3.1 获取系统信息
在一个程序中不能直接应用某个系统的设备参数,否则将不利于程序的移植。因此,如果确实需要用到这样的设备参数,则需要一个系统信息函数来获得。VC++ 编译器所提供这样的函数为GetSystemInfo()。该函数需要一个指向SYSTEM_INFO结构的指针作为参数。其原型表示为:
l
void GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);
l
其中lpSystemInfo返回LPSYSTEM_INFO结构的地址,用于装载适当的系统信息,这个结构体定义为:
l
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
};
};
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO;
l
其中参数含义如下所述。
dwOemId:是一个过时选项,用于与Windows NT 3.5以及以前的版本兼容。
wProcessorArchitecture:指明处理的结构,如Intel、Alpha、Intel 64位或Alpha 64位。
dwPageSize:用于显示CPU的页面大小。在x86 CPU上,这个值是4096字节。在Alpha CPU上,这个值是8192字节。在IA-64上,这个值是8192字节。
lpMinimumApplicationAddress:用于给出每个进程可用地址空间的最小内存地址。在Windows 98上,这个值是0x400000,因为每个进程的地址空间中下面的4MB是不能使用的。在Windows 2K/XP上,这个值是0x10000,因为每个进程的地址空间中开头的64KB总是空闲的。
lpMaximumApplicationAddress:用于给出每个进程可用地址空间的最大内存地址。在Windows 98上,这个地址是0x7FFFFFFF,因为共享内存映射文件区域和共享操作系统代码包含在上面的2GB分区中。在Windows XP上,这个地址是0x7FFEFFFF。
dwActiveProcessorMask:位屏蔽,指明哪个CPU是活动的。
dwNumberOfProcessors:计算机中CPU的数目。
dwProcessorType:处理器类型。
dwAllocationGranularity:保留的地址空间区域的分配粒度。
wProcessorLevel:进一步细分处理器的结构。
wProcessorRevision:用于进一步细分处理器的级别。
wReserved:保留供将来使用。
在以上参数中只有lpMinimumApplicationAddress、lpMaximumApplicationAddress、dwPageSize和dwAllocationGranularity与内存有关。
1.3.2 在应用程序中使用虚拟内存
对内存分配可以采用不同的方法,常用的方法有:用C/C++语言的内存分配函数,例如,用malloc() 和 free()、new 和 delete 函数分配和释放堆内存;用Windows传统的全局或者局部内存分配函数,如GlobalAlloc()和GlobalFree();用Win32的堆分配函数,如HeapAlloc()和HeapFree();用Win32的虚拟内存分配函数,如VirtualAlloc()和VirtualFree()。注意,用不同的方法分配内存后,要用相对应的函数来释放所占用的内存。这里只介绍Win32的虚拟内存分配函数。
在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。这时地址空间内的内存是不能使用的,必须通过VirtualAlloc()函数来分配其中的各个区域,对其进行保留。VirtualAlloc()函数原型为:
l
LPVOID VirtualAlloc(
LPVOID lpAddress,
DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect
);
l
该函数用来分配一定范围的虚拟页。参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。
分配完成后,即在进程的虚拟地址空间中保留了一个区域,可以对此区域中的内存进行保护权限许可范围内的访问。当不再需要访问此地址空间区域时,应释放此区域,由VirtualFree()负责完成。其函数原型为:
l
BOOL VirtualFree(
LPVOID lpAddress,
DWORD dwSize,
DWORD dwFreeType
);
l
其中参数含义如下所述。
lpAddress:指向待释放页面区域的指针。如果参数dwFreeType指定了MEM_RELEASE,则lpAddress必须为页面区域保留由VirtualAlloc()所返回的基地址。
dwSize:指定了要释放的地址空间区域的大小,如果参数dwFreeType指定了MEM_RELEASE标志,则将dwSize设置为0,由系统计算在特定内存地址上的待释放区域的大小。
dwFreeType:为所执行的释放操作的类型,其可能的取值为MEM_RELEASE和MEM_DECOMMIT,其中MEM_RELEASE标志指明要释放指定的保留页面区域,MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。
如果VirtualFree()执行完成,将回收全部范围的已分配页面,此后如再对这些已释 放页面区域内存进行访问将引发内存访问异常。释放后的页面区域可供系统继续分配 使用。
1.3.3 获取虚存状态
Windows API函数GlobalMemoryStatus()可用于检索关于当前内存状态的动态信息。在软件的About对话框中,通常用这个函数来获取系统内存的使用情况。其函数原型为:
l
void GlobalMemoryStatus(LPMEMORYSTATUS lpmstMemStat);
l
其中lpmstMemStat返回MEMORYSTATUS结构的地址,这个结构体的定义为:
l
typedef struct MEMORYSTATUS{
DWORD dwLength;
DWORD dwMemoryLoad;
DWORD dwTotalPhys;
DWORD dwAvailPhys;
DWORD dwTotalPageFile;
DWORD dwAvailPageFile;
DWORD dwTotalVirtual;
DWORD dwAvailVirtual;
} MEMORYSTATUS ,* LPMEMORYSTATUS;
l
其中参数含义如下所述。
dwLength:MEMORYSTATUS结构大小。
dwMemoryLoad:已使用内存所占的百分比。
dwTotalPhys:物理存储器的总字节数。
dwAvailPhys:空闲物理存储器的字节数。
dwTotalPageFile:页文件包含的最大字节数。
dwAvailPageFile:用户模式分区中空闲内存大小。
dwTotalVirtual:用户模式分区大小。
dwAvailVirtual:表示当前进程中还剩下的自由区域的总和。
在调用GlobalMemoryStatus()之前,必须将dwLength成员初始化为用字节表示的结构的大小,即一个MEMORYSTATUS结构的大小。这个初始化操作使得Microsoft能够在新版本Windows系统中将新成员添加到这个结构中,而不会破坏现有的应用程序。当调用GlobalMemoryStatus()时,它将对该结构的其余成员进行初始化并返回。
如果某个应用程序在内存大于4GB的计算机上运行,或者合计交换文件的大小大于4GB,那么可以使用新的GlobalMemoryStatusEx()函数。其函数的原型为:
l
BOOL GlobalMemoryStatusEx(MEMORYSTATUSEX &mst);
l
其中mst返回MEMORYSTATUSEX结构的填充信息,该结构体与原先的MEMORYSTATUS结构基本相同,差别在于新结构的所有成员的大小都是64位宽,因此它的值可以大于4 GB。
1.3.4 确定虚拟地址空间的状态
对内存的管理除了对当前内存的使用状态信息进行获取外,还经常需要获取有关进程的虚拟地址空间的状态信息。例如,如何得到一个进程已提交的页面范围?这就要用到两个 API函数VirtualQuery()或VirtualQueryEx()来进行查询。这两个函数的功能相似,不同就是VirtualQuery()只是查询本进程内存空间信息,而VirtualQueryEx()可以查询指定进程的内存空间信息。VirtualQuery()函数原型如下:
l
DWORD VirtualQuery(
LPVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
DWORD dwLength
);
l
VirtualQueryEx()函数原型如下:
l
DWORD VirtualQueryEx(
HANDLE hProcess ,
LPCVOID lpAddress ,
PMEMORY_BASIC_INFORMATION lpBuffer ,
DWORD dwLength
);
l
其中参数含义如下所述。
hProcess:进程的句柄。
lpAddress:想要了解其信息的虚存地址。
lpBuffer:返回MEMORY_ BASIC_INFORMATION结构的地址。
dwLength:返回的字节数。
PWEMORY_BASIC_INFORMATION的定义如下:
l
typedef struct _MEMORY_BASIC_INFORMATION{
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
DWORD RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION, * PMEMORY_BASIC_INFORMATION;
l
其中参数含义如下所述。
BaseAddress:被查询内存块的基地址。
AllocationBase:用VirtualAlloc()分配该内存时实际分配的基地址。
AllocationProtect:分配该页面时,页面的一些属性,如PAGE_READWRITE、PAGE_EXECUTE等(其他属性可参考 Platform SDK)。
RegionSize:从BaseAddress开始,具有相同属性的页面的大小。
State:页面的状态,有3种可能值:MEM_COMMIT、MEM_FREE和MEM_ RESERVE,这个参数是最重要的,从中可知指定内存页面的状态。
Protect:页面的属性,它可能的取值与 AllocationProtect 相同。
Type:指明了该内存块的类型,有3种可能值:MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE。
1.3.5 改变内存页面保护属性
在进行进程挂钩时,经常要向内存页中写入部分代码,这就需要改变内存页的保护属性。有幸的是Win32提供了两个API函数VirtualProtect()和VirtualProtectEx(),它们可以对改变内存页保护。例如,在使用这两个函数时,可以先按PAGE_READWRITE属性来提交一个页的地址,并且立即将数据填写到该页中,然后再把该页的属性改变为PAGE_READONLY,这样可以有效地保护数据不被该进程中的任何其他线程重写。在调用这两个函数之前最好先了解有关页面的信息,可以通过VirtualQuery()来实现。
VirtualProtect()与VirtualProtectEx()函数的区别在于VirtualProtect()只适用于本进程,而VirtualProtectEx()可以适用于其他进程。VirtualProtect()函数原型如下:
BOOL VirtualProtect(
PVOID pvAddress,
DWORD dwSize,
DWORD flNewProtect,
PDWORD pflOldProtect
);
l
VirtualProtectEx()函数原型如下:
l
BOOL VirtualProtectEx(
HANDLE hProcess,
PVOID pvAddress,
DWORD dwSize,
DWORD flNewProtect,
PDWORD pflOldProtect
);
l
其中参数的含义如下所述。
hProcess:要修改内存的进程句柄。
pvAddress:指向内存的基地址(它必须位于进程的用户方式分区中)。
dwSize:用于指明想要改变保护属性的字节数。
flNewProtect:代表PAGE_*保护属性标志中的任何一个标志,但PAGE_ WRITECOPY和PAGE_EXECUTE_WRITECOPY这两个标志除外。
pflOldProtect:是DWORD大小的地址,VirtualProtect()和VirtualProtectEx()将用原先与pvAddress位置上的字节相关的保护属性填入该地址。尽管许多应用程序并不需要该信息,但是必须为该参数传递一个有效地址,否则该函数的运行将会失败。
1.3.6 进行一个进程的内存读写
前面已经说明了如何获得一个进程的内存属性、如何分配内存和如何改变内存页的保护属性,其最终的目的是要对一个进程中内存内容进行读写。要完成此工作,需要用到两个函数:ReadProcessMemory() 和WriteProcessMemory(),这两个函数非常有用。如果知道了一个进程的句柄和内存地址,就可以用ReadProcessMemory()函数来得到该进程和该地址中的内容,此函数的原型为:
l
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead
);
l
其中hProcess为要读入的进程句柄,lpBaseAddress为读内存的起始地址,lpBuffer为读入数据的地址,nSize为要读入的字节数,lpNumberOfBytesRead为实际读入的字 节数。
同样,如果知道了一个进程的句柄和内存地址,可以用WriteProcessMemory()函数向该进程和该地址中写入新的内容,这个函数的原型为:
l
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten
);
l
其中参数hProcess为要写入的进程句柄,lpBaseAddress为写内存的起始地址,lpBuffer为写入数据的地址,nSize为要写入的字节数,lpNumberOfBytesWritten为实际写入的字节数。