动态库调用失败分析
现象
DLL中如下:
string Test()
{
string strData = "Test Data";
return strData;
}
void Test2(string strInput)
{
。。。
}
EXE中如下:
void Test3()
{
string strRet =Test();
string strInpu;
Test2(strInpu);
};
int main()
{
Test3();
return 0;
}
在exe中调用dll中的接口时,会出现如下图所示崩溃。
调试发现是在Test3()调用结束时崩溃。
解决方案
分别在exe和dll项目上:右键->配置属性->C/C+±>代码生成->运行库->MD(MDD),然后重新编译dll和exe。
原因分析
名词解释:
MT:mutithread,多线程库,编译器会从运行时库里面选择多线程静态连接库来解释程序中的代码,即连接LIBCMT.lib库
MTd:mutithread+debug,多线程调试版,连接LIBMITD.lib库
MD:MT+DLL,多线程动态库,连接MSVCRT.lib库,这是个导入库,对应动态库为MSVCRT.dll
MDd: MT+DLL+debug,多线程动态调试库,连接MSVCRTD.lib库,对应动态库为MSVCRTD.dll
开发多线程编程时需选择上述四个中的一个(vs默认给定以上四种),单线程的情况在此不做讨论。
LibC.lib:用于单线程应用程序的静态应用程序的静态链接库
LibCD.lib:用于单线程应用程序的静态链接库的调试版
原因: 内存在一个模块中申请,但是在另一个模块中释放,释放的时候导致此种bug出现。
a)当采用MT编译时,系统会给每个模块分配不同的堆内存,Dll一块内存,exe一块内存。
b)当采用MD编译时,使用的都是exe的堆内存,则不会出现错误。
代码分析:
1、在Test3()中执行代码string strRet =Test();时,string作为Test()函数的返回值,Test函数在返回之前会调用string的拷贝构造函数(因为你strData为局部变量,函数结束会自动销毁,它的值不能作为返回值)创建一个临时对象X,然后将该对象的值赋值给strRet,在Test3()函数结束时,strRet对象会销毁,但是strRet和X是使用相同的数据空间,strRet销毁时会释放掉X的数据空间。strRet在exe中,数据空间是在dll中申请,所以会出错。(即使返回引用也同样有此问题,返回引用仍然会调用拷贝构造函数)
2、在Test3()中执行代码Test2(strInpu);时,同样会调用拷贝构造函数,不同的是,此处是在exe中分配的数据空间,在Test2()函数结束时释放。dll中释放exe中分配的空间,所以出错。
底层原理
MT编译的程序在Windows上启动时运行时库会为进程和dll分别分配内存空间,编码中使用的new/malloc对应运行时库中的HeapAlloc,HeapAlloc会给出一个heap的句柄和大小size。当进行delete/free时,各个不同模块通过HeapAlloc分配的句柄来进行内存的释放。当不同模块交叉时,申请和释放所属的句柄不相同,导致崩溃。简易图如下所示:
借用其他作者的一段话帮助理解
以下转自:https://www.cnblogs.com/WengYB/archive/2011/08/18/2144727.html
在linux下,每个进程只有一个heap,在任何一个动态库模块so中通过new或者malloc来分配内存的时候都是从这个唯一的heap中分配的,那么自然你在其它随便什么地方都可以释放。这个模型是简单的。
但是在windows下面,问题变得复杂了。
1、windows允许一个进程中有多个heap,那么这样就需要指明一块内存要在哪个heap上分配,win32的HeapAlloc函数就是这样设计的,给出一个heap的句柄,给出一个size,然后返回一个指针。每个进程都至少有一个主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(), size)来分配的内存是可以在dll以外释放的,因为这时内存分配在全局的主heap上,而不是分配在dll自己的__crtheap上)