我只花了最后一小时解决C#中非托管内存的一个奇怪问题。
首先,一点上下文。我有一个C#DLL导出一些本地方法(通过this awesome project template),然后由Delphi应用程序调用。其中一个C#方法必须将结构传递回Delphi,然后将其转换为记录。我已经可以告诉你感到恶心,所以我不再详细介绍了。是的,这很难看,但替代品是COM ......不,谢谢。
以下是违规代码的简化:
IntPtr AllocBlock(int bufferSize)
{
IntPtr ptrToMem = Marshal.AllocHGlobal(bufferSize);
// zero memory
for(int i = 0; i < bufferSize; i++)
Marshal.WriteInt16(ptrToMem, i, 0);
return ptrToMem;
}
实际上还有其他一些东西在这里发生,与本地资源跟踪有关,但基本上就是这样。如果你已经发现了这个错误,那就做得很好。
基本上,问题在于我使用WriteInt16
而不是WriteByte
,因为智能感知辅助拼写错误,导致最后一次迭代在缓冲区末尾写入一个字节。我想是容易犯的错误。
然而,在调试器中使这种痛苦变得如此痛苦的原因是在调试器中以静默方式失败,并且应用程序的其余部分继续工作。内存已经分配,除了最后一个字节以外的所有内容都为零,所以它运行正常。在调试器外部启动时,它会导致应用程序因访问冲突而崩溃。经典的Heisenbug情况 - 当您尝试分析它时,该错误消失了。请注意,此崩溃不是托管异常,而是真正的CPU级别访问冲突。
现在,这让我感到困惑有两个原因:
连接任何调试器时都没有抛出异常 - 我尝试了Visual Studio,CodeGear Delphi 2009和OllyDbg。附上后,该程序运作良好。没有附加时,程序崩溃了。所有尝试都使用了完全相同的可执行文件。我的理解是,调试不应该改变应用程序的行为,但它显然是这样。
通常我希望此操作在我的托管代码中导致AccessViolationException
,但在ntdll.dll
中因内存访问冲突而死亡。
现在,公平地说,我的案例可能是C#历史上最模糊(也可能是误导)的角落案例之一,但我对于如何附加任何调试器来防止崩溃感到迷茫。我特别感到惊讶的是它在OllyDbg下运行,它不会像Visual Studio那样干扰任何过程。
那么,到底发生了什么?为什么在调试期间吞下(或不引发)异常,而不是在调试器之外?当我试图在分配的内存块之外调用Marshal.WriteInt16
时,为什么没有抛出托管访问冲突异常,正如文档所说的那样?
答案 0 :(得分:2)
您只需将一些堆内存设置为零。这不会失败,因为没有检查你从哪里得到这个地址。这会在以后的堆管理(在ntdll中)中产生问题。这个page显示了为什么以后只能检测到溢出的原因。
出于类似的原因,您没有使用附加的调试器失败。当Windows检测到附加了调试器时,将使用不同的堆管理器。调试堆管理器添加了一个用于检测溢出的后缀,它不会禁用堆管理器,但会在释放块时显示损坏。有关详细信息,请参阅上面的页面。
我不知道堆管理器是如何在操作系统级别上启动的,但我在stackoverflow上找到了一个相关的答案: Visual C++: Difference between Start with/without debugging in Release mode
据我了解堆管理器切换,如果从桌面/控制台以发布模式启动进程并稍后附加调试器,则调试器应在使用标准堆管理器时停止堆损坏。这可以用来测试我的假设。