在《Windows核心编程 第五版》第19章 DLL基础(511页)中给出了一个建议:

“当一个 MT 版本的模块如果提供一个内存分配函数的时候,它必须同时提供另一个用来释放内存的函数。”。

说得更加直白一点就是,“对于 MT 的模块,不要跨模块进行内存释放。”。但是核心编程这本书上面没有具体分析原因,本文就来分析具体的原因。

一、不同堆分配的内存块不能相互释放

Windows 的堆管理器对每个进程都维护了多个“堆”,我们从每个“堆”中分配处理的内存块的地址都不一样。所以我们不能将从“堆 A”中分配出来的内存块拿到“堆 B”中,让“堆 B”来释放,这样就会导致程序异常。

如上图,通过malloc函数从“堆 A”中分配 100 字节内存块,内存块地址为0x123456;从“堆 B”中分配 100 字节内存块,内存块地址为0x345678.
如果将0x123456这个地址拿到“堆 B”中去释放,势必会导致异常,因为“堆 B”中没有这地址。

那么我们是不是可以使用HeapFree函数来释放hHeap参数指定的“堆”中的任何内存块了。答案是:不能。
回忆前面介绍的HeapFree函数,

1
2
3
4
5
BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem
);

这个函数只要求传入了内存块的起始地址指针,但没有要求传入需要释放的内存块的大小,那么该函数是如何知道起始地址指针指向的内存块的大小了?

我们可以简单的理解为,HeapAlloc函数每次分配内存块的时候都会额外分配一点空间用于存储一个结构体,该结构体中存储了本次分配的内存块的大小等信息。大致如下图:

所以,HeapFree函数首先会通过lpMem指针计算出“结构体”的地址,然后从结构体中获取到分配的内存块的具体大小,最后执行释放操作。

基于上面的原因,我们不能在HeapFree函数的lpMem参数中传入随意的地址,因为该地址处可能没有存储用于存放内存块信息的结构体,所以释放操作就会失败。free函数也一样,因为free函数内部也是调用的HeapFree函数。

二、MD 模块内存可以相互释放

为什么 MD模块内存可以相互释放,而MT模块的却不可以了?

2.1 MT 模块内存相互释放会崩溃

我们先分析为什么 MT 模块的内存间相互释放会崩溃?

现在有 2 个模块(A.dllB.dll)都是使用MT运行时库,即加载的静态库libcmt.lib(可以参考理解C/C++运行时库),在A.dll中使用malloc分配 100 字节的内存,malloc返回的内存地址为0x123456。然后将该地址传给B.dll,在B.dll中调用free函数来释放这个内存。如图:

Windows内存体系(5)--堆我们知道,DLL 在启动代码_DllMainCRTStartup中会建立一个“堆”(堆句柄存放在_crtheap 变量中),所以 A.dll 和 B.dll 中都会有一个 crt 堆。

为了区分,我们将A.dll中的crt堆称作_crtheap_AB.dll中的crt堆称作_crtheap_B

从上面图可以看到,A.dllmalloc的内存拿到B.dll去中去free,就相当于从堆_crtheap_A中分配的内存拿到另一个堆_crtheap_B中的释放。第一节已经解释了为什么不能这样做了。

2.2 MD 模块内存相互释放不会崩溃

现在我们分析为什么 MD 模块的内存间相互释放不会崩溃。

还是 2 个模块(A.dllB.dll),但是现在他们都是使用MD运行时库,即加载的动态库msvcr100.dll,程序的代码的过程和上面一样,还是在A.dll中使用malloc分配 100 字节的内存,malloc返回的内存地址为0x123456。然后将该地址传给B.dll,在B.dll中调用free函数来释放这个内存。但是这个时候程序却不会崩溃,通过下面的图我们基本可以明白原因了,如图:

因为 A、B 两个 dll 都是链接的·msvcr100.dll·,同一个 dll 在一个进程只会被加载一次,所以进程中只会有一个 crt 堆(_crtheap),mallocfree都是运行时库提供的函数,所以都会调到运行时库里面去,然后从运行时库里面的_crtheap分配和释放内存块。因为分配和释放都是在同一个堆上,所以不会崩溃。

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。