跨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的内存分配释放问题 Heap corruption

当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;
};

这个class很简单,构造的时候,把传进来的内存地址保存一下,然后析构的时候释放,另外有一个函数可以用来改变里面的内存。

在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);    

这段代码也会crash:

跨DLL的内存分配释放问题 Heap corruption

看了这两个例子,我们来分析一下根本原因吧。

根本原因

假设DLL是静态link crt (MT),EXE是动态link (MD)。我画了个示意图。

跨DLL的内存分配释放问题 Heap corruption

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);

这样,当w2被创建的时候,w2的虚表里面指向的是EXE里面的那个虚函数ChangeValue。这样当DLL调用ChangeValue的时候,系统会根据虚表来查找虚函数ChangeValue,显然ChangeValue是EXE里面的那份。这样new和ChangeValue里面的delete就在同一个CRT里面了,就是EXE的那份CRT,所以就没有问题了。看一下call stack就会很清楚了。
跨DLL的内存分配释放问题 Heap corruption

首先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. 好像说起来还是挺简单的,但是实际上想真的搞清楚这个问题,还是得搞自己一步一步去跟一下,这样就会很清楚了。
————————————————
版权声明:本文为CSDN博主「zj510」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zj510/article/details/35290505

上一篇:Coloring Contention


下一篇:Continuous Median