汇编语言动态内存分配(堆分配)

动态内存分配 (dynamic memory allocation),又被称为堆分配 (heap allocation),是编程语言使用的一种技术,用于在创建对象、数组和其他结构时预留内存。比如在 Java 语言中,下面的语句就会为 String 对象保留内存:

String str = new String("abcde");

同样的,在 C++ 中,对变量使用大小属性就可以为一个整数数组分配空间:

int size;
cin >> size;     //用户输入大小
int array[] = new int[size];

C、C++ 和 Java 都有内置运行时堆管理器来处理程序请求的存储分配和释放。程序启动时,堆管理器常常从操作系统中分配一大块内存,并为存储块指针创建空闲列表 (free list)。

当接收到一个分配请求时,堆管理器就把适当大小的内存块标识为已预留,并返回指向该块的指针。之后,当接收到对同一个块的删除请求时,堆就释放该内存块,并将其返回到空闲列表。每次接收到新的分配请求,堆管理器就会扫描空闲列表,寻找第一个可用的、且容量足够大的内存块来响应请求。

汇编语言程序有两种方法进行动态分配:
  • 方法一:通过系统调用从操作系统获得内存块。
  • 方法二:实现自己的堆管理器来服务更小的对象提出的请求。

利用下表中的几个 Win32 API 函数就可以从 Windows 中请求多个不同大小的内存块。表中所有的函数都会覆盖通用寄存器,因此程序可能想要创建封装过程来实现重要寄存器的入栈和出栈操作。

函数 描述
GetProcessHeap 用 EAX 返回程序现存堆区域的 32 位整数句柄。如果函数成功,则 EAX 中的返回值为堆句柄。 如果函数失败,则 EAX 中的返回值为 NULL
HeapAlloc 从堆中分配内存块。如果成功,EAX 中的返回值就为内存块的地址。如果失败,则 EAX 中的返 回值为 NULL
HeapCreate 创建新堆,并使其对调用程序可用。如果函数成功,则 EAX 中的返回值为新创建堆的句柄。如果失败,则 EAX 的返回值为 NULL
HeapDestroy 销毁指定堆对象,并使其句柄无效。如果函数成功,则 EAX 中的返回值为非零
HeapFree 释放之前从堆中分配的内存块,该堆由其地址和堆句柄进行标识。如果内存块释放成功,则返回值为非零
HeapReAlloc 对堆中内存块进行再分配和调整大小。如果函数成功,则返回值为指向再分配内存块的指针。如果函数失败,且没有指定 HEAP GENERATE EXCEPTIONS,则返回值为 NULL
HeapSize 返回之前通过调用 HeapAlloc 或 HeapReAlloc 分配的内存块的大小。如果函数成功,则 EAX 包含被分配内存块的字节数。如果函数失败,则返回值为 SIZE_T-1 ( SIZE_T 等于指针能指向的最大字节数 )

GetProcessHeap

如果使用的是当前程序的默认堆,那么 GetProcessHeap 就足够了。这个函数没有参数,EAX 中的返回值就是堆句柄:

GetProcessHeap PROTO

示例调用:
.data
hHeap HANDLE ?
.code
INVOKE GetProcessHeap
.IF eax == NULL           ;不能获取句柄
    jmp quit
.ELSE
    mov hHeap,eax         ;句柄 ok
.ENDIF

HeapCreate

HeapCreate 能为当前程序创建一个新的私有堆:

HeapCreate PROTO,
    flOptions:DWORD,                    ;堆分配选项
    dwInitialSize:DWORD,               ;按字节初始化堆大小
    dwMaximumSize:DWORD        ;最大堆字节数

flOptions 设置为 NULL。dwInitialSize 设置为初始堆字节数,其值的上限为下一页的边界。如果 HeapAlloc 的调用超过了初始堆大小,那么堆最大可以扩展到 dwMaximumSize 参数中指定的大小(上限为下一页的边界)。调用后,EAX 中的返回值为空就表示堆未创建成 功。HeapCreate 的调用示例如下:
HEAP_START = 2000000 ; 2 MB
HEAP_MAX = 400000000 ; 400 MB
.data
hHeap HANDLE ?       ; 堆句柄
.code
INVOKE HeapCreate, 0, HEAP_START, HEAP_MAX
.IF eax == NULL      ; 堆未创建
    call WriteWindowsMsg ; 显示错误消息
    jmp quit
.ELSE
    mov hHeap,eax    ; 句柄 OK
.ENDIF

HeapDestroy

HeapDeatroy 销毁一个已存在的私有堆(由 HeapCreate 创建)。需向其传递堆句柄:

HeapDestroy PROTO,
    hHeap:DWORD          ;堆句柄

如果堆销毁失败,则 EAX 等于 NULL。下面为示例调用,其中使用了 WriteWindowsMsg 过程:
.data
hHeap HANDLE ?                ;堆句柄
.code
INVOKE HeapDestroy, hHeap
.IF eax == NULL
    call WriteWindowsMsg      ;显示错误消息
.ENDIF

HeapAlloc

HeapAlloc 从已存在堆中分配一个内存块:

HeapAlloc PROTO,
    hHeap:HANDLE,       ;现有堆内存块的句柄
    dwFlags :DWORD,    ;堆分配控制标志
    dwBytes:DWORD     ;分配的字节数

需传递下述参数:
  • hHeap:32 位堆句柄,该堆由 GetProcessHeap 或 HeapCreate 初始化。
  • dwFlags:一个双字,包含了一个或多个标志值。可以选择将其设置为 HEAP_ZERO_MEMORY,即设置内存块为全零。
  • dwBytes:一个双字,表示堆分配的字节数。

如果 HeapAlloc 成功,则 EAX 包含指向新存储区的指针;如果失败,则 EAX 中的返回值为 NULL。下面的代码用 hHeap 标识一个堆,从该堆中分配了一个 1000 字节的数组,并将数组初始化为全零:
.data
hHeap HANDLE ?    ;堆句柄
pArray DWORD ?    ;数组指针
.code
INVOKE HeapAlloc, hHeap, HEAP_ZERO_MEMORY, 1000
.IF eax == NULL
    mWrite "HeapAlloc failed"
    jmp quit
.ELSE
    mov pArray,eax
.ENDIF

HeapFree

函数 HeapFree 释放之前从堆中分配的一个内存块,该堆由其地址和堆句柄标识:

HeapFree PROTO,
    hHeap:HANDLE,
    dwFlags:DWORD,
    lpMem:DWORD

第一个参数是包含该内存块的堆的句柄。第二个参数通常为零,第三个参数是指向将被释放内存块的指针。如果内存块释放成功,则返回值非零。如果该块不能被释放,则函数返回零。

示例调用如下:

INVOKE HeapFree, hHeap, 0, pArray

Error Handling

若在调用 HeapCreate、HeapDestroy 或 GetProcessHeap 时遇到错误,可以通过调用 API 函数 GetLastError 来获得详细信息。还可以调用 Irvine32 链接库的函数 WriteWindowsMsg。

HeapCreate 调用示例如下:
INVOKE HeapCreate, 0, HEAP_START, HEAP_MAX
.IF eax == NULL                    ;失败?
    call WriteWindowsMsg           ;显示错误信息
.ELSE
    mov    hHeap,eax               ;成功
.ENDIF
反之,函数 HeapAlloc 在失败时不会设置系统错误码,因此也就无法调用 GetLastError 或 WriteWindowsMsg。