参考链接:
https://blog.youkuaiyun.com/weixin_30653097/article/details/96512399
(讲述了,运行时库在启动的时候在exe或dll入口点之前就使用api HeapCreate创建了一个crt维护的堆。*运行时库什么时候启动的?你可以在main函数打个断点,你会发现其实系统kernal.dll模块调用的是运行时库的mianStartxxxx函数,再由运行时库调用的main入口函数,windows桌面应用程序亦是如此,这样看来,其实main也是crt库函数,只是由应用开发者实现)
https://blog.youkuaiyun.com/10km/article/details/80522287(关键点讲述了,静态链接运行时库std::string模板类时,exe和dll两个模块都在编译期间,生成了一份std::string类的代码)
https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/potential-errors-passing-crt-objects-across-dll-boundaries?view=msvc-170
(指出:通过跨 DLL 边界的函数调用将 C 运行时 (CRT) 对象(如文件句柄、区域设置和环境变量)传入或传出 DLL 时,如果 DLL 或调用到 DLL 中的任何文件使用不同的 CRT 库副本,则可能会出现意外的行为。)
由以上资料可以得出
1:运行时库在启动的时候会创建一个堆,那么有几个运行时库副本,就有几个不同的堆。
2:静态链接,会产生静态代码副本,所以静态副本启动就会伴随创建一个堆。
查阅其他资料,都说明跨dll传递crt 对象出现问题的原因是,操作了不属于自己的堆。准确的说,应该是操作的 地址 在自己的堆上没有分配。
综合这两点就能说的通 为什么/md 的形式,动态链接相同版本的运行时库不会有问题。因为动态链接的时候 无论是exe 模块还是dll模块它们自己本身没有运行时库的副本,而是将操作运行时库的堆操作放给了运行时库dll(msvcxxx.dll),只有它一个操作,且只有一个堆,那么肯定是没问题的。
接下来将验证静态链接运行时库,就是不同副本间的堆操作交叉的问题。实验exe和dll都采用的/MTD链接方式
1 首先创建一个dll,编写如下代码:
extern "C" __declspec(dllexport) void TestFunc(std::string str);//验证跨dll 操作std::string 会出现问题(释放了形式参数)
extern "C" __declspec(dllexport) void TestFunc2(std::string& str);//验证跨dll 只要不出现释放操作就不会有问题
__declspec(dllexport) TestClass TestFunc3(TestClass test);//验证就是不同模块出现了相同的代码,导致了堆操作交叉的问题(既链接2,表达的std::string 在不同模块出现了代码副本)
//TestFunc3 使用.def文件导出
//dll 实现
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
void TestFunc(std::string str){
//std::string tmp = str;
/*str = "sssssssssssssssssssssssssssssssxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";*/
std::cout << "TestFunc1" << std::endl;
}
void TestFunc2(std::string& str){
str = "sssssssssssssssssssssssssssssssxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
std::cout << "TestFunc2" << std::endl;
}
TestClass TestFunc3(TestClass test)
{
std::cout << "TestFunc3 param:test:" << std::hex<<&test << std::endl;
return test;
}
//testclass代码
//testclass.h
class TestClass{
public:
TestClass();
TestClass(const TestClass& that);
~TestClass();
private:
int* test_;
};
//testclass.cpp
#include "pch.h"
#include "export_class.h"
#include <iostream>
TestClass::TestClass()
{
std::cout << "TestClass1" << std::endl;
test_ = new int(0);
}
TestClass::TestClass(const TestClass& that)
{
std::cout << "copy TestClass1 this:"<<std::hex<< this << std::endl;
test_ = new int(*that.test_);
}
TestClass::~TestClass()
{
std::cout << "~TestClass1 this:" <<std::hex << this << std::endl;
delete test_;
test_ = nullptr;
}
//主程序代码
//main.cpp
//测试crt 跨dll 传递 crt 对象问题
typedef void(*TESTFUNC1)(std::string str);
void TestCrt(){
HMODULE dll = LoadLibrary(L"Dll_test_crt.dll");
TESTFUNC1 testfunc1 = (TESTFUNC1)GetProcAddress(dll, "TestFunc");
std::cout << "123" << std::endl;
std::string test("12");
std::cout << "12" << std::endl;
testfunc1(test);
test = "11111111111111111111111111sssssssssssssssssssssssssssssssssssssssssssssssssss111111111111111111111111111";
testfunc1(test);
}
typedef void(*TESTFUNC2)(std::string& str);
void TestCrt2(){
HMODULE dll = LoadLibrary(L"Dll_test_crt.dll");
TESTFUNC2 testfunc2 = (TESTFUNC2)GetProcAddress(dll, "TestFunc2");
std::string test = "12";
testfunc2(test);
}
//测试当exe 和 dll 中具有一样的类的情况
class TestClass{
public:
TestClass();
TestClass(const TestClass& that);
~TestClass();
private:
int* test_;
};
TestClass::TestClass()
{
std::cout << "TestClass2" << std::endl;
//test_ = new int(0);
}
TestClass::TestClass(const TestClass& that)
{
std::cout << "copy TestClass2 this:"<<std::hex<< this << std::endl;
/* test_ = new int(*that.test_);*/
}
TestClass::~TestClass()
{
std::cout << "~TestClass2 this:" <<std::hex << this << std::endl;
/*delete test_;
test_ = nullptr;*/
}
typedef TestClass(*TESTFUN3)(TestClass test);
void TestCrt3(){
TestClass test;
HMODULE dll = LoadLibrary(L"Dll_test_crt.dll");
TESTFUN3 testfunc3 = (TESTFUN3)GetProcAddress(dll, "TestFunc3");
std::cout << "草泥马1" << std::endl;
TestClass result = testfunc3(test);
std::cout << "result:" << std::hex << &result << std::endl;
std::cout << "草泥马2" << std::endl;
}
int main(int argc, char* argv[]) {
//TestCrt();
//TestCrt2();
TestCrt3();
system("pause");
return 0;
}
经过测试TestCrt()在释放形式参数时,会崩溃。
TestCrt2() 对传入的str引用重新赋了值还是没崩(和想象中的有点不一样)
TestCrt3()的输入可以解释出一些东西:
TestClass2
草泥马1
copy TestClass2 this:00AFF904
TestFunc3 param:test:00AFF904
copy TestClass1 this:00AFF928
~TestClass1 this:00AFF904
说明下TestClass2 是exe中TestClass类的构造函数输出的,以TestClass2 来标识exe中的TestClass;以TestClass2 来标识dll中的TestClass类。
在程序输出~TestClass1 this:00AFF904 后就崩溃了
从中可以看出,在分配形式参数时调用的exe中的TestClass类的拷贝构造函数,而在返回时,调用的是dll中的TestClass类拷贝构造函数,当TestFunc3结束时,释放形式参数的时候,调用的dll中的TestClass类的析构函数。
所以看出来就是在分配形式参数时,调用的外部的构造函数,释放的时候调用的本模块的构造函数,既构造和析构不配对。所以new test_和delete test_ 不配对,操作的crt堆不配对。
如果把 操作test_成员变量的代码全部屏蔽,会发现程序不会崩溃,因为没有操作堆上的数据,所以那么微软文档说的 crt对象,应该就是堆上分配的数据块。
所以只要涉及到堆上使用crt分配的数据(在堆上)都不应该静态链接crt跨dll模块传递.
注意是crt分配的数据,如果采用 api 创建和操作堆上数据,是没问题的。因为这种情况相当都是交给系统的dll操作,只有它一个操作。这种情况就和/MD 链接运行时库是一样的场景。
所以,大家说的 谁分配谁释放遵守这个原则就没问题。