从C++应用程序分配的内存在C++/CLI DLL中读取时出现AccessViolationException异常

10

我有一个C++客户端连接到一个C++/CLI DLL,该DLL初始化了一系列的C# dll。

这曾经是有效的。发生故障的代码没有改变。在抛出异常之前没有调用已更改的代码。我的编译环境已更改,但在与以前相似的环境中重新编译仍然失败。(编辑:正如我们在答案中看到的那样,这不完全正确,我只是在旧环境中重新编译库,而不是库和客户端一起升级。客户端项目已经升级,不能轻松回退。)

除了我以外的其他人重新编译了库,我们开始遇到内存管理问题。作为字符串传递的指针不能位于进程地址空间的底部64K。我重新编译它,所有东西都没有进行代码更改,一切顺利。(警报#1)最近它被重新编译,并且与字符串相关的内存管理问题再次出现,但这次它们无法消失。新错误为未处理的异常:System.AccessViolationException:企图读取或写入受保护的内存。这通常是其他内存损坏的指示。

我很确定问题不在我看到异常的地方,代码在成功和失败的构建之间没有改变,但我们应该审查以完成。忽略那些名称,我对它正在使用这些字符串的设计没有太多控制。对于混淆表示歉意,注意_bridgebridge是不同的事物。由于这个问题已经太长了,许多行代码都被省略了。

库中定义:

struct Config
{
    std::string aye;
    std::string bee;
    std::string sea;
};

extern "C" __declspec(dllexport) BridgeBase_I* __stdcall Bridge_GetConfiguredDefaultsImplementationPointer(
    const std::vector<Config> & newConfigs, /**< new configurations to apply **/
    std::string configFolderPath, /**< folder to write config files in **/
    std::string defaultConfigFolderPath, /**< folder to find default config files in **/
    std::string & status /**< output status of config parse **/
    );

客户端函数中:

GatewayWrapper::Config bridge;
std::string configPath("./config");
std::string defaultPath("./config/default");
GatewayWrapper::Config gwtransport;
bridge.aye = "bridged.dll";
bridge.bee = "1.0";
bridge.sea = "";
configs.push_back(bridge);
_bridge = GatewayWrapper::Bridge_GetConfiguredDefaultsImplementationPointer(configs, configPath, defaultPath, status);
请注意,崩溃的库调用位于与向量声明、结构声明、字符串赋值和向量推入相同的作用域中。此代码段中没有线程调用,但有其他正在执行其他任务的线程。此处没有指针计算,在该区域内除了标准库之外可能没有堆分配。我可以在调试器中运行代码,直到Bridge_GetConfiguredDefaultsImplementationPointer调用,并且调试器中configs向量的内容看起来正确。回到库中,在第一个子函数中,调试器无法解决,我已将失败语句分解为多个控制台打印。
System::String^ temp
List<CConfig^>^ configs = gcnew List<CConfig ^>((INT32)newConfigs.size());
for( int i = 0; i< newConfigs.size(); i++)
{
  std::cout << newConfigs[i].aye<< std::flush; // prints
  std::cout << newConfigs[i].aye.c_str() << std::flush; // prints
  temp = gcnew System::String(newConfigs[i].aye.c_str());
  System::Console::WriteLine(temp); // prints
  std::cout << "Testing string creation" << std::endl; // prints
  std::cout << newConfigs[i].bee << std::flush; // crashes here
}

如果我将newConfigs[i].bee移动到temp的赋值上面,或者注释列表声明/赋值,则在访问bee时会发生相同的异常。

仅供参考,一个在向量中的结构体中的std::string应该已经到达了它的目的地

为什么我的try/catch没有捕获这个异常

https://dev59.com/z3RB5IYBdhLWcg3w9Lvn#918891

通用AccessViolationException相关问题

上述问题中的建议

  • 更改为.NET 3.5,更改目标平台 - 这些解决方案可能会对具有多个项目的大型解决方案造成严重问题。
  • HandleProcessCorruptedStateExceptions - 不适用于C++,此装饰仅适用于C#。捕获此错误可能是个很糟糕的主意。
  • 更改legacyCorruptedStateExceptionsPolicy - 这是关于捕获错误,而不是防止它
  • 安装.NET 4.5.2 - 无法安装,已经有4.6.1。 安装4.6.2没有帮助。 在另一台未安装4.5或4.6的计算机上重新编译也没有帮助。(尽管在安装Visual Studio 2013之前,这在我的计算机上编译和运行过,这强烈暗示.NET库存在问题?)
  • VSDebug_DisableManagedReturnValue - 我只看到这与调试器中的特定崩溃有关,并且Microsoft的帮助表示其他AccessViolationException问题可能是无关的。(http://connect.microsoft.com/VisualStudio/feedbackdetail/view/819552/visual-studio-debugger-throws-accessviolationexception
  • 更改Comodo防火墙设置 - 我不使用此软件
  • 将所有代码更改为托管内存 - 不可行。通过C++ / CLI从C++调用C#的整体设计是不易更改的。我被特别要求以这种方式设计它,以便从现有的C++代码中利用现有的C#代码。
  • 确保分配内存 - 内存应在C++客户端的堆栈上分配。我试图使向量不是引用参数,强制将向量副本复制到显式库控制的内存空间中,但没有帮助。
  • "未受管理代码中的访问冲突会在受管理代码中显示为AccessViolationException。" - 事实,而不是解决方案。

1
我不能确定,但我认为这是标准库实现不匹配的问题:你的C++代码使用的std::string定义很可能与你的C++/CLI代码使用的std::string定义不同。确保两个项目都使用/MD,参见这里这里获取更多信息。 - Lucas Trzesniewski
好的想法,但那不是它。两个调试版本都使用/MDd,两个发布版本都使用/MD。 - Denise Skidmore
继续发布第二个链接作为答案。它是相关的。我们第一次遇到这个问题时,有人开始在Visual Studio 2013中编译库,而客户端仍在使用2010进行编译。我一直认为2013是一个问题,所以当客户端开始在2013中编译时,我仍然在使用2010编译库,但实际上不匹配才是问题,而不是特定的版本。在2013中同时编译两者就可以正常工作。 - Denise Skidmore
好的,如果是这样的话,那就清楚了。就像我说的,这只是一个运行时库不匹配的问题。你现在尝试做的事情是使用一个分配器来分配对象,然后使用来自不同运行时版本的另一个分配器来释放它 - 这样做可能会出现问题。我稍后会尝试写一个答案。 - Lucas Trzesniewski
不是释放内存,只是读取。在库函数返回并且原始对象超出范围之后才应该被释放。 - Denise Skidmore
显示剩余5条评论
3个回答

9
但问题是不匹配,而不是特定版本的问题。
是的,在VS中这是黑体法律。不幸的是,您错过了内置于VS2012中的对策,将此错误转换为可诊断的链接器错误。以前(在VS2010中),CRT将使用HeapAlloc()分配自己的堆。现在(在VS2013中),它使用默认进程堆,即GetProcessHeap()返回的那个。
这本身足以在Vista或更高版本上运行应用程序时触发AVE,从一个堆分配内存并从另一个堆释放会在运行时触发AVE,在启用Debug Heap进行调试时会触发调试器停止。
问题还不止于此,另一个重要问题是std::string对象布局在不同版本之间不同。您可以通过一个小测试程序发现这一点:
#include <string>
#include <iostream>

int main()
{
    std::cout << sizeof(std::string) << std::endl;
    return 0;
}
  • VS2010 调试版:32
  • VS2010 发布版:28
  • VS2013 调试版:28
  • VS2013 发布版:24

我模糊地记得 Stephen Lavavej 提到了 std::string 对象大小的缩减,这也被作为一种特性介绍过,但我找不到相关内容了。调试版本多出来的 4 字节是由于迭代器调试特性所致,可以在预处理器定义中使用 _HAS_ITERATOR_DEBUGGING=0 来禁用它。虽然这不是你想轻易舍弃的特性,但如果混合调试版和发布版的可执行文件及其动态链接库,则会带来很大的风险。

毫无疑问,不同的对象大小严重影响 Config 对象在使用一个版本的标准 C++ 库构建的 DLL 中创建时所产生的结果。许多问题,其中最基本的一个是代码将简单地从错误的偏移量读取 Config::bee 成员。 几乎可以保证会发生 AVE(应用程序验证错误)。当代码分配小型 Config 对象但写入大型 std::string 时,会随机破坏堆或栈帧,造成更多的痛苦。

不要混合使用。


6
我相信2013年引入了许多STL容器内部数据格式的更改,作为减少内存使用和提高性能的推动的一部分。我知道vector变得更小了,而string基本上是一个精美的vector<char>Microsoft承认不兼容性
“为了启用新的优化和调试检查,Visual Studio实现的C++标准库有意打破了二进制兼容性。因此,在使用C++标准库时,使用不同版本编译的目标文件和静态库不能混合在一个二进制文件(EXE或DLL)中,也不能在使用不同版本编译的二进制文件之间传递C++标准库对象。”
如果你要在可执行文件和/或DLL之间传递std::*对象,必须确保它们使用的是相同版本的编译器。建议在启动时让客户端及其DLL以某种方式协商,比较任何可用的版本(例如编译器版本+标志、boost版本、directx版本等),以便快速捕获此类错误。将其视为跨模块断言。
如果您想确认这是否是问题,可以选择一些来回传递的数据结构,并检查它们在客户端与DLL中的大小。我怀疑你上面的Config类在其中一个失败案例中会注册不同。
我还想提一下,在DLL调用中使用智能容器可能是一个不好的主意。除非您可以保证应用程序和DLL不会尝试释放或重新分配对方容器的内部缓冲区,否则您可能会遇到堆破坏问题,因为应用程序和DLL各自拥有自己的内部C++堆。我认为这种行为最多被视为未定义。即使传递const &参数,在某些情况下仍可能导致重新分配,因为const不能阻止编译器操作mutable内部。

抱歉,如果我能够分配奖金,我一定会这样做的。 - Denise Skidmore
另一个问题讨论了不同对象的大小。http://stackoverflow.com/questions/14090341/difference-in-byte-size-of-stl-containers-in-visual-studio-2010-and-2012 - Denise Skidmore

0

你似乎遇到了内存损坏的问题。Microsoft Application Verifier 是发现损坏的宝贵工具。使用它来找到你的 bug:

  1. 将其安装到你的开发机器上。
  2. 将你的 exe 添加到其中。
  3. 只选择 Basics\Heaps
  4. 按保存。如果你保持应用程序验证器打开,也没关系。
  5. 运行你的程序几次。
  6. 如果它崩溃了,请调试它,这一次,崩溃将指向你的问题,而不是程序中的某个随机位置。

PS:在开发项目中始终启用 Application Verifier 是一个好主意。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接