Chinaunix首页 | 论坛 | 博客
  • 博客访问: 129618
  • 博文数量: 37
  • 博客积分: 1490
  • 博客等级: 上尉
  • 技术积分: 326
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-01 16:38
文章分类

全部博文(37)

文章存档

2011年(1)

2010年(23)

2009年(13)

我的朋友

分类: WINDOWS

2010-01-04 17:28:05

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

图1  Windows的内存分层管理
Windows使用一个以页为基础的虚拟内存系统,与分页有关的概念已经在第1章的1.3.2小节中有所介绍。Windows充分利用了80x86处理器保护模式下的线性寻址机制和分页机制,这些机制是Win32内存管理的基础,Win32提供了一组虚拟内存管理函数来管理虚拟内存,主要用于保留/提交/释放虚拟内存,在虚拟内存页上改变保护方式,锁定虚拟内存页以及查询一个进程的虚拟内存等操作,这是一组位于底层的函数。
堆管理函数相对比较高级一点,堆的主要功能就是有效地管理内存和进程的地址空间。DOS操作系统下的C语言中就已经有了“堆”的概念,这时的“堆”是程序初始化时向操作系统申请并预留的大块内存,程序通过C函数在这块空间中申请和释放内存。
在Win32中,进程可以使用的整个地址空间就是一个堆。并且“堆”的概念又被引伸了一步:Win32中分两种堆,一种是进程的“默认堆”,默认堆只有一个,指的就是可以使用的整个地址空间;另一种是“动态堆”,也称为“私有堆”,私有堆类似于DOS下C语言中使用的那种堆,一个进程可以随意建立多个私有堆,也可以随意将它们释放,私有堆全部位于默认堆中,从概念上看,它和默认堆并没有什么不同,就像一个跨国公司和属下的子公司同样都是按照公司的规程操作一样。使用堆管理函数可以对所有的私有堆和默认堆进行操作。
标准内存管理函数总是在默认堆中分配和释放内存,这组函数就是常规意义上的内存管理函数。
内存映射文件函数相对比较独立,它是为了文件操作的方便性而设立的,当对文件进行操作的时候,一般总是先打开文件,然后申请一块内存用做缓冲区,再将文件数据循环读入并处理,当文件长度大于缓冲区长度的时候需要多次读入,每次读入后处理缓冲区边界位置的数据往往是个麻烦的问题。曾经介绍过Windows可以使用磁盘文件当做虚拟内存(参考图1.5,虚拟内存的实现),内存映射文件函数使用同样的办法将一个文件直接映射到进程的地址空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容。
对比这些函数,可以发现它们涉及的系统资源是各不相同的,如表10.1所示。
表1  不同内存管理函数的操作对象
内存管理函数                         涉 及 方 面
 
标准内存管理函数                  一个进程的默认堆
 
堆管理函数                        一个进程的虚拟地址空间、系统内存、进程堆资源结构
 
虚拟内存管理函数                  一个进程的虚拟地址空间、系统页文件、系统内存、硬盘空间
 
内存映射文件函数                  一个进程的虚拟地址空间、系统页文件、系统内存、硬盘空间、标准文件I/O
 
1.2  内存的当前状态
在第1章中已经介绍过,一个进程可以寻址的地址空间是4 GB,但用户可以直接管理的地址空间是多大呢?实际上,高端的2 GB是供操作系统内核使用的,其中安排了操作系统的代码和数据(Windows 9x中还包括共享内存映射的地址空间),可供应用程序使用的地址空间是低端的2 GB,这2 GB除去应用程序与用户DLL等的代码和静态数据段以后,余下来的才是内存管理函数可以使用的地址空间,应用程序和用户DLL的大小一般只有几兆字节到上百兆字节,所以可以认为能自由使用的地址空间基本上是2 GB。
既然用户可以使用的地址空间大概为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字段返回的是交换文件的最大值,并不是当前实际建立的交换文件的大小,一般当前的交换文件大小会小于这个数值,但这个数值的大小也不是确定的,如果需要的话,系统会增加它的大小直到不再有空余的磁盘空间放置交换文件为止。

.386
.model flat, stdcall
option casemap :none
include         windows.inc
include         user32.inc
includelib      user32.lib
include         kernel32.inc
includelib      kernel32.lib
ICO_MAIN            equ     1000
DLG_MAIN            equ     100
IDC_INFO            equ         101
.data?
hInstance       dd      ?
hWinMain         dd     ?
.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

_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

start:
                invoke  GetModuleHandle,NULL
                mov     hInstance,eax
                invoke  DialogBoxParam,hInstance,DLG_MAIN,NULL,\
                        offset _ProcDlgMain,NULL
                invoke  ExitProcess,NULL
end     start

MemInfo.rc文件如下:
#include           
#define ICO_MAIN                1000
#define DLG_MAIN                100
#define IDC_INFO                101
ICO_MAIN        ICON                "Main.ico"
DLG_MAIN DIALOG 188, 193, 140, 75
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "内存状态"
FONT 9, "宋体"
{
 LTEXT "", IDC_INFO, 6, 6, 135, 65
}
程序设置了一个定时器,以周期为1秒来刷新当前内存的使用情况,在WM_TIMER消息中用GlobalMemoryStatus获取内存状态并用wsprintf将数据转换成字符串,然后显示在对话框的IDC_INFO文本框中。
在笔者的计算机中,程序运行的结果如图10.2所示,计算机的物理内存配置为128 MB,这个数值和物理内存总数(dwTotalPhys字段)符合,dwMemoryLoad字段显示的72%等于空闲物理内存(dwAvailPhys字段)除以物理内存总数,计算机上当前虚拟内存交换文件大小为192 MB,小于最大限制dwTotalPageFile字段。
在与地址空间相关的数值上,dwTotalVirtual字段的显示结果是2 147 352 576,等于2 GB减去128 KB,这是因为这2 GB的最低端的和最高端的两个64 KB是系统保留的(00000000h~0000ffffh,7fff0000h~7fffffffh)。

图2  Meminfo程序的运行结果
Windows可以根据内存使用的需求自动调整交换文件的大小,比如Meminfo程序运行显示的当前磁盘交换文件可用空间(dwAvailPageFile字段)为168 MB,但是如果尝试申请一个高达300 MB的内存块,会发现仍然可以申请成功,这时dwTotalPageFile字段的大小会自动增长到500多MB,把内存块释放掉的话,dwTotalPageFile字段会恢复到原来的数值。虚拟内存的使用给我们带来了很多的方便,我们可以使用超过物理内存好几倍的内存空间,但是如果所需的内存大大高于物理内存的大小,那么申请内存还是会失败,因为这会引起物理内存和交换文件之间的数据频繁交换,大量的磁盘请求将使系统性能降低到没有实际使用的意义,读者可以尝试在128 MB物理内存的计算机上申请一个1 GB的内存块,即使拥有远远大于1 GB的磁盘剩余空间供交换文件使用,也是不会成功的!如果读者需要使用大大高于物理内存的内存空间,可以尝试自己进行磁盘交换工作。
1.3  标准内存管理函数
标准内存管理函数的功能是在进程的默认堆中申请和释放内存块,它由下面一些函数组成:GlobalAlloc,GlobalFree和GlobalReAlloc分别用来申请、释放和修改内存大小;GlobalLock和GlobalUnlock用来进行锁定操作;而GlobalDiscard,GlobalFlags,GlobalHandle和GlobalSize等用来丢弃内存或获取已分配内存的一些信息。
在Win16中,内存管理函数有“全局”或“本地”之分,它们的区别在于返回的指针是远指针还是近指针,全局内存管理函数名是以“Global”开头的,而“本地”内存管理函数名是以“Local”开头的。在Win32中,指针并没有远近之分,只有一种32位的指针,但为了保持向下兼容,这些函数名仍然沿用了下来,上面列出的这些函数名都是以“Global”开头的,同样,Win32中也存在以“Local”开头的内存管理函数名,只要这些函数名中的“Global”全部换成“Local”就可以了。这两组函数在Win32中是完全相同的,读者可以自由使用名字以Global或Local为前缀的函数。
用标准内存管理函数可以分配的内存有两种:固定地址的内存块和可移动的内存块,而可移动的内存块又可以进一步定义为可丢弃的,让我们逐步来讨论它们的不同。
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    e   ax
       mov lpNewMemory,eax
.endif
lpMemory是先前申请的内存块指针,dwBytes是新的大小,如果这个数值比原来申请的时候要小,也就是需要缩小内存块,那么uFlags标志参数可以是NULL,如果缩小内存块的操作不成功,那么函数的返回值为0,否则是新的缩小了的内存块指针,当然,这个指针和原来的指针肯定是一样的。
但是需要扩大一个内存块的时候,情况就稍微有些复杂了。让我们做一个实验来模拟这样一种情况:首先申请两个1000h大小的固定内存块,得到两个指针,读者可以发现第二块几乎紧接第一块内存,一般情况下如果第一块内存的地址是X,那么第二块内存的地址几乎就是X+1000h,如果需要将第一个内存块扩大到2000h字节,那么只能在别的地方开辟一个2000h大小的内存块,因为原来位置后面的1000h已经被第二块内存占用了,这就意味着新的指针可能和原来的不一样。
 
Win32汇编(内存管理2)
  核心提示:可以在GlobalReAlloc函数中通过指定不同的uFlags来规定是否允许Windows在必要的时候移动内存块。当uFlags中有GMEM_MOVEABLE选项的时候,如果需要移动内存块,Windows会在别的地方开辟一块新的内存,并把原来内存块中的内容自动复制到新的内存块中,这时函数的返回值是...
可以在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改变它的线性地址,为什么要使用可移动的内存块呢?惟一的理由是防止内存的碎片化,这里的碎片化指的是用户程序自己地址空间的碎片化,而不是指整个操作系统。读者可能有个疑问:与DOS操作系统相比,Win32用户程序可用的地址空间要大得多,整整2 GB的地址空间难道还怕用完吗?让我们先用一个例子来演示一下,并由此引伸出可移动内存块的使用方法。
在这个例子中,让我们来设计一个“阴谋”,用一个极端的方法“谋杀”掉所有的地址空间:程序首先申请一个1 MB大小的固定内存块,然后继续申请内存并把前面申请的内存块大小改为100 B,由此循环,因为缩小内存块释放出来的空间大小为999 900 B,新申请的内存块无法使用这些地址空间,只能继续使用后面大块的地址空间,如果没有算错的话,经过2 000次左右的循环就会把全部的地址空间分割成2 000个999 900 B大小的空间(2GB等于2 000个1 MB),到时候虽然只保留了近200 KB大小的内存(2 000个100 B),但是这2 000个100 B均匀分布在2 GB的地址空间内,以至于接下来任何大于999 900 B的内存申请操作都无法成功。

.386
.model flat, stdcall
option casemap :none

include         windows.inc
include         user32.inc
includelib      user32.lib
include         kernel32.inc
includelib      kernel32.lib

ICO_MAIN            equ     1000
DLG_MAIN            equ     100
IDC_MEMORY      equ         101
IDC_COUNT       equ         102
IDC_INFO            equ     103

.data?
hInstance       dd      ?
hWinMain            dd      ?
dwTotalMemory   dd      ?
dwCount         dd      ?
ifCanQuit       dd      ?
.const
szInfo          db      '无法继续申请 1MB 大小的内存!',0
.code
_ProcThread     proc        uses ebx ecx edx esi edi,lParam
                    local   @lpLastMem
 
                    invoke  GlobalAlloc,GPTR,1000000
                    mov     @lpLastMem,eax
                    inc     dwCount
                    add     dwTotalMemory,1000000
                    .repeat
                            push        @lpLastMem
                            invoke  GlobalAlloc,GPTR,1000000
                            mov     @lpLastMem,eax
                            .if     eax
                                    add     dwTotalMemory,1000000
                                    inc     dwCount
                            .endif
                            pop     eax
                            invoke  GlobalReAlloc,eax,100,GMEM_ZEROINIT
                            sub     dwTotalMemory,1000000 - 100
                            invoke  SetDlgItemInt,hWinMain,IDC_MEMORY,\
                                    dwTotalMemory,FALSE
                            invoke  SetDlgItemInt,hWinMain,IDC_COUNT,\
                                    dwCount,FALSE
                    .until  ! @lpLastMem
                    invoke  SetDlgItemText,hWinMain,IDC_INFO,addr szInfo
                    mov     ifCanQuit,1
                    ret
 
_ProcThread     endp

_ProcDlgMain        proc        uses ebx edi esi hWnd,wMsg,wParam,lParam
                    local   @dwTemp
 
                    mov     eax,wMsg
                    .if     eax ==  WM_CLOSE
                            .if     ifCanQuit
                                    invoke  EndDialog,hWnd,NULL
                            .endif
;********************************************************************
                    .elseif eax ==  WM_INITDIALOG
                            push        hWnd
                            pop     hWinMain
                            invoke  LoadIcon,hInstance,ICO_MAIN
                            invoke  SendMessage,hWnd,WM_SETICON,ICON_BIG,eax
                            invoke   CreateThread,NULL,0,offset _ProcThread,NULL,\
                                    NULL,addr @dwTemp
                            invoke  CloseHandle,eax
;********************************************************************
                    .else
                            mov eax,FALSE
                            ret
                    .endif
                    mov     eax,TRUE
                    ret
 
_ProcDlgMain        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
                    invoke  GetModuleHandle,NULL
                    mov     hInstance,eax
                invoke  DialogBoxParam,hInstance,DLG_MAIN,\
                        NULL,offset _ProcDlgMain,NULL
                invoke  ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                end     start
对应的资源文件Fragment.rc如下:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include               
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN                1000
#define DLG_MAIN                100
#define IDC_MEMORY          101
#define IDC_COUNT           102
#define IDC_INFO                103
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN        ICON                "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 308, 207, 130, 50
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "碎片内存演示"
FONT 9, "宋体"
{
 RTEXT "申请内存总数:", -1, 7, 8, 60, 8
 EDITTEXT IDC_MEMORY, 69, 5, 55, 12,
ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_TABSTOP
 RTEXT "申请次数:", -1, 7, 21, 60, 8
 EDITTEXT IDC_COUNT, 69, 19, 55, 12,
      ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_TABSTOP
 LTEXT "", IDC_INFO, 7, 37, 120, 8
}

程序在WM_INITDIALOG消息中建立了一个线程来循环申请内存(相当于在后台执行_ProcThread子程序,与多线程相关的内容请参见第12章)。全局变量dwCount记录了申请的次数,每次申请内存就将它的值加1。dwTotalMemory记录了程序申请到的内存总数,每申请一个1 MB的内存,程序将它的值加上1 000 000,每次用GlobalReAlloc缩小内存块,则将它的值减去999 900。当最后申请内存失败的时候,repeat循环结束。
在Windows 2000下运行一下程序以验证结果,几秒的运行中,显示的计数不断增加,最后的结果如图3所示。

图3  内存碎片化的演示结果
结果和预想的一样,经过2 027次的操作,只保留了近202 700 B的内存,程序就成功地“谋杀”了所有的地址空间,让整个2 GB中间充满了碎片,以至于连1 MB大小的内存也无法申请了!当程序在Windows 9x中运行时,由于9x系统在高端和低端轮换分配内存块,所以同样的办法就不会产生内存碎片,但是如果在循环中先Alloc两次、然后Realloc两次的话仍然可以造成内存碎片化。
虽然这是一个极端的情况,但在现实中会发生吗?会的!例如编写一个遍历二叉树的程序,每增加一个结点的时候申请一块内存,用来存放指向其他结点的指针以及附加在结点上的数据,当结点处理完毕后缩小内存块,只留下指针数据,那么情况就和演示程序类似,当树的结点足够多的时候,经过一段时间的操作,内存中就会充满碎片。
解决内存碎片化的办法很简单,因为碎片之间有大量的内存是空闲的,只要允许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,成功的话返回值是一个句柄而不是内存指针,用户需要保存这个句柄,在锁定或释放内存的时候还要用到它。一个进程可以申请的可移动内存的块数最大不能超过65 536个,申请固定内存块时则没有数量限制。
要使用可移动内存之前,需要把它锁定,这相当于告诉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函数获取的指针可能已经失效了,所以调整可移动内存块的大小最好还是先将内存解锁,等调整完毕以后再锁定使用。
由于使用可移动的内存块多了一个锁定的动作,速度自然要比使用固定的内存块要慢一点,但固定内存块又存在碎片问题,程序中使用哪种方法有个取舍的问题。如果程序要频繁地分配和释放不定长的内存块,内存的碎片化现象就比较严重,特别是当程序长时间运行时,这种情况下使用可移动内存块比较好;如果程序只进行少量的内存操作,或者虽然频繁分配和释放内存,但使用的内存块长度都是一样的,则使用固定内存块可以节省时间。
3. 可丢弃的内存块
分配可移动内存块的时候还可以配合GMEM_MOVEABLE标志使用GMEM_DI SCARDABLE标志,这样生成的内存块是可丢弃的内存块,表示当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指针的检测——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函数可以获知一个内存块的尺寸。

Win32汇编(内存管理3)
  核心提示:10.1.4 堆管理函数 Windows的“堆”分为默认堆和私有堆两种。默认堆是在程序初始化时由操作系统自动创建的,所有的标准内存管理函数都是在默认堆中申请内存的;而私有堆相当于在默认堆中保留了一大块内存,用堆管理函数可以在这个保留的内存块中分配内存。一个进程的默认堆只有一个,而私有堆可以被创建多个...
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。
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的任意值,但系统会自动把它进位到一个页面的整数倍大小。另外,虽然参数的最大值可以指定为2 GB,但实际上能够被保留的最大值是该进程中最大的连续自由地址空间。
flAllocationType参数用来决定如何分配地址,它可以是以下取值的组合:
●   MEM_COMMIT——为指定地址空间提交物理内存。
●   MEM_RESERVE——保留指定地址空间,不分配物理内存。
●   MEM_TOP_DOWN——尽可能使用高端的地址空间。
flProtect参数用来指定保护的类型,它可以是以下取值之一:
●   PAGE_READONLY——为已提交物理内存的地址空间设定只读属性。
●   PAGE_READWRITE——为已提交物理内存的地址空间设定可读写属性。
●   PAGE_EXECUTE——为已提交物理内存的地址空间设定可执行属性。
●   PAGE_EXECUTE_READ——为已提交物理内存的地址空间设定可读和可执行属性。
●   PAGE_EXECUTE_READWRITE——为已提交物理内存的地址空间设定可读、可写和可执行属性。
●   PAGE_NOACCESS——将保留的地址空间设定为不可存取模式。
VirtualFree函数的使用语法是:
 Win32汇编(内存管理4)
  核心提示:invoke VirtualFree,lpAddress,dwSize,dwFreeTypelpAddress和dwSize参数指定地址和地址空间的大小,dwFreeType指定释放地址空间的方式,它可以是以下的数值:● MEM_DECOMMIT——为一个已经提交物理内存的地址空间解除提交。● ME...
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
这个语句将一个页面4 096 B的保留地址提交到物理内存。在提交的时候,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,dwFill   ;以dwFill填充内存块
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值。缓冲区中剩余的字节则不予测试。
 
 
阅读(2093) | 评论(0) | 转发(0) |
0

上一篇:多线程

下一篇:API

给主人留下些什么吧!~~