📄 010.txt
字号:
当可丢弃内存块的锁定计数为0时,程序也可以使用GlobalDiscard函数主动将它丢弃,这和Windows将它丢弃的效果是一样的:
invoke GlobalDiscard,hMemory
使用内存函数时有两个地方需要特别注意:
(1)NULL指针的检测——GlobalAlloc函数和GlobalLock函数都可以返回内存指针,在使用指针前一定要检测它的有效性,如果使用了函数执行失败而返回的NULL指针来访问数据,会导致程序越权访问不该访问的地方,从而被Windows毫不留情地终止掉,这就是例子代码中总是有个if语句来判断eax是否为NULL的原因。
(2)注意访问越界问题——越界操作也会引起越权访问,千万不要到超出内存块长度的地方去访问,例如,使用lstrcpy之类的函数处理字符串之前,先用lstrlen检测字符串长度是一个好习惯。
4. 获取内存块的信息
标准内存管理函数中的其他函数GlobalFlags,GlobalHandle和GlobalSize用来获取已分配内存块的一些信息。
GlobalFlags函数主要用来获取可移动内存块当前的锁定计数,也可以用来检测可丢弃内存块是否已经被丢弃。对一个hMemory调用GlobalFlags函数如下所示:
invoke GlobalFlags,hMemory
如果不是返回GMEM_INVALID_HANDLE,则表示调用成功,这时返回值的低8位是内存块的锁定计数,程序可以用GMEM_LOCKCOUNT对获取计数值进行and操作(在Windows.inc头文件中,GMEM_LOCKCOUNT定义为0ffh):
invoke GlobalFlags,hMemory
and eax,GMEM_LOCKCOUNT
mov dwLockCount,eax
返回值的其他数据位可能包含下列标志:
● GMEM_DISCARDABLE 表示内存块是可丢弃内存块。
● GMEM_DISCARDED 表示内存块已经被丢弃。
GlobalHandle可以从GlobalLock函数得到的lpMemory值获取其对应的hMemory,而GlobalSize函数可以获知一个内存块的尺寸。
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——查询内存页的状态。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -