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

AccessViolationException reading memory allocated in C++ application from C++/CLI DLL

我有一个 C++ 客户端到 C++/CLI DLL,它初始化一系列 C# dll。

这曾经有效。失败的代码没有改变。在抛出异常之前不会调用已更改的代码。我的编译环境变了,在和老环境差不多的机器上重新编译还是失败。 (编辑:正如我们在答案中看到的,这并不完全正确,我只是在旧环境中重新编译库,而不是库和客户端一起重新编译。客户端项目已经升级,无法轻易返回。)

除我之外有人重新编译了库,我们开始遇到内存管理问题。 The pointer passed in as a String must not be in the bottom 64K of the process's address space. 我重新编译了它,并且在没有更改代码的情况下一切正常。 (警报#1)最近重新编译,字符串的内存管理问题重新出现,这次他们不会消失。新错误是 Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

我很确定问题不在我看到异常的地方,代码在成功和失败的构建之间没有变化,但我们应该检查它是否完整。忽略事物的名称,我无法控制这些字符串的设计。很抱歉造成混淆,但请注意 _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);

请注意,对崩溃的库的调用与向量声明、结构声明、字符串赋值和向量推回在同一范围内 这段代码中没有线程调用,但是有其他线程运行ning在做其他事情。这里没有指针数学,除了标准库之外,该区域没有堆分配。

我可以 运行 在调试器中调用 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 的赋值上方或注释掉列表 declaration/assignment,我在访问 bee 时会遇到相同的异常。

仅供参考,向量结构中的 std::string 应该已经到达目的地 ok

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

捕获

Generic AccessViolationException 相关问题

以上问题的建议

你似乎有内存损坏。 Microsoft Application Verifier 在查找损坏方面非常有用。用它来查找您的错误:

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

PS:始终为您的开发项目启用 Application Verifier 是个好主意。

我相信 2013 年对 STL 容器的内部数据格式进行了大量更改,作为减少内存使用和提高性能的推动的一部分。我知道vector变小了,而string基本上是美化了vector<char>

Microsoft acknowledges the incompatibility:

"To enable new optimizations and debugging checks, the Visual Studio implementation of the C++ Standard Library intentionally breaks binary compatibility from one version to the next. Therefore, when the C++ Standard Library is used, object files and static libraries that are compiled by using different versions can't be mixed in one binary (EXE or DLL), and C++ Standard Library objects can't be passed between binaries that are compiled by using different versions."

如果您要在可执行文件 and/or DLL 之间传递 std::* 对象,您绝对必须确保它们使用相同版本的编译器。最好让您的客户端及其 DLL 在启动时以某种方式协商,比较任何可用版本(例如编译器版本 + 标志、boost 版本、directx 版本等),以便您快速捕获此类错误。将其视为跨模块断言。

如果您想确认这是问题所在,您可以选择一些来回传递的数据结构,并检查它们在客户端和 DLL 中的大小。我怀疑您上面的 Config class 在其中一个失败案例中会以不同方式注册。

我还想提一下,首先在 DLL 调用中使用智能容器可能不是一个好主意。除非您可以保证应用程序和 DLL 不会尝试释放或重新分配对方容器的内部缓冲区,否则您很容易 运行 陷入堆损坏问题,因为应用程序和 DLL 各自都有自己的内部 C++ 堆.我认为这种行为充其量被认为是未定义的。即使传递 const& 参数在极少数情况下仍可能导致重新分配,因为 const 不会阻止编译器欺骗 mutable 内部。

but it was the mismatch, not the specific version that was the problem

是的,这是 VS 中的黑字法。不幸的是,您只是错过了 VS2012 中内置的将此错误转化为可诊断链接器错误的反制措施。以前(以及在 VS2010 中),CRT 将使用 HeapAlloc() 分配自己的堆。现在(在 VS2013 中),它使用默认进程堆,即 GetProcessHeap() 返回的进程堆。

当您在 Vista 或更高版本上 运行 您的应用程序时,这本身足以触发 AVE,从一个堆分配内存并从另一个堆释放它会在 运行 时触发 AVE,在启用调试堆的情况下调试时调试器中断。

这不是结束,另一个重要问题是 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 禁用它。这不是您很快就会想要丢弃的功能,但它会使 EXE 及其 DLL 的调试和发布版本混合在一起非常致命。

不用说,当在使用一个版本的标准 C++ 库构建并在另一个版本中使用的 DLL 中创建 Config 对象时,不同的对象大小严重影响字节。许多不幸事件,最基本的一个是代码将简单地从错误的偏移量读取 Config::bee 成员。 AVE(几乎)是有保证的。当代码分配 Config 对象的小风格但写入 std::string 的大风格时,会更加痛苦,这会随机破坏堆或堆栈帧。

不要混。