Windows 下主程序与动态库(*.dll)释放对方分配的内存操作要点

发布时间 2023-04-21 14:23:37作者: DoubleLi
同样的代码程序:
主程序中释放了一块在 动态库(*.dll)或共享库(*.so) 中分配的内存,
Windows 将会出现程序崩溃,而 Linux 则正常运行。
 
在 linux 下,每个进程只有一个 heap ,
在任何一个共享库模块 *.so 中通过 new 或者 malloc 来分配内存的时候都是从这个唯一的 heap 中分配的,
那么自然你在其它什么地方都可以释放。
 
但是 windows 下面确不是如此:
1. windows 允许一个进程中有多个 heap ,那么这样就需要指明一块内存要在哪个 heap 上分配,
     win32 的 HeapAlloc 函数就是这样设计的,
     给出一个 heap 的句柄、一个可选的分配操作标志、一个字节块大小,然后返回一个指针。
     每个进程都至少有一个主 heap ,它的句柄可以通过 GetProcessHeap 来获得,
     其它的堆句柄可以通过 GetProcessHeaps 取到。
     同样,内存释放的时候通过 HeapFree 来完成,还是需要指定一个堆句柄。
 
2. 这样的设计显然是比较灵活的,
     但是问题在于这样的话,每次分配内存的时候就必须要显式的指定一个 heap 句柄,
     对于 crt 中的 new/malloc ,显然需要特殊处理。
     那么如何处理就取决于 crt 的实现了。
     VC++ 的 crt 是创建了一个单独的 heap,叫做 __crtheap ,它对于用户是看不见的,
     但是在 new/malloc 的实现中,都是用 HeapAlloc 在这个 __crtheap 堆上分配的,
     也就是说 malloc(size) 基本上可以认为等同于 HeapAlloc(__crtheap, size)
     (当然实际上 crt 内部还要维护一些内存管理的数据结构,
      所以并不是每次 malloc 都必然会触发 HeapAlloc ),
     这样 new/malloc 就和 windows 的 heap 机制吻合了。
     (这里说的是 VC 的 crt 实现,我不知道其它 crt 实现是否如此)
 
3. 如果一个进程需要动态库支持,系统在加载 dll 的时候,在 dll 的启动代码 _DllMainCRTStartup 中,
     会创建这个 __crtheap ,所以理论上有多少个 dll,就有多少个 __crtheap 。
     最后主进程的 mainCRTStartup 中还会创建一个为主进程服务的 __crtheap 。
     (由于顺序总是先加载 dll ,然后才启动 main 进程,
      所以你可以看到各个 dll 的 __crtheap 地址比较小,
      而主进程的 __crtheap 地址比较大,当然排在最前面的堆是每个进程的主 heap 。)
 
4. 从上面的分析中可以看出,对于 crt 来说,由于每个 dll 都有自己的 heap ,
     所以每个 dll 通过 new/malloc 分配的内存都是在自己 dll 内部的那个 heap 上用 HeapAlloc 来分配的,
     而如果你想在其它模块中释放,那么在释放的时候 HeapFree 就会失败了,
     因为各个模块的 __crtheap 是不一样的。
 
事情基本清楚了,在 windows 下一个进程存在着多个 heap ,
除了一个主 heap 外,还有很多的 __crtheap ,用来处理通过 C/C++ 的运行库进行的内存操作。
所以使用 new/malloc 来分配的内存实际上都是局部的,可以在多个 dll 中共享,
但是却必须是谁申请谁释放。
 
这个是 Windows 下的一个规则。以前知道这个规则,但是不知道为什么,现在算是比较明白了。
如果在 dll 内部使用 HeapAlloc(GetProcessHeap(), 0, 字节块大小 ) 来分配的内存,
是可以在 dll 以外释放的,
因为这时内存分配在全局的主 heap 上,而不是分配在 dll 自己的 __crtheap 上。