.NET故障排除:“致命执行引擎错误”

45

概述:

我经常在一个应用程序上遇到.NET致命执行引擎错误,但无法调试。弹出的对话框只提供关闭程序或向Microsoft发送有关错误信息的选项。我尝试查看更详细的信息,但不知道如何利用它。

错误:

错误可在事件查看器中应用程序下看到,如下所示:

.NET Runtime version 2.0.50727.3607 - Fatal Execution Engine Error (7A09795E) (80131506)

运行该程序的计算机是Windows XP Professional SP3。(Intel Core2Quad Q6600 2.4GHz,内存为2.0 GB)。其他缺少多线程下载(参见下文)的基于.NET的项目似乎都可以很好地运行。

应用程序:

该应用程序使用VS2008编写的C#/.NET 3.5,通过设置项目进行安装。

该应用程序是多线程的,并使用System.Net.HttpWebRequest及其方法从多个Web服务器下载数据。 我已确定.NET错误与线程或HttpWebRequest有关,但由于此特定错误似乎无法调试,因此我无法更进一步。

我已尝试在许多级别上处理错误,包括在Program.cs中使用以下内容:

// handle UI thread exceptions
Application.ThreadException += Application_ThreadException;

// handle non-UI thread exceptions
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

// force all windows forms errors to go through our handler
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

更多笔记和我尝试过的内容...

  • 在目标计算机上安装了Visual Studio 2008并尝试以调试模式运行,但错误仍然发生,没有提示它出现在源代码的哪个位置。
  • 在已安装版本(发布版)中运行程序时,错误更加频繁地发生,通常在启动应用程序后的几分钟内。在VS2008内部的调试模式下运行程序时,可能会运行数小时或数天,然后生成错误。
  • 重新安装.NET 3.5并确保所有更新都已应用。
  • 因挫败感而打碎随意的小物件。
  • 重写处理线程和下载的代码部分,以尝试捕获和记录异常,尽管记录似乎加剧了问题(并且从未提供任何数据)。

问题:

我可以采取哪些步骤来解决或调试这种类型的错误?内存转储之类的东西似乎是下一步,但我不熟练于解释它们。也许在代码中有更多可以做的事情来尝试捕获错误...如果“致命执行引擎错误”更具说明性,那就太好了,但互联网搜索只告诉我它是许多与.NET相关的项目的常见错误。


你尝试在.NET 4.0下运行了吗?虽然这不是一个解决方案,但它是另一个数据点。 - Eamon Nerbonne
@Eamon:谢谢,这是一个好主意,我会尝试的。 - JYelton
1
你能把问题缩小到特定的组件/类吗?如果可以,你可以通过将实际位置的输出添加到调试日志中来获得至少有问题的代码。 - Bobby
如果出现内存损坏问题,你可以尝试从微软官方网站下载pageheap.exe并查看是否有任何问题。 - zildjohn01
你尝试过通过微软的EnterpriseLibrary日志记录类在可能出现问题的区域增加更多的日志吗?我以前也遇到过类似的问题,但在每个可能失败的点之前和之后添加日志最终帮助我找到了解决方案(这与我原本想象的完全不同,也与异常看起来的情况不同)。 - Jason M
显示剩余2条评论
5个回答

46

嗯,你遇到了一个大问题。当CLR检测到垃圾回收堆的完整性受损时,就会引发该异常。堆损坏是任何用C或C++等非托管语言编写代码的程序员的噩梦。

这些语言使得破坏堆很容易,只需要在分配在堆上的数组的结尾之后写入或在释放内存后继续使用内存,或者使用指针的错误值等等。这些都是托管代码创造出来解决的问题。

但从你的问题来看,你正在使用托管代码。嗯,大部分情况下,你的代码是托管的。但是你执行了许多非托管代码。实际使HttpWebRequest工作的所有低级代码都是非托管的。而CLR也是这样,它是用C++编写的,因此在技术上也有可能损坏堆。但经过4000多次修订和数百万个程序的使用,它仍然存在堆污染的几率非常小。

对于所有其他想要使用HttpWebRequest的非托管代码而言,情况并非如此。你不知道的代码,因为你没有编写它,并且Microsoft也没有记录它。你的防火墙。你的病毒扫描器。你公司的互联网使用监控器。上帝知道谁的“下载加速器”。

隔离问题,首先假设它不是你的代码也不是Microsoft的代码引起的问题。首先假设它是环境引起的问题,然后清除这些垃圾软件。

关于一个史诗级的环境故事,可以阅读此线程


这是一份对“大局”进行了深入分析,并提供了导致此错误发生的原因的见解。不过,如果能够提供一些关于如何复制该错误、解决它或完全避免它的指南或方法,那就更好了。 - JYelton
2
抱歉,没有任何内容。异常是在损坏发生很久以后才被引发的。从原因可能是环境的前提出发,改变环境。 - Hans Passant
例如,我正在考虑使用第三方库,比如 /n 软件的 IPWorks,但我不知道它是否使用相同的非托管代码块或有效地避免它。(http://www.nsoftware.com/ipworks/v8/default.aspx) - JYelton
考虑到它的年龄,很可能包含带有托管包装器的非托管代码。我严重怀疑它能否解决你问题的根本原因。如果你能够获得可靠的崩溃复现,最好花钱购买微软支持服务。 - Hans Passant

9
由于之前的建议比较通用,我认为发布我自己的解决这个异常的具体代码示例和我实现的背景更有用。首先是TL;DR版本:我使用了一个内部C++(未托管)编写的dll。我从我的.NET可执行文件中传入了一个特定大小的数组。未托管代码尝试写入由托管代码未分配的数组位置。这导致内存损坏,后来被设置为垃圾回收。当垃圾收集器准备收集内存时,它首先检查内存(和边界)的状态。当它发现损坏时,就会出现问题。
现在是详细版:
我正在使用内部开发的未托管C++ dll。我的GUI开发是使用C# .Net 4.0完成的。我调用了各种未托管方法。该dll有效地充当我的数据源。以下是来自dll的示例extern定义:
    [DllImport(@"C:\Program Files\MyCompany\dataSource.dll",
        EntryPoint = "get_sel_list",
        CallingConvention = CallingConvention.Winapi)]
    private static extern int ExternGetSelectionList(
        uint parameterNumber,
        uint[] list,
        uint[] limits,
        ref int size);

我将这些方法封装在我的接口中,以便在整个项目中使用:

    /// <summary>
    /// Get the data for a ComboBox (Drop down selection).
    /// </summary>
    /// <param name="parameterNumber"> The parameter number</param>
    /// <param name="messageList"> Message number </param>
    /// <param name="valueLimits"> The limits </param>
    /// <param name="size"> The maximum size of the memory buffer to 
    /// allocate for the data </param>
    /// <returns> 0 - If successful, something else otherwise. </returns>
    public int GetSelectionList(uint parameterNumber, 
           ref uint[] messageList, 
           ref uint[] valueLimits, 
           int size)
    {
        int returnValue = -1;
        returnValue = ExternGetSelectionList(parameterNumber,
                                         messageList, 
                                         valueLimits, 
                                         ref size);
        return returnValue;
    }

这个方法的例子调用如下:
            uint[] messageList = new uint[3];
            uint[] valueLimits = new uint[3];
            int dataReferenceParameter = 1;
            
            // BUFFERSIZE = 255.
            MainNavigationWindow.MainNavigationProperty.DataSourceWrapper.GetSelectionList(
                          dataReferenceParameter, 
                          ref messageList, 
                          ref valueLimits, 
                          BUFFERSIZE);

在GUI中,用户可以浏览包含各种图形和用户输入的不同页面。以前的方法使我能够获取数据以填充ComboBoxes。以下是在出现异常之前我的导航设置和调用示例:
在我的主窗口中,我设置了一个属性:
    /// <summary>
    /// Gets or sets the User interface page
    /// </summary>
    internal UserInterfacePage UserInterfacePageProperty
    {
        get
        {
            if (this.userInterfacePage == null)
            {
                this.userInterfacePage = new UserInterfacePage();
            }

            return this.userInterfacePage;
        }

        set { this.userInterfacePage = value; }
    }

然后,当需要时,我导航到该页面:

MainNavigationWindow.MainNavigationProperty.Navigate(
        MainNavigation.MainNavigationProperty.UserInterfacePageProperty);

虽然一切都运行良好,但我确实遇到了一些严重的问题。当使用对象 (NavigationService.Navigate Method (Object)) 进行导航时,默认情况下 IsKeepAlive 属性的设置为 true。但问题比这更阴险。即使你在该页面的构造函数中将 IsKeepAlive 值明确设置为 false,垃圾回收器仍会像它是 true 一样不管它。对于我的许多页面来说,这并不是什么大问题。它们的内存占用很小,没有太多的操作。但其中许多页面上有一些大型高度详细的图形,用于说明目的。不久之后,我们设备的操作员正常使用此界面就会导致巨大的内存分配,这些内存永远不会清除,最终会堵塞机器上的所有进程。在最初的开发热潮从海啸到更像是潮汐波之后,我终于决定彻底解决内存泄漏问题。我不会详细介绍我实施的所有技巧来清理内存 (WeakReference 图像,解除 Unload() 上的事件处理程序,使用实现 IWeakEventListener 接口的自定义计时器等...)。我所做的关键更改是使用 Uri 而不是对象进行页面导航 (NavigationService.Navigate Method (Uri))。使用此类型的导航有两个重要的区别:

  1. IsKeepAlive 默认设置为 false
  2. 垃圾回收器现在会尝试清理导航对象,就好像 IsKeepAlive 被设置为 false 一样。

所以现在我的导航看起来像:

MainNavigation.MainNavigationProperty.Navigate(
    new Uri("/Pages/UserInterfacePage.xaml", UriKind.Relative));

还有一点需要注意:这不仅影响垃圾收集器对对象的清理方式,而且影响它们在内存中的初始分配,我很快就会发现。

一切似乎都很顺利。当我浏览图形密集页面时,我的内存会迅速清理到接近最初状态,直到我遇到了具有特定调用数据源dll填充某些comboBoxes的特定页面。然后我就遇到了这个讨厌的FatalEngineExecutionError。经过数日的研究和找到模糊的建议或高度特定的解决方案(这些解决方案并不适用于我),以及释放我个人编程武器库中的所有调试工具,我最终决定唯一确保解决这个问题的方法是采取极端措施,逐个复制此特定页面的每个元素、方法和行,直到我最终找到引发此异常的代码。这就像我所暗示的那样单调而痛苦,但我最终找到了问题所在。

实际上是未托管的dll分配内存以写入数据到我提供的数组中的方式出了问题。该特定方法实际上会查看参数编号,并根据期望写入我发送的数组的数据量来分配特定大小的数组。导致代码崩溃的部分:

            uint[] messageList = new uint[2];
            uint[] valueLimits = new uint[2];
            int dataReferenceParameter = 1;
            
            // BUFFERSIZE = 255.
            MainNavigationWindow.MainNavigationProperty.DataSourceWrapper.GetSelectionList(
                           dataReferenceParameter, 
                           ref messageList, 
                           ref valueLimits, 
                           BUFFERSIZE);

这段代码看起来和上面的示例完全一样,但有一个微小的区别。我分配的数组大小是2而不是3。我这样做是因为我知道这个特定的ComboBox只有两个选择项,而页面上的其他ComboBox都有三个选择项。然而,非托管代码并没有像我看到的那样看待它。它得到了我交给它的数组,并试图将一个大小为 3 的数组写入我的大小为 2 的分配内存中,就这样。 *砰!* *崩溃!* 我将分配大小更改为3,错误消失了。
现在,这段特定的代码已经运行了至少一年,没有出现这个错误。但是通过Uri而不是Object导航到这个页面引起了崩溃。这意味着由于我使用的导航方法不同,初始对象必须以不同的方式分配。因为使用我的旧导航方法,内存只是堆积在一起,永远不会被清理,所以似乎即使在一个或两个小位置上有点损坏也无关紧要。一旦垃圾收集器必须对该内存进行实际操作(如清理它),它就会检测到内存损坏并抛出异常。具有讽刺意味的是,我的主要内存泄漏掩盖了致命的内存错误! 显然,我们将审查此接口,以避免未来发生这种简单假设导致的崩溃。希望这可以帮助一些其他人找出他们自己代码中的问题。

4
感谢您的努力和详细调查。这对其他人可能非常有用。 - JYelton

3
这是一篇关于如何解决类似问题的演示文稿:Hardcore production debugging in .NET by Ingo Rammer,可能会对您有所帮助。
我有一些 C++/CLI 编程经验,堆损坏通常不会导致此错误;通常堆损坏会引起数据损坏和后续的普通异常或内存保护错误 - 这可能没有任何意义。
除了尝试使用.NET 4.0(它以不同的方式加载非托管代码),您还应该比较 CLR 的 x86 和 x64 版本 - 如果可能的话 - x64 版本具有更大的地址空间,因此具有完全不同的 malloc (+碎片) 行为,所以您可能会运气好,在那里遇到不同的(更易调试的)错误(如果出现)。
此外,您是否在调试器中开启了非托管代码调试(一个项目选项)? 在使用 Visual Studio 运行时,是否打开了托管调试助手?

我还没有打开非托管代码调试,这是我会尝试的,感谢您的建议。关于托管调试助手,我直到现在才知道它们的存在,所以我会立即进行研究。目前有一些MDA被选中,其他的则被清除(在抛出异常时)。我需要研究哪些要启用以及如何从中获取信息。 - JYelton
1
我想授予你问题赏金,因为你提供了许多好的尝试选项。感谢你的参与。 - JYelton

2
在我的情况下,我已经使用 AppDomain.CurrentDomain.FirstChanceException 安装了一个异常处理程序。该处理程序记录一些异常,而且几年来一切都很正常(实际上,这个调试代码不应该留在生产环境中)。
但是,在配置错误之后,记录器开始失败,处理程序本身也会抛出异常,这显然导致了一个看似无处不在的FatalExecutionEngineError
因此,任何遇到此错误的人都可以花费几秒钟搜索代码中任何地方的FirstChanceException出现次数,可能会节省几个小时的头疼 :)

-2

如果你正在使用 thread.sleep(),那可能就是原因。非托管代码只能从 kernell.32 sleep() 函数中休眠。


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