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

Posted

技术标签:

【中文标题】用户 DLL/EXE 中的堆分配失败【英文标题】:Heap allocation failing in user DLL/EXE 【发布时间】:2018-01-29 23:34:43 【问题描述】:

正确链接的 DLL 和 EXE 应该有一个空闲存储区,它们都可以从中分配基于堆的对象。这是 Chis Becke 在Who allocates heap to a DLL? 中的回答:

…C++ 运行时负责创建它的 freestore 并决定 如何分配它。 具体来说,如果您使用 Dll 运行时选项,则单个 dll - msvcrtxx.dll - 管理单个 在所有 dll 和 exe 之间共享的 freestore,这些 dll 链接到该 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 ] = '\0';


// Calling code in NewFailMfc2.
Www();

有比我更了解 DLL/EXE freestore 工作原理的人知道问题出在哪里吗?

(我之前曾尝试在“全局函数::operator newMyApp1MyApp2编译时失败。在询问过程中,我发现问题比@987654335更普遍地发生@标准库。)

编辑1:

在 MSDN 中,一个不错的虚拟代理为我找到了 Potential Errors Passing CRT Objects Across DLL Boundaries。不幸的是,它推荐的唯一解决方案是使用/MD 编译器选项编译所有程序,而不是/MT,它使用CRT 的多个副本,这会自动导致越界 和内存访问冲突。

对于像我这样的应用开发者来说,这可不是什么好消息。我需要的是一个最佳实践,这样我就可以应用它并满足我的交付期限,而不必处理神秘的低级内存问题。我怎么知道std:random_device 类型中有一个对全局::operator new 的隐藏调用?我不会,直到它违反访问权限。直到现在,在所有这些研究之后,我才意识到,通过调用全局 new,它跨越了边界,这给了我的 DLL/EXE 访问冲突。非常晦涩。

EDIT2:

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

【问题讨论】:

“根据 Chris 的说法,msvcrtxx.dll 和编译时/运行时链接器负责获取所有 DLL/EXE 的联合 freestore。” ——他不是这么说的,也不是真的。 这是过时的信息,自 VS2012 以来就不是这样了。以前,是的。您必须确保使用完全相同的设置使用完全相同的 VS 版本构建所有模块。可以像使用 dll 的发布版本来调试 exe 一样简单。确保所有项目都在同一个解决方案中,以便它们都可以使用相同的设置。 我相信 Hans 是正确的——通常我保持一个经验法则,即每个模块分配和处理它自己的内存,因为以这种方式在进程之间共享内存比它应该做的工作要多得多。至少在不使用内置规定的情况下这样做——不确定 Windows 是否像 Linux 那样容易公开。 @Hans Passant 我知道所有参与的 DLL 和 EXE 必须是相同的版本。我在 MSDN 中找到了一篇文章,解释了使用堆对象跨越 DLL/EXE 边界的危险。 “我需要的是一个最佳实践,这样我就可以应用它并满足我的交付期限,而不必处理神秘的低级内存问题”你一定是原生编程新手。 【参考方案1】:

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

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

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

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

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

【讨论】:

/MDd 是我的NewFailMfc1 和`NewFailMfc2` 生成的测试程序中的默认运行时库选项值,称为多线程调试DLL。这意味着/MD 编译器选项不能成为解决边界交叉问题的候选者。 因为实际上只有一个进程在调用另一个 EXE 中创建的二进制文件,所以在 IPC 意义上真的没有边界跨越。我现在看到了。那么究竟是什么边界导致了访问冲突呢? (见 EDIT2)【参考方案2】:

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

【讨论】:

COM 是跨进程边界共享堆的唯一方法吗?还是有其他低级的方式?对 Pipes 之类的标准 IPC 甚至 COM 不感兴趣,因为这些工具会为项目带来太多复杂性。 COM 在这里帮不上忙。您需要实现在进程之间共享的共享内存模型,并使用 C++ 放置 new 运算符在该共享内存中构造\销毁对象。您可以在 DLL 中实现共享内存模型,以便加载 DLL 的进程可以访问共享内存。请检查此链接以在 DLL 中使用共享内存。您也可以考虑任何其他方法,但需要某种共享内存模型来满足您的要求。 msdn.microsoft.com/en-us/library/windows/desktop/… 以下链接描述了一种有效的算法,用于跨进程边界从共享内存段分配和释放内存codeproject.com/Articles/14525/…【参考方案3】:

Explicit Instantiation

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

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

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

从应用程序开发人员的角度来看,我认为我的原始代码应该可以正常工作,或者至少产生的错误更能说明问题所在,因此我将在 EDIT2 中提及我的错误报告strong> 有效。

【讨论】:

【参考方案4】:

忘记这一切。如果这能以某种方式起作用,那只是你的运气。

更好的方法是运行COM 进行内存共享。只需在此处查看IMalloc 示例。 ftp://210.212.172.242/Digital_Library/CSE/Computer,%20Technology%20and%20Engineering%20eBooks/Books7/petzold_rus.part1%20(2)/DISK/CODE/CHAP20/

没那么容易是的。

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

想提一下,在发布的示例中(win95 次)你正在收集 IMallocole 从地面级接口。它可以被认为是 MS Windows 源代码的一部分。我对今天的内置 n IMalloc 有任何了解,不确定是否相同。

【讨论】:

请澄清你的答案。 无法访问 petzold 链接。你能在你的答案中发布有问题的段落吗?

以上是关于用户 DLL/EXE 中的堆分配失败的主要内容,如果未能解决你的问题,请参考以下文章

在C中的堆上分配数组

对象向量是不是在 C++ 中的堆或堆栈上分配?

C 程序是不是可以访问和更改分配给另一个程序的堆中的内存地址?

数据结构中的堆和栈 与 内存分配中的堆区和栈区 分析

线程特定的堆分配

JVM的堆分配