用户DLL/EXE中的堆分配失败

Heap allocation failing in user DLL/EXE

本文关键字:分配 失败 DLL EXE 用户      更新时间:2023-10-16

正确链接的DLL和EXE应该有一个自由存储,它们都可以从中分配基于堆的对象。以下是Chis-Becke在"谁将堆分配给DLL?"中的回答:

…是C++运行时负责创建其自由存储并决定如何分配。具体来说,如果使用Dll运行时选项,则单个Dll-msvcrtxx.Dll-管理单个在所有dll和exe之间共享的自由存储,这些dll和exe链接到该dll

由于这是真的,那么我应该能够在其他DLL/EXE中定义的DLL/EXE的new对象。根据Chris的说法,msvcrtxx.dll和编译时/运行时链接器负责在哪里可以获得所有DLL/EXE的联合自由存储。

这对我不起作用。

为了测试这一点,我生成了两个MFC对话框程序:NewFailMfc1和NewFailMfc2。执行new时,运行访问NewFailMfc1Www函数的NewFailMfc2失败。

// Code in NewFailMfc1.
void Www()
{
char* ch { nullptr };
ch = new char[ 100 ]; // error: attempts to allocate memory somewhere else than in the prescribed joint DLL/EXE freestore
ch[ 0 ] = '';
}
// Calling code in NewFailMfc2.
Www();

有人比我更了解DLL/EXE自由存储的工作原理,知道问题是什么吗?

(我之前曾在"全局函数::operator newMyApp1MyApp2中编译时失败"中问过一次这个问题。在问的过程中,我发现这个问题比<random>标准库中更普遍。)

第1版:

在MSDN中,一个很好的虚拟代理为我发现了跨越DLL边界传递CRT对象的潜在错误。不幸的是,它建议的唯一解决方案是使用/MD编译器选项编译所有程序,而不是使用CRT的多个副本的/MT,这会自动导致跨越边界和内存访问冲突。

对于像我这样的应用程序开发人员来说,这不是一个好消息。我需要的是一个最佳实践,这样我就可以应用它并在交付截止日期前完成交付,而不必处理神秘的低级内存问题。我如何fx知道在std:random_device类型中存在对全局::operator new的隐藏调用?我不会,直到它的访问被侵犯。直到现在,经过所有这些研究,我才意识到,通过它调用全局new,它是越过了一个边界,这给了我的DLL/EXE访问违规。非常晦涩。

第2版:

我在Visual Studio中提交了一份关于std::random_device实现的错误报告。请参阅"std::random_device实例化在某些情况下会导致访问冲突"。

无论这意味着什么,都有可能跨越边界:)首先,你需要了解发生了什么。

当你分配内存时,实际上CRT可以比你要求的多分配一点。例如,流行的做法(至少在过去)是多分配4个字节(用您的系统位代替),在开始时写入分配的内存大小,并向您返回ptr + 4。因此,当你释放内存时,系统知道它应该释放多少。

这是一个有点简化的画面。不同的编译器、同一编译器的不同版本以及同一编译器同一版本的不同配置可以做不同的事情。例如,调试配置可以使用一些填充来检测缓冲区溢出,以及其他技巧。因此,当您在一个二进制文件中分配内存并在另一个二进制中解除分配时,如果使用不同的编译器,这可能会导致内存损坏(在最好的情况下会立即崩溃)。

这和许多其他原因导致了一个常见的建议:在分配内存的二进制文件中释放内存。这通常是通过提供API类的Release成员函数并使析构函数私有来实现的,或者通过unique_ptr(或shared_ptr)使用自定义deleter或其他技术来实现的。

现在谈谈/MD的建议。/MD表示动态CRT(=在dll中),由于不可能在同一进程中两次加载相同的dll,这意味着相同的CRT将用于分配和解除分配。这仍然不是针对不同版本或不同编译器的解决方案。例如,许多应用程序使用插件系统,在这种情况下,要求所有插件都由特定的编译器/版本/配置编译不是一个好主意

多线程调试DLL堆将是每个进程的,因此NewFailMfc1和NewFailMfc2将有自己的私有堆,即使这两个应用程序都与多线程调试DLL链接。使用多线程调试DLL堆只能解决在同一进程地址空间内跨多个堆跨越边界的问题,而不是一种可以用于跨进程边界共享堆的机制。

显式实例化

显式实例化强制编译器为模板类或函数的特定参数列表生成代码。如果没有该代码,我导入的DLL/EXE二进制文件在运行时实例化(如ch = new char[ 100 ]std::random_device rd;)时会失败,这会隐式地执行全局::operator new。我找不到任何有用的解释来解释为什么会发生这种情况。不幸的是,IPC的讨论并没有明确区分涉及多个运行进程的客户端-服务器运行时和导入编译并导出到其他地方的二进制代码(DLL/EXE)的运行时代码。

解决方案是向失败的类添加一个模板参数,并向显式实例化该类的每个模块添加一个.cpp文件,如MyClsModule1.cppMyClsModule2.cpp等。在这些文件中,我显式实例化了该模块的类。我还为每个包含extern的模块添加了.h文件,如MyClsModule1.hMyClsModule2.h,这样就不会在特定模块中发生重复的代码生成。使用这种方法,每个模块在编译时生成特定于模块的类代码,这迫使模块的线程允许访问该模块的进程堆的堆实例化。

这个现代C++解决方案对我来说很优雅,因为它使我不必在应用程序代码中重新引入复杂的IPC解决方案,如COM

从应用程序开发人员的角度来看,我认为我的原始代码应该有效,或者至少生成了更能说明问题所在的错误,所以我将保留EDIT2中提到的错误报告。

忘了这一切。如果这一切以某种方式奏效,这只是你的运气。

更好的方法是运行COM进行内存共享。看看这里的IMalloc样本。ftp://210.212.172.242/Digital_Library/CSE/Computer,%20技术%20和%20工程%20电子书/Books7/petzold_rus.part1/20(2)/DISK/CODE/CHAP20/

不是那么容易。

正如本规范前面所述分配的内存通过接口传递,COM需要为内存分配特定的。。。。http://www.opengroup.org/comsource/techref2/CHP05GDC.HTM

想提到的是,在发布的样本(win95次)中,您正在收集IMallocole来自地面阶段的接口。它可以被认为是MS Windows源代码的一部分。我不知道今天的建筑——nIMalloc,不确定它是一样的。