zoukankan      html  css  js  c++  java
  • __acrt_first_block == header 异常原因分析

    问题描述
    最近在写dll动态库时,动态库函数返回的std::string对象在析构时抛出了异常:

    为简化描述问题,测试代码如下(MSVC /MT 编译),就是返回一个简单的std::string
    tools.h

    # if defined(_WIN32) && !defined(__CYGWIN__)
    # ifdef GFAUX_EXPORTS
    # define GAX_API __declspec(dllexport)
    # else
    # define GAX_API __declspec(dllimport)
    # endif
    # else
    # define GAX_API
    # endif
    #include <string>
    // 返回一个std::string
    GAX_API std::string test();

    tools.cpp

    #include "tools.h"
    std::string test()
    {
    return std::string("hello!!!!!");
    }

    原因分析
    关于__acrt_first_block == header异常,google上查了一下,根本的原因是对象在析构时不正确的释放内存导致的。stackoverflow上这篇文章的回复写得比较清晰:Debug Assertion Failed! Expression: __acrt_first_block == header

    std::string是STL中定义的模板类,所以编译器在编译动态库时会将std::string实例化,在编译exe时也会将其实例化,也就是说有两套std::string实例代码分别在exe和dll中.
    因为我的dll是/MT编译的所以连接的是static crt,所以动态库有自己独立的heap,参见参考资料3.
    那么问题来了:
    如下面的exe调用代码,当test()返回一个std::string对象给exe时,这个对象的内存是由dll分配的。但在exe中并不能区分这个std::string对象的内存是不是自己的的heap中分配的。在main结束时要析构result,会调用exe中实例化的std::string析构函数代码来释放内存,然后就会抛出__acrt_first_block == header异常。
    调用测试代码
    main.cpp

    #include <iostream>
    #include "tools.h"
    int main(int argc, char *argv[]) {
    std::string result = test();// 从dll返回std::string,result的内存是由dll分配的
    std::cout << result << std::endl;
    } // 析构result时抛出异常

    如果和exe和动态库都是/MD编译,不会存在上述问题,因为大家使用同一个heap,内存在哪里释放都是一样的。
    但我的项目需要必须用静态链接(/MT)所以不能通过修改动态库的编译方式的方法解决问题。

    解决方案
    知道了原因,就可以推导出解决问题的关键在于不能让exe去析构dll返回的std::string,简单的办法就是在dll中定义一个只包含一个std::string类型成员的class A,test()返回类型改为class A,这样以来exe就不再直接析构std::string,而是析构dll中的class A,class A在析构成员时就能正确释放在当前dll中heap分配的内存了。
    如果为每个需要封装的类型都定义一个class A也够烦的,所以可以把这个class A设计成一个模板类raii_dll,它不干别的,只是为了正确释放dll或exe中的对象。代码如下:

    /* 用于dll分配的资源T的raii管理类,析构时自动正确释放资源
    * T为资源类型,外部不可修改
    */
    template<typename T>
    class raii_dll {
    public:
    typedef raii_dll<T> _Self;
    typedef T resource_type;
    /* 默认构造函数 */
    raii_dll() :_resource() {}
    /** res 资源对象 */
    explicit raii_dll(const T& res) :
    _resource(res) {
    }
    /* 获取资源引用 */
    const T& get() const noexcept { return _resource; }
    const T& operator*() const noexcept { return get(); }
    const T& operator()() const noexcept { return get(); }
    /* 成员指针引用运算符 */
    const T* operator->()const noexcept { return &get(); }
    private:
    /* 封装的资源对象,外部不可修改 */
    T _resource;
    }; /* raii_dll */
    请注意为了确保dll返回的对象不会被赋值为exe的内存对象,这里get()返回的是常量引用(const &)
    有了raii_dll这个模板类,我们可以重新设计一下test()的接口定义

    tools.h

    # if defined(_WIN32) && !defined(__CYGWIN__)
    # ifdef GFAUX_EXPORTS
    # define GAX_API __declspec(dllexport)
    # else
    # define GAX_API __declspec(dllimport)
    # endif
    # else
    # define GAX_API
    # endif
    #include <string>
    #include "raii_dll.h"
    // 实例化并导出模板raii_dll,确保只在dll中有一份raii_dll<std::string>实例代码
    template class GAX_API raii_dll<std::string>;
    // 返回raii_dll<std::string>
    GAX_API raii_dll<std::string> test();

    tools.cpp

    #include "tools.h"
    raii_dll<std::string> test()
    {
    return raii_dll<std::string>("hello!!!!!");
    }

    调用测试代码也同步修改
    main.cpp

    #include <iostream>
    #include "tools.h"
    int main(int argc, char *argv[]) {
    raii_dll<std::string> result = test();
    // 调用operator()返回对象引用
    std::cout << result() << std::endl;
    }

    总结
    通过这次跳坑填坑的经历,针对动态的接口设计可以总结几点设计原则,以避免上述的问题,就可以传递复杂类型:

    动态库设计接口时,应该避免直接返回stl类型,如果不可避免(比如本例),就封装将其成一个类返回(可以照搬本文的方法)
    动态库接口函数的输入/出参数如果是class,应尽量设计为常量引用(const &),不允许被修改。
    如本例,如果允许raii_dll中的_resource被exe重新赋值,程序立即就崩了。
    参考资料
    《Debug Assertion Failed! Expression: __acrt_first_block == header》
    《跨DLL的内存分配释放问题 Heap corruption》
    《Windows 下主程序与动态库(*.dll)释放对方分配的内存操作要点》
    ————————————————
    版权声明:本文为CSDN博主「10km」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/10km/article/details/80522287

  • 相关阅读:
    [leetcode]397. Integer Replacement
    [LeetCode]396. Rotate Function
    [leetcode]633. Sum of Square Numbers
    [LeetCode]367. Valid Perfect Square判断完全平方数
    [LeetCode]319. Bulb Switcher灯泡开关
    [LeetCode]313. Super Ugly Number超级丑数,丑数系列看这一道就行了
    [LeetCode]231. Power of Two判断是不是234的幂
    使用导入外部样式文件方式
    使用内部样式
    使用链接外部样式文件方式
  • 原文地址:https://www.cnblogs.com/nuoforever/p/15159723.html
Copyright © 2011-2022 走看看