• 跨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;
    };
    

    这个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是静态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);
    

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

    首先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

  • 相关阅读:
    网络技术全方位解析之三:RAID
    Linux系统安全隐患及加强安全管理的方法
    Silverlight 程序架构
    (转载)Qt:给QLineEdit加上一个搜索按钮
    (转载)StarUML启动时候出现"System Error. Code:1722. RPC服务器不可用."错误的解决办法
    (转载)starUML connect elements exactly
    (转载)Qt:拖拽图片到QLabel上并显示
    (转载)Qt:禁止qDebug的输出
    (转载)葱的营养价值和食用功效
    (转载)Qwt的安装与使用
  • 原文地址:https://www.cnblogs.com/nuoforever/p/15159746.html
Copyright © 2020-2023  润新知