《WINDOWS 环境下32位汇编语言程序设计》第10章 内存管理和文件操作(1)

本章涉及内存的分配、释放,以及文件I/O的有关操作,之所以把这两部分集中在一章中,是因为它们的关系比较密切------进行与文件有关的操作之前往往需要分配内存,以便用做文件读写的缓冲区,而要将内存中的数据保存起来往往需要写文件。

Windows操作系统中有一个新的概念:内存映射文件。通过这个功能将文件的内容直接映射到内存中,并使用读写内存的方法来对文件进行读写,内存映射文件是内存管理函数的一种,但它必须与文件操作函数配合使用。

本章的10.1节介绍与内存管理相关的内容,接下来介绍与文件、目录、磁盘操作有关的内容。由于内存映射文件要涉及文件操作,所以放在最后一节讨论。

10.1 内存管理

10.1.1 内存管理基础

Win32中的内存管理是分层次的,系统提供了几组层次不同的函数来管理内存,它们是标准内存管理函数、堆管理函数、虚拟内存管理函数和内存映射文件函数。所有的这些函数都是为了让用户能在比较高的层次上方便地管理内存,以便将程序和底层的内存分页机制隔离开来。如图10.1所示,这几组函数的层次是各不相同的。

图10.1 Windows的内存分层管理

Windows使用一个以页为基础的虚拟内存系统,与分页有关的概念已经在第1章的1.3.2节中有所介绍。Windows充分利用了80x86处理器保护模式下的线性寻址机制和分页机制,这些机制是Win32内存管理的基础。Win32提供了一组虚拟内存管理函数来管理虚拟内存,主要用于保留/提交/释放虚拟内存,在虚拟内存页上改变保护方式、锁定虚拟内存页,以及查询一个进程的虚拟内存等操作,这是一组位于底层的函数。

堆管理函数相对比较高级一点,堆的主要功能就是有效地管理内存和进程的地址空间。DOS操作系统下的C语言中就已经有了"堆"的概念,这时的"堆"是程序初始化时向操作系统申请并预留的大块内存,程序通过C函数在这块空间中申请和释放内存。

在Win32中,进程可以使用的整个地址空间就是一个堆。并且"堆"的概念又被引申了一步:Win32中分两种堆,一种是进程的"默认堆",默认堆只有一个,指的就是可以使用的整个地址空间;另一种是"动态堆",也称为"私有堆",私有堆类似于DOS下C语言中使用的那种堆,一个进程可以随意建立多个私有堆,也可以随意将它们释放,私有堆全部位于默认堆中,从概念上看,它和默认堆并没有什么不同,就像一个跨国公司和属下的子公司同样都是按照公司的规程操作一样。使用堆管理函数可以对所有的私有堆和默认堆进行操作。

标准内存管理函数总是在默认堆中分配和释放内存,这组函数就是常规意义上的内存管理函数。

内存映射文件函数相对比较独立,它是为了文件操作的方便性而设立的,当对文件进行操作的时候,一般总是先打开文件,然后申请一块内存用做缓冲区,再将文件数据循环读入并处理,当文件长度大于缓冲区长度的时候需要多次读入,每次读入后处理缓冲区边界位置的数据往往是个麻烦的问题。曾经介绍过Windows可以使用磁盘文件当做虚拟内存(参考图1.5,虚拟内存的实现),内存映射文件函数使用同样的办法将一个文件直接映射到进程的地址空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容。

对比这些函数,可以发现它们涉及的系统资源是各不相同的,如表10.1所示。

表10.1 不同内存管理函数的操作对象

10.1.2 内存的当前状态

在第1章中已经介绍过,一个进程可以寻址的地址空间是4 GB,但用户可以直接管理的地址空间是多大呢?实际上,高端的2 GB是供操作系统内核使用的,其中安排了操作系统的代码和数据(Windows 9x中还包括共享内存映射的地址空间),可供应用程序使用的地址空间是低端的2 GB,这2 GB除去应用程序与用户DLL等的代码和静态数据段以后,余下来的才是内存管理函数可以使用的地址空间,应用程序和用户DLL的大小一般只有几兆字节到几十兆字节,所以可以认为能自由使用的地址空间基本上是2GB。

既然用户可以使用的地址空间大概为2 GB,读者千万不要认为就可以申请2 GB的内存了,因为这2 GB只是可以使用的"地址"空间,而不是可以使用的"内存"空间,可分配内存的大小还受制于物理内存和磁盘交换文件的大小。因为物理内存和磁盘交换文件是供整个系统和所有用户程序使用的,所有系统内核、当前执行的所有用户程序的代码、数据,以及分配的内存总量并不能超过物理内存和磁盘交换文件的总和。

当设计一个可能需要申请大量内存的程序时,如何预先得知系统的配置情况呢?对此可以使用GlobalMemoryStatus函数:

复制代码
invoke GlobalMemoryStatus,lpBuffer

lpBuffer指向一个MEMORYSTATUS结构,结构的定义如下:

复制代码
    MEMORYSTATUS STRUCT
      dwLength            DWORD      ?      ;本结构的长度
      dwMemoryLoad        DWORD      ?      ;已用内存的百分比
      dwTotalPhys         DWORD      ?      ;物理内存总量
      dwAvailPhys         DWORD      ?      ;可用物理内存
      dwTotalPageFile     DWORD      ?      ;交换文件总的大小
      dwAvailPageFile     DWORD      ?      ;交换文件中空闲部分大小
      dwTotalVirtual      DWORD      ?      ;用户可用的地址空间
      dwAvailVirtual      DWORD      ?      ;当前空闲的地址空间
    MEMORYSTATUS ENDS

在调用之前需要首先将dwLength字段设置为MEMORYSTATUS结构的长度,当调用GlobalMemoryStatus函数后,函数会在结构中返回对应的数值。注意:dwTotalPageFile字段返回的是交换文件的最大值,并不是当前实际建立的交换文件的大小,一般当前的交换文件大小会小于这个数值,但这个数值的大小也不是确定的,如果需要的话,系统会增加它的大小直到不再有空余的磁盘空间放置交换文件为止。

在所附光盘的Chapter10\MemInfo目录中的MemInfo.asm文件利用这个功能定时获取并显示当前内存的使用信息,部分源代码如下:

复制代码
                      .const
    szInfo       db            '物理内存总数     %lu字节',0dh,0ah
                      db       '空闲物理内存     %lu字节',0dh,0ah
                      db       '虚拟内存总数     %lu字节',0dh,0ah
                      db       '空闲虚拟内存     %lu字节',0dh,0ah
                      db       '已用内存比例     %d%%',0dh,0ah
                      db       '------------------------------------------------',0dh,0ah
                      db       '用户地址空间总数 %lu字节',0dh,0ah
                      db       '用户可用地址空间 %lu字节',0dh,0ah,0
                      .code
    _GetMemInfo proc
                      local    @stMemInfo:MEMORYSTATUS
                      local    @szBuffer[1024]:byte
                      mov      @stMemInfo.dwLength,sizeof @stMemInfo
                      invoke   GlobalMemoryStatus,addr @stMemInfo
                      invoke   wsprintf,addr @szBuffer,addr szInfo,\
                               @stMemInfo.dwTotalPhys,@stMemInfo.dwAvailPhys,\
                @stMemInfo.dwTotalPageFile,\
                @stMemInfo.dwAvailPageFile,\
                @stMemInfo.dwMemoryLoad,\
                @stMemInfo.dwTotalVirtual,@stMemInfo.dwAvailVirtual
                      invoke  SetDlgItemText,hWinMain,IDC_INFO,addr @szBuffer
                      ret
    _GetMemInfo endp
    ...

程序每隔1秒钟在_GetMemInfo子程序中用GlobalMemoryStatus获取内存状态并用wsprintf将数据转换成字符串,然后显示在对话框的IDC_INFO文本框中。

在笔者的计算机中,程序运行的结果如图10.2所示,计算机的物理内存配置为128MB,这个数值和物理内存总数(dwTotalPhys字段)符合,dwMemoryLoad字段为72%,等于空闲物理内存(dwAvailPhys字段)和物理内存总数的百分比与100%的差值,计算机上当前虚拟内存交换文件大小为192 MB,小于最大限制dwTotalPageFile字段。

图10.2 Meminfo程序的运行结果

在与地址空间相关的数值上,dwTotalVirtual字段的显示结果是2147352576,等于2 GB减去128 KB,这是因为这2 GB的最低端的和最高端的两个64 KB是系统保留的(00000000h~0000ffffh,7fff0000h~7fffffffh)。

Windows可以根据内存使用的需求自动调整交换文件的大小,比如,Meminfo程序运行显示的当前磁盘交换文件可用空间(dwAvailPageFile字段)为168 MB,但是如果尝试申请一个高达300 MB的内存块,会发现仍然可以申请成功,这时dwTotalPageFile字段的大小会自动增长到500多兆字节,把内存块释放掉的话,dwTotalPageFile字段会恢复到原来的数值。虚拟内存的使用给我们带来了很多的方便,我们可以使用超过物理内存好几倍的内存空间,但是如果所需的内存大大高于物理内存的大小,那么申请内存还是会失败,因为这会引起物理内存和交换文件之间的数据频繁交换,大量的磁盘请求将使系统性能降低到没有实际使用的意义,读者可以尝试在128 MB物理内存的计算机上申请一个1 GB的内存块,即使拥有远远大于1 GB的磁盘剩余空间供交换文件使用,也是不会成功的!如果读者需要使用大大高于物理内存的内存空间,可以尝试自己进行磁盘交换工作。

【完整的代码笔记】

MemInfo.rc文件内容

复制代码
#include		<resource.h>

#define	ICO_MAIN		1000
#define	DLG_MAIN		100
#define	IDC_INFO		101

ICO_MAIN	ICON		"Main.ico"

DLG_MAIN DIALOG 188, 193, 240, 75
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "内存状态"
FONT 9, "宋体"
{
 LTEXT "", IDC_INFO, 6, 6, 235, 65
}

源代码:

复制代码
;MemInfo.asm----------------------获取并显示当前内存使用情况
;---------------------------------------------------------------------
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff MemInfo.asm
;rc MemInfo.rc
;Link /subsystem:windows MemInfo.obj MemInfo.res

.386
.model flat, stdcall 
option casemap:none 

;include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

;equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 100 
IDC_INFO 	equ 101

;数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
.const 
szInfo	byte '物理内存总数     %lu 字节  %lu KB  %lu MB',0dh,0ah
		byte '空闲物理内存     %lu 字节  %lu KB  %lu MB',0dh,0ah
		byte '虚拟内存总数     %lu 字节',0dh,0ah
		byte '空闲虚拟内存     %lu 字节',0dh,0ah
		byte '已用内存比例     %d%%',0dh,0ah
		byte '------------------------------------------------',0dh,0ah
		byte '用户地址空间总数 %lu 字节',0dh,0ah
		byte '用户可用地址空间 %lu 字节',0dh,0ah,0
 
;代码段
.code 
_GetMemInfo proc 
	local @stMemInfo:MEMORYSTATUS
	local @szBuffer[1024]:byte 
	local @totalPhysKB:dword 
	local @totalPhysMB:dword 
	local @availPhysKB:dword 
	local @availPhysMB:dword 
	
	mov @stMemInfo.dwLength, sizeof @stMemInfo 
	invoke GlobalMemoryStatus, addr @stMemInfo 
	
	mov eax, @stMemInfo.dwTotalPhys
	shr eax, 10 				;除以1024转成KB单位 
	mov @totalPhysKB, eax 
	shr eax, 10 				;除以1024转成MB单位 
	mov @totalPhysMB, eax 
	mov eax, @stMemInfo.dwAvailPhys
	shr eax, 10 				;除以1024转成KB单位 
	mov @availPhysKB, eax 
	shr eax, 10 				;除以1024转成MB单位 
	mov @availPhysMB, eax 
	
	invoke wsprintf, addr @szBuffer, addr szInfo, \
				@stMemInfo.dwTotalPhys, @totalPhysKB, @totalPhysMB, \
				@stMemInfo.dwAvailPhys, @availPhysKB, @availPhysMB, \
				@stMemInfo.dwTotalPageFile, @stMemInfo.dwAvailPageFile, \
				@stMemInfo.dwMemoryLoad, \
				@stMemInfo.dwTotalVirtual, @stMemInfo.dwAvailVirtual
	invoke SetDlgItemText, hWinMain, IDC_INFO, addr @szBuffer 
	ret 
_GetMemInfo endp 

;--------------------------------------------------------------------
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	mov eax, wMsg 
	.if eax == WM_TIMER 
		call _GetMemInfo 
	.elseif eax == WM_CLOSE 
		invoke KillTimer, hWnd, 1 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke LoadIcon, hInstance, ICO_MAIN 
		invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax 
		invoke SetTimer, hWnd, 1, 1000, NULL 
		call _GetMemInfo 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

编译运行(XP系统运行)

10.1.3 标准内存管理函数

标准内存管理函数的功能是在进程的默认堆中申请和释放内存块,它由下面一些函数组成:GlobalAlloc、GlobalFree和GlobalReAlloc函数分别用来申请、释放和修改内存大小;GlobalLock和GlobalUnlock函数用来进行锁定操作;而GlobalDiscard,GlobalFlags,GlobalHandle和GlobalSize等函数用来丢弃内存或获取已分配内存的一些信息。

这组函数是为了向后兼容而保留的,在Win16平台下,内存有"全局"和"本地"之分,全局堆(Global Heap)是系统中所有进程所共有的堆,包括系统进程及用户进程,它们使用上述Global开头的函数来使用全局堆中的内存。而每个进程又拥有一个私有的本地堆(Local Heap),进程使用一组Local开头的函数来使用本地堆中的内存,函数名称和上述Global开头的函数一一对应。

在Win16下,所有进程在全局堆中申请的内存会交错在一起,从而使得一个用户进程不小心的内存越界存取会导致整个操作系统的崩溃。到了Win32下,考虑到安全因素,可以被所有进程使用的全局堆被废弃,本地堆则改名为进程堆(Process Heap),也就是默认堆。

为了兼容性,GlobalAlloc/LocalAlloc等函数被沿用了下来,但Win32下两组函数完全相同,它们都是在底层直接调用HeapAlloc从默认堆中分配内存。

由于GlobalAlloc/LocalAlloc等函数是为了兼容性目的而保留的,MSDN中明确说明不建议继续使用,而是推荐使用下一节讲到的HeapAlloc等堆管理函数。

用标准内存管理函数可以分配的内存有两种:固定地址的内存块和可移动的内存块,而可移动的内存块又可以进一步定义为可丢弃的,让我们逐步来讨论它们的不同。

1.固定的内存块

常规意义上的内存就是固定的内存块,因为申请到内存后,这块内存的线性地址是固定不变的。要申请一块固定的内存,可以使用函数:

复制代码
invoke GlobalAlloc, GMEM_FIXED or GMEM_ZEROINIT, dwBytes
.if eax
    mov lpMemory,eax
.endif

第一个参数是标志,GMEM_FIXED表示申请的是固定的内存块,GMEM_ZEROINIT表示需要将内存块中的所有字节预先初始化为0,也可以简单地使用GPTR标志,它就相当于是GMEM_FIXED or GMEM_ZEROINIT;第2个参数dwBytes指出了需要申请的是以字节为单位的内存大小。如果内存申请失败,eax中返回NULL,否则返回值是一个指向内存块起始地址的指针,用户需要保存这个指针,在使用内存或者释放内存的时候还要用到它。

如果要释放一个先前申请的固定内存块,可以使用GlobalFree函数:

复制代码
invoke GlobalFree,lpMemory

如果释放成功,函数返回NULL,否则函数返回的值就是输入的lpMemory。程序在不再使用内存块的时候应该使用这个函数将内存释放,即使程序在退出的时候忘记了释放内存,Windows也会自动将它们释放。

在实际使用中往往需要改变一个内存块的大小,这时候就要用到GlobalReAlloc函数,这个函数可以缩小或者扩大一块已经申请到的内存:

复制代码
invoke GlobalReAlloc, lpMemory, dwBytes, uFlags
.if eax
    mov lpNewMemory, eax
.endif

lpMemory是先前申请的内存块指针,dwBytes是新的大小,如果这个数值比原来申请的时候要小,也就是需要缩小内存块,那么uFlags标志参数可以是NULL。操作不成功时函数的返回值为0,否则返回新的缩小了的内存块指针,当然,这个指针和原来的指针肯定是一样的。

但是需要扩大一个内存块的时候,情况就稍微有些复杂了。让我们做一个实验来模拟这样一种情况:首先申请两个1000h大小的固定内存块,得到两个指针,读者可以发现第二块几乎紧接第一块内存,一般情况下如果第一块内存的地址是X,那么第二块内存的地址几乎就是X+1000h,如果需要将第一个内存块扩大到2000h字节,那么只能在别的地方开辟一个2000h大小的内存块,因为原来位置后面的1000h已经被第二块内存占用了,这就意味着新的指针可能和原来的不一样。

可以在GlobalReAlloc函数中通过指定不同的uFlags来规定是否允许Windows在必要的时候移动内存块。当uFlags中有GMEM_MOVEABLE选项的时候,如果需要移动内存块,Windows会在别的地方开辟一块新的内存,并把原来内存块中的内容自动复制到新的内存块中,这时函数的返回值是新的指针,原来的指针作废。

如果不指定GMEM_MOVEABLE选项,那么只有当内存块后面扩展所需的空间没有被使用时,函数才会执行成功,否则,函数失败并返回NULL,这时原来的指针继续有效。

为了保证内存块扩大成功,建议总是使用下面的语句来扩大和缩小内存:

复制代码
invoke GlobalReAlloc, lpMemory, dwBytes, GMEM_ZEROINIT or GMEM_MOVEABLE
.if eax
    mov lpMemory,eax
.endif

指定GMEM_ZEROINIT选项可以使内存块扩大的部分自动被初始化为0,然后程序判断返回值,如果改变大小成功的话,则用新的指针替换原来的指针,其他和原来指针有关的值也不要忘了同时更新。

2.可移动的内存块

可移动的内存块在不使用的时候允许Windows改变它的线性地址,为什么要使用可移动的内存块呢?唯一的理由是防止内存的碎片化,当进程长时间频繁地申请和释放不同大小的内存块后,申请的大量小块内存可能零散地分布在整个地址空间中,虽然空闲的内存总数不小,但是却没有剩下连续的大块空闲地址,导致无法再申请大块的内存。

解决内存碎片化的办法很简单,因为碎片之间有大量的内存是空闲的,只要允许Windows移动小块的在用内存,就可以将碎片合并成大块的空闲内存,但是在用内存被移动后,程序中对应的指针也要随着改变,不然就会访问到错误的地址,而且,在使用内存的过程中,内存需要有个锁定的过程,否则用到一半的时候被Windows移动了,结果依然是错误的,只有程序将内存解锁,Windows才可以自由移动它们,这就引申出可移动内存块的概念和操作的基本方法。

要申请一个可移动的内存块,使用的函数还是GlobalAlloc,但需要使用不同的参数:

复制代码
invoke GlobalAlloc, GMEM_MOVEABLE or GMEM_ZEROINIT, dwBytes
.if eax
    mov hMemory,eax
.endif

GMEM_MOVEABLE标志指定了分配的内存是可移动的,GMEM_ZEROINIT同样表示将申请到的内存块的内容初始化为0(也可以用GHND标志,它就相当于GMEM_MOVEABLE or GMEM_ZEROINIT);如果内存申请失败,eax中返回NULL,成功的话,返回值是一个句柄而不是内存指针,用户需要保存这个句柄,在锁定或释放内存的时候还要用到它。一个进程可以申请的可移动内存的块数最大不能超过65536个,申请固定内存块时则没有数量限制。

要使用可移动内存之前,需要把它锁定,这相当于告诉Windows现在程序要使用这块内存了,不能将它移动,锁定内存使用GlobalLock函数:

复制代码
invoke GlobalLock,hMemory
.if eax
    mov lpMemory, eax
.endif

函数的入口参数是GlobalAlloc返回的内存句柄,如果锁定成功,函数返回一个指针,程序可以用使用固定内存块同样的方法来使用它;如果锁定失败,则函数返回NULL。每次锁定返回的指针位置可能是不同的,但内存块中的数据不会变化。

当程序暂时不需要操作这块内存的时候,应该将它解锁,否则和使用固定的内存块就没有区别了,解锁使用GlobalUnlock函数:

复制代码
invoke GlobalUnlock, hMemory

函数的参数同样是GlobalAlloc返回的句柄,解锁成功的话函数返回非0值。读者可能有个问题:在多线程的程序中,两个地方同时锁定内存,但当一个地方还在使用的情况下另一个地方却调用GlobalUnlock将内存解锁了怎么办?其实不用担心这个问题,Windows为每个可移动的内存句柄维护一个锁定计数,每次锁定内存的时候计数加1,解锁的时候计数减1,只有当计数为0的时候内存才真正被解锁,所以只要程序中的GlobalLock函数和GlobalUnlock函数是配对的,就不用担心这个问题。

要释放一个可移动的内存块,同样使用GlobalFree函数:

复制代码
invoke GlobalFree, hMemory

但使用的参数是GlobalAlloc返回的内存句柄,如果释放成功,函数返回NULL。不管内存当前是否处在锁定状态,都可以被成功释放。

调整可移动内存块的大小,同样使用GlobalReAlloc函数:

复制代码
invoke GlobalReAlloc,hMemory,dwBytes,GMEM_ZEROINIT or GMEM_MOVEABLE

如果调整成功,返回值就是输入的hMemory,失败的话,返回值是NULL。即使内存块在锁定状态,函数仍然可以调用成功,但由于这时候内存块可能已经被移动了位置,原来用GlobalLock函数获取的指针可能已经失效了,所以调整可移动内存块的大小最好还是先将内存解锁,等调整完毕以后再锁定使用。

使用可移动内存块来防止内存碎片化的方法听起来很不错,但不幸的是,这只在Win16下有效。由于在Win32下上述函数仅仅是为了兼容性目的而存在的,系统仅仅保证这些函数在使用上不会出错,而在底层已经不再去做碎片拼合的工作了。而HeapAlloc等函数又不支持可移动内存块机制,所以在Win32下要防止内存碎片化,最好的办法还是使用内存池,有兴趣的读者可以自行查阅相关资料。

3.可丢弃的内存块

分配可移动内存块的时候需要使用GMEM_MOVEABLE标志,如果同时配合使用GMEM_DISCARDABLE标志的话,生成的内存块是可丢弃的内存块,表示当Windows急需内存使用的时候,可以将它从物理内存中丢弃,可丢弃的内存块首先必须是可移动的内存块。函数调用如下:

复制代码
invoke GlobalAlloc,GHND or GMEM_DISCARDABLE, dwBytes
.if eax
    mov hMemory, eax
.endif

当用GlobalLock锁定内存的时候,如果返回NULL指针,表示内存已经被Windows丢弃了,当然其中的数据也丢失了,程序需要重新生成数据。当内存块被丢弃的时候,内存句柄还是有效的,如果程序还要使用这个句柄,那么可以对它使用GlobalReAlloc函数来重新分配内存。

当可丢弃内存块的锁定计数为0时,程序也可以使用GlobalDiscard函数主动将它丢弃,这与Windows将它丢弃的效果是一样的:

复制代码
invoke GlobalDiscard,hMemory

使用内存函数时有两个地方需要特别注意:

(1)NULL指针的检测------很多函数都返回内存指针,在使用指针前一定要检测它的有效性,如果使用了函数执行失败而返回的NULL指针来访问数据,会导致程序越权访问不该访问的地方,从而被Windows毫不留情地终止掉,这就是例子代码中总是有一个if语句来判断eax是否为NULL的原因。

(2)注意访问越界问题------越界操作也会引起越权访问,千万不要到超出内存块长度的地方去访问,例如,使用lstrcpy之类的函数处理字符串之前,先用lstrlen检测字符串长度是一个好习惯。

10.1.4 堆管理函数

Windows的"堆"分为默认堆和私有堆两种。默认堆是在程序初始化时由操作系统自动创建的,所有的标准内存管理函数都是在默认堆中申请内存的;而私有堆相当于在默认堆中保留了一大块内存,用堆管理函数可以在这个保留的内存块中分配内存。一个进程的默认堆只有一个,而私有堆可以被创建多个。

默认堆可以直接被使用,而私有堆在使用前需要先创建,看上去要麻烦一点,但实际上,有些时候使用私有堆可能更有好处。

首先,可以使用默认堆的函数有多种,而它们可能在不同的线程中同时对默认堆进行操作,为了保持同步,对默认堆的访问是顺序进行的,也就是说,在同一时间内每次只有一个线程能够分配和释放默认堆中的内存块。如果两个线程试图同时分配默认堆中的内存块,那么只有一个线程能够进行,另一个线程必须等待第一个线程的内存块分配结束之后才能继续执行。而私有堆的空间是预留的,不同线程在不同的私有堆中同时分配内存并不会引起冲突,所以整体的运行速度更快。

其次,当系统必须在物理内存和页文件之间进行页面交换的时候,系统的性能会受到很大的影响,在某些情况下,使用私有堆可以防止系统频繁地在物理内存和交换文件之间进行数据交换,因为将经常访问的内存局限于一个小范围地址的话,页面交换就不太可能发生,把频繁访问的大量小块内存放在同一个私有堆中就可以保证它们在内存中的位置接近。

再则,使用私有堆也有利于封装和保护模块化的程序。当程序包含多个模块的时候,如果使用标准内存管理函数在默认堆中分配内存,那么所有模块分配的内存块是交叉排列在一起的,如果模块A中的一个错误导致内存操作越界,可能会覆盖掉模块B使用的内存块,到模块B执行的时候出错了,我们却很难发现错误的源头来自于模块A。如果让不同的模块使用自己的私有堆,那么它们使用的内存就会完全隔离开来,虽然越界错误仍然可能发生,但很容易跟踪和定位。

最后,使用私有堆也使大量内存的清理变得方便,在默认堆中分配的内存需要一块块单独释放,但将一个私有堆释放后,在这个堆里的内存就全部被释放掉了,并不需要预先释放堆中的每个内存块,这样非常便于模块的扫尾工作。

1.私有堆的创建和释放

创建私有堆的函数是HeapCreate:

复制代码
invoke HeapCreate,flOptions,dwInitialSize,dwMaximumSize
.if eax && (eax < 0c0000000h)
    mov hHeap, eax
.endif

flOptions参数是标志,用来指定堆的属性,可以指定的属性有HEAP_NO_SERIALIZE和HEAP_GENERATE_EXCEPTIONS两种。

HEAP_GENERATE_EXCEPTIONS标志用来指定函数失败时的返回值,不指定这个标志的话,函数失败时返回NULL,否则返回一个具体的出错代码,以便于程序详细了解出错原因。出错代码的定义值都大于0c0000000h,因为0c0000000h开始的地址空间为系统使用,分配的内存地址不可能高于这个地址,所以检测函数执行是否成功的时候可以使用上面的测试语句来比较返回值是否在0~0c0000000h之间。

HEAP_NO_SERIALIZE标志用来控制对私有堆的访问是否要进行独占性的检测,前面曾经提到在默认堆中申请内存块的操作是顺序进行的,多个线程同时申请内存的请求只有一个能马上执行,其他将处于等待状态,对于一个私有堆来说,这个限制仍然存在,当从堆中分配内存时,系统有下面的操作步骤:

(1)遍历已分配的和空闲的内存块的链接表。

(2)寻找一个空闲内存块的地址。

(3)通过将空闲内存块标记为"已分配"来分配新内存块。

(4)将新内存块添加给内存块链接表。

当两个线程几乎同时在同一个堆中申请内存时,如果第一个线程执行了(1)、(2)两步的时候被系统切换到第二个线性,线程2同样又执行(1)、(2)两步,那么它们找到的空闲内存块就会是同一块内存,结果可想而知。解决问题的办法就是让单个线程独占对堆和它的链接表的访问权,当一个线程全部执行了这4个步骤后才允许第二个线程开始第一个步骤。

在用默认参数建立的堆中申请内存,系统会进行独占的检测工作,当然这要花费一定的系统开销。但是当以下情况存在时,可以保证不会同时有多个线程在同一个堆中申请内存:

● 进程只使用一个线程。

● 进程使用多个线程,但是每个线程只访问属于自己的私有堆。

● 进程使用多个线程,但程序中已经有其他措施来保证它们不会同时去访问同一个私有堆。

在这些情况下,可以指定HEAP_NO_SERIALIZE标志来建立私有堆,这样建立的堆不会进行独占性的检测,访问速度可以更快。

参数dwInitialSize指定创建堆时分配给堆的物理内存,随着堆中内存的分配,当这些内存被使用完时,堆的长度可以自动扩展。dwMaximumSize参数指定了能够扩展到的最大值,当扩展到最大值时再尝试在堆中分配内存的话就会失败,这个值决定了系统给堆保留的连续地址空间的大小,函数会自动将这两个参数的数值调整为页面大小的整数倍。如果dwMaximumSize参数的值指定为0,那么堆没有最大值限制,扩展范围只受限于空闲的内存总量。如果dwMaximumSize指定为非0值,在堆中申请的最大单个内存块不能大于7FFF8h(相当于524 KB),dwMaximumSize指定0的话就没有这个限制。

如果一个私有堆不再需要了,可以通过调用HeapDestroy函数将它释放:

复制代码
invoke HeapDestroy,hHeap

释放私有堆可以释放堆中包含的所有内存块,也可以将堆占用的物理内存和保留的地址空间全部返还给系统。如果函数运行成功,返回值是TRUE。当在进程终止的时候没有调用HeapDestroy函数将私有堆释放时,系统会自动释放。

虽然在默认堆中的内存申请主要使用标准内存管理函数,而堆管理函数的主要管理对象是私有堆,但是如果编程者愿意的话,也可以用堆管理函数在默认堆中分配内存,毕竟默认堆也是一个堆,但这样的话首先需要有一个句柄来代表默认堆,默认堆的句柄不能用HeapCreate来创建,但可以用GetProcessHeap函数来获取,这个函数没有输入参数,如果执行成功则返回默认堆的句柄。注意:因为这个句柄是"获取"的而不是"创建"的,所以不能调用HeapDestroy来释放它,如果对它调用HeapDestroy函数,系统会将它忽略。

2.在堆中分配和释放内存块

如果要在堆中分配内存块,可以使用HeapAlloc函数:

复制代码
invoke HeapAlloc,hHeap,dwFlags,dwBytes
.if eax && (eax < 0c0000000h)
    mov lpMemory, eax
.endif

hHeap参数就是前面创建堆时返回的堆句柄(或者使用GetProcessHeap函数得到的默认堆句柄),用来表示在哪个堆中分配内存,dwBytes是需要分配的内存块的字节数,dwFlags是标志,它可以是下面值的组合:

● HEAP_NO_SERIALIZE------当使用HeapCreate时指定了HEAP_NO_SERIALIZE标志,以后这个堆中使用的所有HeapAlloc函数都不进行独占检测。如果使用HeapCreate时没有指定HEAP_NO_SERIALIZE标志,可以在这里使用HEAP_NO_SERIALIZE标志单独指定对本次分配操作不进行独占检测。

● HEAP_GENERATE _EXCEPTIONS------如果申请内存失败函数返回具体的出错原因,而不仅返回一个NULL。同样,当使用HeapCreate时指定了此标志的情况下,在这里就不必再一次指定。

● HEAP_ZERO_MEMORY------将分配的内存用0初始化。

当函数分配内存成功的时候,返回值是指向内存块第一个字节的指针,如果分配内存失败,返回值要视dwFlags的设置,如果没有指定HEAP_GENERATE_EXCEPTIONS标志,那么返回值为NULL,否则,返回值可能是下面的数值:

● STATUS_NO_MEMORY------取值为0C0000017h,表示内存不够。

● STATUS_ACCESS_VIOLATION------取值为0C0000005h,表示参数不正确或者堆被破坏。

在堆中分配的内存块只能是固定地址的内存块,不像GlobalAlloc函数一样可以分配可移动的内存块。如果要释放分配到的内存块,可以使用HeapFree函数:

复制代码
invoke HeapFree, hHeap, dwFlags, lpMemory

hHeap参数是堆句柄,lpMemory是HeapAlloc函数返回的内存块指针,dwFlags参数中也可以使用HEAP_NO_SERIALIZE标志,含义与使用HeapAlloc时相同。当函数执行成功的时候,返回值为非0值,执行失败则函数返回0。

对于用HeapAlloc分配的内存块,也可以使用HeapReAlloc重新调整大小:

复制代码
invoke HeapReAlloc,hHeap,dwFlags,lpMemory,dwBytes
.if eax && (eax < 0c0000000h)
    mov lpMemory, eax
.endif

其中dwBytes指定了新的大小,dwFlags为标志,可以组合指定的标志有:

● HEAP_GENERATE_EXCEPTIONS------参见HeapAlloc函数的说明。

● HEAP_NO_SERIALIZE------参见HeapAlloc函数的说明。

● HEAP_ZERO_MEMORY------当扩大内存块的时候,将新增的部分初始化为0,当缩小内存的时候,本参数无效。

● HEAP_REALLOC_IN_PLACE_ONLY------与GlobalReAlloc函数类似,当内存块的高处已经被其他内存块占据的时候,要扩大内存块必须将它移动位置,当没有指定这个标志的时候,函数会在需要的时候自动移动内存块,如果指定了这个标志,则不允许内存块移动,这时,当内存块高处不是空闲的时候,函数的执行会失败。

如果函数执行成功,返回值是指向新内存块的指针,显而易见,当缩小或扩大内存块时指定了HEAP_REALLOC_IN_PLACE_ONLY标志,则这个指针必定和原来的相同,否则的话,它既有可能和原来的指针相同也有可能不同。

3.其他堆管理函数

除了上面的一些函数,堆管理函数中还有HeapLock,HeapUnlock,GetProcessHeaps,HeapCompact,HeapSize,HeapValidate和HeapWalk等函数。

GetProcessHeaps函数用来列出进程中所有的堆(注意:不要和用来获取默认堆句柄的GetProcessHeap函数搞混),HeapWalk用来列出一个堆中所有的内存块,HeapValidate函数用来检验一个堆中所有内存块的有效性。这3个函数平时很少使用,一般在调试的时候使用。

GetProcessHeaps函数的用法是:

复制代码
invoke GetProcessHeaps,NumberOfHeaps,lpHeaps

其中lpHeaps是一个指针,指向用来接收堆句柄的缓冲区,NumberOfHeaps参数指定了这个缓冲区中可以存放句柄的数量,显然,缓冲区的长度应该等于NumberOfHeaps乘以4字节。函数执行后,进程中所有堆的句柄全部返回到缓冲区中,其中也包括默认堆的句柄。

HeapWalk函数的用法是:

复制代码
.repeat
    invoke  HeapWalk,hHeap,lpEntry
    push     eax
    ;检测缓冲区中的内存块信息
    pop      eax
.until   !eax

hHeap是需要操作的堆句柄,lpEntry指向一个包含有PROCESS_HEAP_ENTRY结构的缓冲区。调用HeapWalk函数时,函数每次在PROCESS_HEAP_ENTRY结构中返回一个内存块的信息,如果还有其他内存块,函数返回TRUE,程序可以一直循环调用HeapWalk函数直到函数返回FALSE为止。在多线程的程序中使用HeapWalk,必须首先使用HeapLock函数将堆锁定,否则调用会失败。

HeapValidate用来验证堆的完整性或堆中某个内存块的完整性:

复制代码
invoke HeapValidate, hHeap, dwFlags, lpMemory

其中hHeap指定要验证的堆。如果lpMemory为NULL,那么函数顺序验证堆中所有的内存块;如果lpMemory指定了一个内存块,则只验证这个内存块。dwFlags是标志,可以指定HEAP_NO_SERIALIZE标志。如果验证结果是所有的内存块都完好无损,函数返回非0值,否则函数返回0。

HeapLock函数和HeapUnlock函数用来锁定堆和解锁堆。这两个函数主要用于线程的同步,当在一个线程中调用HeapLock函数时,这个线程暂时成为这个堆的所有者,也就是说只有这个线程能对堆进行操作(包括分配内存、释放、调用HeapWalk等函数),在别的线程中对这个堆的操作会等待在那里,直到所有者线程调用HeapUnlock解锁为止。这两个函数的语法如下:

复制代码
invoke HeapLock, hHeap
invoke HeapUnlock, hHeap

如果函数执行成功,返回值为非0值,否则函数返回0。一般来说,很少在程序中使用这两个函数,而总是使用HEAP_NO_SERIALIZE标志来进行同步控制,指定了这个标志的话,HeapAlloc,HeapReAlloc,HeapSize和HeapFree等函数会在内部自己调用HeapLock和HeapUnlock函数。

HeapCompact函数用于合并堆中的空闲内存块并释放不在使用中的内存页面:

复制代码
invoke   HeapCompact,hHeap,dwFlags

HeapSize函数返回堆中某个内存块的大小,这个大小就是使用HeapAlloc,以及HeapReAlloc时指定的大小:

复制代码
invoke   HeapSize,hHeap,dwFlags,lpMemory

lpMemory指定了需要返回大小的内存块,函数的返回值是内存块的大小,如果执行失败,函数返回−1。

10.1.5 虚拟内存管理函数

不管某个进程实际可用的物理内存是多少,每个进程可以使用的地址空间总是2 GB,用户程序不必考虑一个线程地址对应的物理内存究竟安排在什么地方------是在真正的物理内存中?在磁盘交换文件中?还是根本没有物理内存与之对应。

一个进程的整个地址空间是客观存在的,但是否有内存与该段地址空间中的地址相关联是另外的问题,Windows负责在适当的时间把线程地址映射到物理内存或磁盘上的交换文件上,这就是虚拟内存的基本概念。

在程序运行的时候,进程中每个地址都可以处于下列3种状态的1种中:

● 占用状态------线程地址已经映射到实际的物理内存中。也称为已提交状态。

● 自由状态------没有映射到物理内存中,线程地址当前也没有被程序使用。

● 保留状态------虽然线程地址没有映射到物理内存中,但它不会被使用,直到程序希望使用它为止。

进程开始的时候,所有地址都是处于自由状态的,这意味着它们都是自由空间并且可以被提交到物理内存,或者为将来使用而保留起来。任何自由状态地址在能够被使用前,必须首先被分配为保留状态或已提交状态。

当使用标准内存管理函数分配内存的时候,用户无法指定内存块位于哪个线程地址,或者不要位于哪个线程地址,而使用虚拟内存管理函数可以做到这一点。但这样做的理由是什么呢?考虑这样一种情况:程序需要一个内存块用做缓冲区,随着程序的运行,这个内存块可能随时需要扩展,最大可能扩展为100 MB大小,所以希望系统在分配其他内存块的时候不要使用这个内存块后面100 MB大小范围内的地址空间,这样,就可以随时将内存块扩大而不必移------动它的位置。

除了这样一个主要的用途外,虚拟内存管理函数还提供转换虚拟地址空间页状态的能力,一个应用程序可以把内存的状态从已提交改变为保留,或把保护的模式从PAGE_READWRITE(可读写)改变为PAGE_READONLY(只读),从而防止对某段地址空间的写访问;应用程序也可以锁定一页内存,不让它被交换到磁盘中。

虚拟内存管理函数是一组名字以Virtual开头的函数,主要包括下面几种:

● VirtualAlloc和VirtualFree------进行地址空间的分配和释放工作。

● VirtualLock和VirtualUnlock------对内存页进行锁定和解锁。

● VirtualQuery或VirtualQueryEx------查询内存页的状态。

● VirtualProtect或VirtualProtectEx------改变内存页的保护属性。

1.保留和释放地址空间

保留或提交一段地址空间,使用VirtualAlloc函数;释放或解除提交地址空间,则使用VirtualFree函数。先来看VirtualAlloc函数的使用方法:

复制代码
invoke   VirtualAlloc,lpAddress,dwSize,flAllocationType,flProtect

lpAddress参数指定需要保留或提交的地址空间的位置,参数可以使用NULL值也可以指定一个具体的地址。NULL值表示由函数自行在某个最方便的位置保留地址范围,非NULL值指定了一个准确的初始地址。如果函数返回NULL,表示执行失败,否则返回一个指针,指向被保留地址范围的开始位置。

dwSize参数表示函数应该分配的地址范围大小,它可以是0 B~2 GB的任意值,但系统会自动把它进位到一个页面的整数倍大小。另外,虽然参数的最大值可以指定为2GB,但实际上能够被保留的最大值是该进程中最大的连续自由地址空间。

flAllocationType参数用来决定如何分配地址,它可以是以下取值的组合:

● MEM_COMMIT------为指定地址空间提交物理内存。

● MEM_RESERVE------保留指定地址空间,不分配物理内存。

● MEM_TOP_DOWN------尽可能使用高端的地址空间。

flProtect参数用来指定保护的类型,它可以是以下取值之一:

● PAGE_READONLY------为已提交物理内存的地址空间设定只读属性。

● PAGE_READWRITE------为已提交物理内存的地址空间设定可读写属性。

● PAGE_EXECUTE------为已提交物理内存的地址空间设定可执行属性。

● PAGE_EXECUTE_READ------为已提交物理内存的地址空间设定可读和可执行属性。

● PAGE_EXECUTE_READWRITE------为已提交物理内存的地址空间设定可读、可写和可执行属性。

● PAGE_NOACCESS------将保留的地址空间设定为不可存取模式。

VirtualFree函数的使用语法是:

复制代码
invoke   VirtualFree,lpAddress,dwSize,dwFreeType

lpAddress和dwSize参数指定地址和地址空间的大小,dwFreeType指定释放地址空间的方式,它可以是以下的数值:

● MEM_DECOMMIT------为一个已经提交物理内存的地址空间解除提交。

● MEM_RELEASE------释放保留的地址空间。

现在来看如何使用它们来保留地址空间和释放保留的地址空间。使用VirtualAlloc函数保留一个地址空间的分配方式使用MEM_RESERVE,由于被保留的地址空间还没有提交给物理内存,是无法访问的,所以保护属性必须使用PAGE_NOACCESS标志,具体的语句是:

复制代码
invoke   VirtualAlloc,NULL,10485760,MEM_RESERVE,PAGE_NOACCESS
.if eax
    mov      lpAddress,eax
.endif

这一段代码导致系统保留一个10 MB大小的地址空间。当在一个进程中保留地址时,没有物理内存页被提交,也没有在页文件中为它保留空间,而只是阻止了其他内存分配函数对该段地址的请求而已,保留一个地址范围并不保证将来会有可用的物理内存来提交给这些地址。

保留地址的操作是很快的,保留一个小的地址范围和保留一个大范围的地址空间的速度差不多,因为在操作期间,并没有资源分配。

如果要释放保留的地址空间,可以使用MEM_RELEASE方式调用VirtualFree函数:

复制代码
invoke   VirtualFree,lpAddress,0,MEM_RELEASE

lpAddress就是上面调用VirtualAlloc返回的指针,dwSize参数在这里必须为0。当使用上面的VirtualAlloc函数保留了一段地址空间以后,接下来还可以继续多次调用同样的函数提交这段地址空间中的不同页面,所以到最后不同的页面可能处在不同的状态中(提交的和没有提交的)。如果用VirtualFree函数释放这个地址空间,所有的页面必须处在相同的状态下(可以是全部提交的或全部没有提交的),否则释放操作会失败。当不同页面的状态不同的时候,最好首先将所有的已提交页面逐一解除提交,最后再使用上面举例的方法释放整个地址空间。

有时候,两次调用VirtualAlloc函数保留了两段连在一起的地址空间,对于这种情况,虽然两段地址空间实际上是连在一起的,但也无法调用VirtualFree函数将它们一次释放,必须调用两次VirtualFree函数将它们分别释放。

2.使用保留的地址空间

要使用保留的地址,首先必须提交物理内存给该地址。提交内存到地址与保留内存同样使用VirtualAlloc函数,只是调用的方式使用MEM_COMMIT标志。在已经保留的地址段中,内存可以按一页的大小被分次提交,也可以一次提交所有的保留地址。

当内存被提交时,可能全部被分配为物理内存页,也可能一部分或全部被分配在页文件中,直到它被访问。一旦内存页已提交,系统就会像对待用其他函数分配的内存块一样来对待它们。

使用VirtualAlloc函数提交地址空间的方法是:

复制代码
invoke   VirtualAlloc,lpAddress,4096,MEM_COMMIT,PAGE_READWRITE
.if eax
    mov lpMemory,eax
.endif

这个语句将一个页面(4096字节)的保留地址提交到物理内存。在提交的时候,lpAddress参数不能指定为NULL,而是要指定一个特定的地址来准确地指示被保留地址的哪一页会被提交。而且,页的属性现在要指定是可以访问的,不能再使用PAGE_NOACCESS,可以使用PAGE_READWRITE和PAGE_READONLY等属性。如果函数执行成功,返回的是被提交地址中第一页的起始线程地址,执行失败将返回NULL。

提交内存的时候,系统只能按页面的整数倍大小提交,函数会自动按照lpAddress和dwSize指定的范围把与这个范围同属一个页面的地址全部提交,所以当lpAddress指定的数值不是一个页的整数倍的时候,返回的lpMemory就不会和指定的lpAddress相同,而是被修改为页的边界地址。

如果要一次提交全部保留的地址空间,那么可以把保留和提交的操作合并到同一次对VirtualAlloc函数的调用中:

复制代码
invoke   VirtualAlloc,NULL,dwSize,MEM_RESERVE or MEM_COMMIT,PAGE_READWRITE
.if eax
    mov lpMemory,eax
.endif

这种方法与用GlobalAlloc函数直接分配一块内存没有多大的差别,唯一的好处就是可以自己指定分配的内存块地址。

如果想对已经提交的页面解除提交,让它们从提交状态返回到保留状态,可以使用VirtualFree函数,这时需要使用MEM_DECOMMIT参数:

复制代码
invoke   VirtualFree,lpMemory,dwSize,MEM_DECOMMIT

同样,函数操作的对象是整个页面,如果指定的内存范围不是整个页面,函数会自动将整个范围同属一个页面的地址全部解除提交。

3.内存页的保护和锁定

除了用VirtualAlloc函数在提交内存的时候指定不同的保护方式外,也可以在以后用VirtualProtect函数来改变虚拟内存页的保护方式。比如,应用程序可以按PAGE_READWRITE来提交一个页并立即将数据写到该页中,然后马上使用VirtualProtect函数将该页的保护方式改为PAGE_READONLY,这样可以有效地保护数据不被该进程中的任何线程重写。VirtualProtect函数的用法是这样的:

复制代码
invoke   VirtualProtect,lpAddress,dwSize,flNewProtect,lpflOldProtect

flNewProtect是新的保护方式,取值可以参考VirtualAlloc函数中的flProtect参数,lpflOldProtect是指向一个双字的指针,函数会在这里返回原来的保护方式,如果不需要知道原来的方式,可以把这个参数设置为NULL。

VirtualProtect函数还可以用在什么地方呢?MSDN中由Randy Kath书写的一篇文章"Managing Virtual Memory in Win32"中的例子很有代表性:

"一个用于缓冲数据的应用程序接收到一组大小变化的数据流,由于其他应用程序对CPU时间的竞争,数据流可能在某些时候超出进程的能力。为了防止这种现象发生,应用程序可以在开始时为一个缓冲区提交一些内存页,然后使用PAGE_NOACCESS保护来保护内存的顶端页,使得任何想要访问该内存的请求都会产生一个异常。应用程序也在该代码的外层代码中使用一个异常处理程序来处理访问冲突。"

"当处理能力不够的时候,缓冲区会满到这个受保护的顶端页,于是会产生一个访问冲突,这时应用程序就知道缓冲区已经到了其极限,该应用程序可以通过将页保护改变为PAGE_READWRITE来响应,允许该缓冲区接收任何附加的数据,并且继续不间断地执行。同时,应用程序加载另一个线程来减缓数据流,直到该缓冲区恢复到一个理想的操作范围。当情况恢复到正常,顶端的页又返回为PAGE_NOACCESS页,附加的线程也结束了。这样可以将页保护和异常处理程序结合使用来提供独一无二的内存管理机会。"

另外,应用程序还可以使用VirtualLock和VirtualUnlock函数,它们的功能分别是将内存页锁定在物理内存中以及解除锁定。这两个函数的语法很简单:

复制代码
invoke   VirtualLock,lpAddress,dwSize
invoke   VirtualUnlock,lpAddress,dwSize

"锁定"的意思是要求系统总是将指定的内存页保留在物理内存中,不许将它交换到磁盘页文件中。如果程序中有些内存被频繁使用,将它们保留在物理内存可以提高访问的速度。由于锁定太多的页面会导致其他页面被频繁交换到页文件中,所以Windows限制每个进程能同时锁定的页数不能超过30个。只有已经被提交的内存页才能被锁定,对一个保留的地址进行锁定操作是不能成功的。

10.1.6 其他内存管理函数

Win32中还有其他的一些内存管理函数,可以用来完成一些辅助的功能,如内存填充、移动以及测试函数等。

1.填充和移动内存

填充和移动内存本来就可以用几句简单的代码实现,如下面的代码可以将从szSource开始的dwSize大小的内存块移动到szDest处:

复制代码
mov      esi,offset szSource
mov      edi,offset szDest
mov      ecx,dwSize
cld
rep      movsb

而下面的代码可以将szDest处的dwSize字节填充为0:

复制代码
xor      eax,eax
mov      edi,offset szDest
mov      ecx,dwSize
cld
rep      stosb

如果把xor eax,eax换成mov al,xx,那么上面代码的功能就是将这块内存填充为xx。

虽然填充和移动的功能这么简单,但Win32中还是有对应的API函数:

复制代码
invoke  RtlMoveMemory,offset szDest,offset szSource,dwSize ;移动内存
invoke  RtlFillMemory,offset szDest,dwSize,dbFill   ;以dbFill填充内存块
invoke  RtlZeroMemory,offset szDest,dwSize           ;以0填充内存块

可以看到,使用这些函数时,仅传递参数和调用的开销就远远超过了前面举例的两段代码,但是由于使用它们的可读性比较好,所以在具体的使用中要有所取舍。如果执行速度比较重要,比如,在一个循环中使用,同样的代码要被使用很多遍,还是应该使用嵌入的几句汇编代码;如果为了让程序看上去简洁一些,那就不妨使用这几个API函数。

2.内存状态测试

有时候在访问一块内存之前,可能想知道这块内存的属性究竟是什么,是可写的?可读的?还是可执行的?这些功能可以用测试函数来完成:

复制代码
invoke   IsBadCodePtr,lpMemory
invoke   IsBadReadPtr,lpMemory,dwSize
invoke   IsBadWritePtr,lpMemory,dwSize
invoke   IsBadStringPtr,lpMemory,dwSize

这些函数的功能如下:

● IsBadCodePtr函数测试某个指针指向的单个字节是否可读,如果可读则返回0,否则返回非0值。

● IsBadReadPtr函数测试某段内存是否可读,如果这段内存的所有字节都是可读的,则返回0,如果中间包含有不可读的字节则返回非0值。

● IsBadWritePtr函数测试某段内存是否可写,如果这段内存的所有字节都是可写的,则返回0,如果中间包含有不可写的字节则返回非0值。

● IsBadStringPtr函数测试的同样是可读性,lpMemory参数指向一个以0结尾的字符串,字符串的最大长度为dwSize,如果整个字符串包含结尾的一个0都是可读的,则函数返回0,否则返回非0值。缓冲区中剩余的字节则不予测试。