Fork me on GitHub

一个模块分配的内存由另外一个模块释放~引发的血案

exe调用了cglover.dll中的一个导出类成员函数,是一个读取lua配置文件类,其中一个函数是读取int数组,并通知参数std::vector传递。
结果一出调用函数作用域,就挂调用,挂在std::vector的析构释放内存上~ 郁闷找不到原因,往床上一躺,突然想起关于很早以前就在游戏编程精粹上看到过exe释放dll分配的内存会有问题,但一直没明白为什么,也没有深究,应该是这个问题了。于是上网查了下,然后用windbg跟了下,总算整明白了。

Malloc和Free由CRT提供,分别是

  • DEBUG版本:
    • MSVCR71D.DLL (C运行时库)
    • MSVCP71D.DLL (C++运行时库)
  • RELEASE版本:
    • MSVCR71.DLL (C运行时库)
    • MSVCP71.DLL (C++运行时库)

Malloc和Free内部实际是调用系统提供的HeapAlloc和HeapFree来实现的(Kernel32.dll),这两个API需要HeapCreate创建返回的内存堆HANDLE。
但却不是使用缺省的进程堆(GetProcessHeap), 而是在XXXCRTStartUp入口函数中创建的一个全局句柄HANDLE _crtheap(入口函数中调用CreateHeap);
其实也并不一定是入口函数中创建,这只是针对静态链接CRT库而已,对于动态链接CRT库的,在整个应用程序中,所有动态链接CRT库的使用同一个_crtheap(即CRT模块中的_crtheap),它在ntdll!LdrpCallInitRoutine中调用,并且之后动态链接CRT的模块不再调用heap_init。

经过使用windbg测试,发现Exe或Dll使用静态链接CRT库那么就会在CRTStartup入口函数中静态链接_CRTDLL_INIT中调用的_heap_init函数(里面调用CreateHeap),但是所有使用动态链接CRT库的模块的就不会在CRTStartup中再调用了,整个应用程序装载的时候之后,确定有模块动态链接CRT库,那么就调用MSVCR71D!_CRTDLL_INIT,这时就设置了CRT库的全局变量堆句柄_crtheap,所有动态链接CRT的模块就用到它了。
总结一句,就是这个进程中,所有动态链接CRT库的模块公用CRT模块的heap,而所有静态链接CRT库的模块(包括进程本身)使用自身创建的heap。

动态链接

1
2
3
4
5
6
7
8
9
10
11
12
13
。。。
call_dlld!mainCRTStartup+0x142
kernel32!BaseProcessStart+0x23
。。。。
ntdll!RtlCreateHeap (FPO: [Non-Fpo])
kernel32!HeapCreate+0x55 (FPO: [3,0,4])
MSVCR71D!_heap_init+0x1a (FPO: [Non-Fpo]) (CONV: cdecl) [f:\vs70builds\3077\vc\crtbld\crt\src\heapinit.c @ 173]
MSVCR71D!_CRTDLL_INIT+0xab (FPO: [Non-Fpo]) (CONV: stdcall) [f:\vs70builds\3077\vc\crtbld\crt\src\crtlib.c @ 212]
ntdll!LdrpCallInitRoutine+0x14
ntdll!LdrpRunInitializeRoutines+0x344 (FPO: [Non-Fpo])
ntdll!LdrpInitializeProcess+0x1131 (FPO: [5,89,4])
ntdll!_LdrpInitialize+0x183 (FPO: [Non-Fpo])
ntdll!KiUserApcDispatcher+0x7

静态链接

1
2
3
4
kernel32!HeapCreate+0x55 (FPO: [3,0,4])
call_dlld!_heap_init+0x1a
call_dlld!mainCRTStartup+0xc1
kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])

就是这样了!

当Dll(分配内存)和Exe(释放内存)都使用静态链接CRT库,那么悲剧发生了:Dll分配使用的是Dll的_crtheap全局句柄分配堆,Exe释放使用Exe的_crtheap全局句柄分配堆。

在Debug模式下会有检查释放内存是否在自身的堆内存block链表中,但在Release模式下却是不可预知的崩溃了。

但是当二者都是使用动态链接CRT DLL,那么malloc和free对应使用的堆句柄就是CRT DLL中的_crtheap了。这依赖于各个模块的编译选项设置(都设置为动态链接CRT库)

所以最根本的错误在于:不同的模块使用了不同的堆。
所以必须保证使用同一个堆。
所以最根本的做法还是,哪个模块分配的,由那个模块来释放。

可选的方法有:
1。在DLL中输出一个函数给EXE调用,专门用来释放由DLL分配的内存;
2。用GlobalAlloc()代替new,用GlobalFree()代替delete;
3。使用单一的堆,分配内存使用HeapAlloc(GetProcessHeap(),0,size),释放内存使用HeapFree(GetProcessHeap(),0,p);
4。把dll和exe的Settings的C/C++选项卡的Code Generation的Use Run-time liberary改成Debug Multithreaded DLL,在Release版本中改成Multithreaded DLL;这样使用一个CRT了——MSVCRT.DLL。


(编者注:在不同版本的vc runtime之间也不能混用new和delete,即使用的都是CRT DLL。)


本文地址:http://xnerv.wang/release-memory-allocated-by-another-module/
转载自:一个模块分配的内存由另外一个模块释放~引发的血案