调用 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# 中使用 SysFreeString
和 String
,事实上,正如您所发现的那样,尝试这样做是破坏堆的好方法。
我是否应该认为将在字符串上执行 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:原始字符串将复制四个副本;其中三个副本位于BSTR
s 中,一个位于托管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() 时出现堆损坏错误的主要内容,如果未能解决你的问题,请参考以下文章
尝试使用特定用户通过 ssh 登录时出现“写入失败:管道损坏”
在 Windows 10 上安装 Visual Studio 2015 时出现“安装程序包丢失或损坏”错误