调用 SysFreeString() 时出现堆损坏错误

Posted

技术标签:

【中文标题】调用 SysFreeString() 时出现堆损坏错误【英文标题】:heap corruption error when calling SysFreeString() 【发布时间】:2013-08-20 17:25:44 【问题描述】:

// --------------------------------------- C# 代码 ---------------- --------------

    [DllImport("MarshallStringsWin32.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    extern static void PassStringOut([MarshalAs(UnmanagedType.BStr)] out String str);

    [DllImport("MarshallStringsWin32.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    extern static void FreeString([MarshalAs(UnmanagedType.BStr)] String str);

    static void Main(string[] args)
    
        String str;
        PassStringOut(out str);
        FreeString(str);
    

// --------------------------- C+ 代码 ---------------- --------------

void PassStringOut(__out BSTR* str)

   const std::string stdStr = "The quick brown fox jumps over the lazy dog";
   _bstr_t bstrStr = stdStr.c_str();
   *str = bstrStr.copy();


void FreeString(BSTR str)

   SysFreeString(str);

PassStringOut() 和 FreeString() 中“str”指针的值不同,。我应该通过引用 FreeString() 来传递“str”吗?如果是这样,我应该在 C# 和 C++ 中使用什么语法?

【问题讨论】:

【参考方案1】:

封送层将在托管内存中分配字符串的副本。该副本将由垃圾收集器释放。您不必在 C# 中使用 SysFreeStringString,事实上,正如您所发现的那样,尝试这样做是破坏堆的好方法。

我是否应该认为将在字符串上执行 2 个副本? *str = bstrStr.copy(); 然后由编组层?

让我更详细地描述一下这里发生了什么。

您的Main 方法调用非托管代码,传递String 类型的局部变量的托管地址。封送层创建自己的大小合适的存储来保存BSTR,并将对该存储的引用传递给您的非托管代码。

非托管代码分配一个string 对象,该对象引用与文字关联的存储空间,然后分配一个BSTR 并将原始字符串的第一个副本放入堆分配的BSTR 中。然后,它会制作该BSTR 的第二个副本,并使用对该存储的引用填充 out 参数。 bstrStr 对象超出范围,其析构函数释放原始的BSTR

然后,编组层生成适当大小的托管字符串,并第三次复制该字符串。然后它释放传递给它的BSTR。控制权返回到您的 C# 代码,该代码现在有一个托管字符串。

该字符串被传递给FreeString。封送层分配一个BSTR 并第四次将字符串复制到BSTR 中,然后将其传递给您的非托管代码。然后它释放一个它不拥有的BSTR 并返回。封送层释放了它分配的BSTR,从而破坏了堆。

托管堆保持未损坏;托管字符串将在垃圾收集器选择时由垃圾收集器释放。

我应该通过引用 FreeString() 来传递“str”吗?

没有。相反,您应该停止编写互操作代码,直到您彻底深入了解编组的各个方面是如何工作的。

即使专家也很难在托管代码和非托管代码之间编组数据。我的建议是,您应该退后一步,并获得专家的服务,如果您需要,他们可以教您如何安全、正确地编写互操作代码。

【讨论】:

那么我应该认为将在字符串上执行 2 个副本吗? “*str = bstrStr.copy();”然后通过编组层? @Stephenosella:原始字符串将复制四个副本;其中三个副本位于BSTRs 中,一个位于托管String 中。最后一个副本被双重释放,破坏了堆。有关详细信息,请参阅我的答案。【参考方案2】:

这并不像您认为的那样有效。 pinvoke marshaller 已经自动释放了 BSTR。它发生在您调用 PassStringOut() 时,编组器将其转换为 System.String 并释放 BSTR。这是在本机代码和托管代码之间传递 BSTR 的正常且必要的协议。

FreeString() 中的问题是 new BSTR 由 pinvoke 编组器分配。它发布了两次。首先是您的本机代码,然后是 pinvoke marshaller。当您运行带有调试器的代码时使用的调试堆中的 Kaboom。

你帮的太多了,不要调用 FreeString()。


您可以让 pinvoke marshaller 为您处理 ANSI 字符串,这实际上是默认行为,因为它们在遗留 C 代码中非常常见。您的 C++ 函数可能如下所示:

extern "C" __declspec(dllexport) 
void __stdcall PassStringOut(char* buffer, size_t bufferLen)

   const std::string stdStr = "The quick brown fox jumps over the lazy dog";
   strcpy_s(buffer, bufferLen, stdStr.c_str());

使用匹配的 C# 代码:

class Program 
    static void Main(string[] args) 
        var buffer = new StringBuilder(666);
        PassStringOut(buffer, buffer.Capacity);
        Console.WriteLine(buffer.ToString());
        Console.ReadLine();
    
    [DllImport("Example.dll")]
    private static extern bool PassStringOut(StringBuilder buffer, int capacity);

然而,必须猜测缓冲区的正确大小是一个令人毛骨悚然的细节。

【讨论】:

好的。我明白。最终,我需要从 C++ 中的 std::string 到 C# 中的 System.String 。我不知道所需的 std::string 先验的大小。我之前已经发布过这个。使用 BSTR 作为媒介似乎是最优雅的,尽管它需要构建一个 _bstr_t ,由于开销,我并不特别喜欢它。对于我要使用的东西,最好的方法是什么? BSTR 没什么问题,pinvoke marshaller 非常喜欢它。 _bstr_t 也没有错,它至少可以帮助您将 std::string 转换为 Unicode。如果你不想使用它,那么你需要 MultiByteToWideChar() 去 Unicode 和 SysAllocString() 来获取 BSTR。 _bstr_t 做同样的事情。由于转换,这里总是有开销, std::string 只是上个世纪的字符串类型。如果你想去掉 BSTR 中间人然后传递一个你立即传递给 MultiByteToWideChar() 的初始化 StringBuilder,你的函数必须采用 wchar_t*。 实际上,我连接的后端代码(在 C++ 中)是严格的 ANSI。因此,我将从后端代码(我模拟的)获得的 std::string 将是 ANSI。因此,除了字符串在 C# 中的表示方式之外,实际上并没有要求我将字符串转换为 Unicode。鉴于此,使用 BSTR 和 _bstr_t 仍然是最好的方法吗? 我发布了一个替代方案。 您的替代方案不适用于我的用例,因为我事先不知道所需的缓冲区大小。如果我使用“UnmanagedType.AnsiBStr”作为“MarshalAs”参数,函数原型在 C++ 中会是什么样子?它也会是“out BSTR* str”吗?如果 C# 需要一个 ANSI 字符串,我不应该调用 MultiByteToWideChar() 或使用 _bstr_t,对吗?我可以将 *str 分配给“char *”字符串缓冲区,例如 std::string 。 c_str() ?这里有这个主题的任何好的文档吗?给定一个 MarshalAs 类型对应的 C++ 参数是什么?

以上是关于调用 SysFreeString() 时出现堆损坏错误的主要内容,如果未能解决你的问题,请参考以下文章

安装 phpmyadmin 时出现损坏的包错误

尝试使用特定用户通过 ssh 登录时出现“写入失败:管道损坏”

对图像进行分类时出现“用户警告:EXIF 数据可能损坏”

在 Windows 10 上安装 Visual Studio 2015 时出现“安装程序包丢失或损坏”错误

在 Unity 上安装 Firebase 时出现错误“正在卸载损坏的程序集”

尝试通过运行 Tkinter 的发送进程在进程之间通过管道发送任何内容时出现管道损坏错误