crt 运行时库dll跨模块传递crt对象,出现的崩溃问题

本文探讨了C++运行时库(CRT)堆操作在不同模块间的交互问题,特别是静态链接时可能出现的问题。通过实例演示了如何避免由于跨DLL边界传递CRT对象而导致的异常行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考链接:
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 链接运行时库是一样的场景。

所以,大家说的 谁分配谁释放遵守这个原则就没问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值