跨DLL的内存分配释放问题 Heap corruption
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
转自:http://blog.****.net/zj510/article/details/35290505
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
转自:http://blog.****.net/zj510/article/details/35290505
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
这是个很典型的问题,在MSDN上也有描述。问题是这样的:
在一个DLL里面分配内存,然后在DLL的调用者EXE那里释放内存。
当DLL和EXE里面有一个是使用MT连接CRT的时候就有问题。如果DLL和EXE都使用MD,那么就没有问题。
先来看一下问题
直接使用原生指针来传递
在DLL里面创建一个导出函数,如:
- void TestOriginalPointer(int** p)
- {
- delete *p;
- int* temp = new int;
- *temp = 1;
- *p = temp;
- }
调用者代码:
- // test1
- typedef void(*fTest)(int**);
- fTest TestOriginalPointer = (fTest)GetProcAddress(h, "TestOriginalPointer");
- int* p = new int;
- *p = 0;
- TestOriginalPointer(&p);
这个示例代码在DLL和EXE都是MD连接CRT的时候是没有问题,但是当有一个是MT的时候就crash。看一下调用堆栈
当DLL里面的函数TestOriginalPointer尝试去delete的时候,就crash了。再来看个例子:
创建一个class来传递一段内存
- class MyWrapper
- {
- public:
- explicit MyWrapper(int* p) : m_p(p)
- {}
- ~MyWrapper()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
在DLL里面再创建一个导出函数:
- void TestMyWrapper(MyWrapper& p)
- {
- p.ChangeValue(new int);
- }
调用:
- // test2
- typedef void(*fTestMyWrapper)(MyWrapper& p);
- fTestMyWrapper TestMyWrapper = (fTestMyWrapper)GetProcAddress(h, "TestMyWrapper");
- MyWrapper w(new int);
- TestMyWrapper(w);
看了这两个例子,我们来分析一下根本原因吧。
根本原因
假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。
C++的new在windows上面,应该就是用malloc来实现的,malloc是CRT的一个函数。
在第一个例子中,假如EXE分配的内存地址是0x00008952,那么这个地址只有在灰色的那个CRT里面才有效,它指向了一块内存。然后我们在DLL里面想释放,就调用delete,这里问题就来了,DLL里面静态link了CRT, 那么delete的时候就会在DLL里面的CRT的heap里面找地址0x00008952,鬼知道指向哪里,这个时候去delete就会导致不可预测的后果了。所以这个问题的根本原因就是同一个内存地址在不同的CRT里面指向的地方是不一样。
如果DLL和EXE都是动态link crt,那么就没这个问题了,因为动态link的时候,就只有一个CRT DLL.DLL和EXE都用的是同一个CRT, 所以没问题。但是一旦其中有一个使用了静态link,就出问题了,这个时候就有2个CRT了。每一个静态link crt的DLL或者EXE, 内部都有自己的一份copy。
那么有什么解决方案呢?首先我觉得我们应该尽量避免DLL里面分配,EXE释放,或者反过来。这种代码会有隐患的。但是有些时候不可避免的时候,怎么办呢?办法也是有的。其实我们可以这么想,假设分配和释放是在同一个CRT里面就没有这个问题了。那么我们如何做到这一点呢?malloc,new等函数,我们是不能改变的,但是我们可以考虑给他们包装一层。我们可以使用虚函数。如果我们创建2个虚函数,一个用来分配内存,一个用来释放内存。在对象构造的时候,这个对象的虚表里面就已经指向了创建这个对象的模块里面的CRT的new和delete,那么当我们在DLL里面调用虚函数来释放的时候,系统会为我们找到构造对象时候的释放函数。这样就没有问题了。写代码试试吧。
用虚函数来分配释放内存
将之前的MyWrapper改造一下。其实就是将ChangeValue改成了虚函数。
- class MyWrapperEx
- {
- public:
- explicit MyWrapperEx(int* p) : m_p(p)
- {}
- virtual ~MyWrapperEx()
- {
- if (m_p)
- {
- delete m_p;
- m_p = nullptr;
- }
- }
- virtual void ChangeValue(int* p)
- {
- if (m_p)
- {
- delete m_p;
- m_p = p;
- }
- }
- private:
- int* m_p;
- };
DLL里面新加一个导出函数。
- void TestMyWrapperEx(MyWrapperEx& p)
- {
- p.ChangeValue(new int);
- }
- // test3
- typedef void(*fTestMyWrapperEx)(MyWrapperEx& p);
- fTestMyWrapperEx TestMyWrapperEx = (fTestMyWrapperEx)GetProcAddress(h, "TestMyWrapperEx");
- MyWrapperEx w2(new int);
- TestMyWrapperEx(w2);
首先MyTest.exe调用MyDll2.dll的TestMyWrapperEx.然后在TestMyWrapperEx里面,当调用p.ChangeValue的时候,因为ChangeValue是虚函数,所以会通过虚表来查找,这个虚表刚好是MyTest.exe创建的,所以系统找到了MyText.exe里面的那份ChangeValue,这样new和delete就处于同一个CRT了。如果ChangeValue不是虚函数,那么在编译的时候就已经绑定好了,ChangeValue是DLL里面的那一份,这样new和delete就处于不同的CRT了,所以crash。
上面的代码其实有个问题,当TestMyWrapperEx里面调用p.ChangeValue的时候,先释放内存,在存储一个DLL里面new出来的一个内存,这样当对象析构的时候,就会发生问题了。这个对象(w2)是在EXE里面构造的,所以虚表里面的析构函数指的是EXE里面的那一份,那么现在的情况就是ChangeValue的参数指向的内存是DLL分配的,但是释放在EXE里面了,这样就又crash了。其实解决这个问题很简单,在ChangeValue的参数不要直接传个指针,可以传个需要的内存的大小,在ChangeValue内部来分配,这样就没有问题了。
其实我们可以自己创建一个专门的class来管理内存分配和释放。就好象是std::shared_ptr,如果你阅读std::shared_ptr的源代码,你会发现std::shared_ptr内部就是有一个class来处理delete,这个函数就是个虚函数。原理是差不多的。
OK,最后在总结一下,如果我们使用一个虚函数来管理new和delete,那么就可以通过虚表来找到构造对象的那个模块里面的虚函数。这样就可以保证new和delete处于同一个CRT. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。