__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

原文地址:https://www.cnblogs.com/nuoforever/p/15159723.html