Dépannage de .NET «Erreur fatale du moteur d’exécution»

Résumé:

Je reçois périodiquement une erreur de moteur d’exécution fatale .NET sur une application que je n’arrive pas à déboguer. La boîte de dialog qui s’affiche ne propose que la fermeture du programme ou l’envoi d’informations sur l’erreur à Microsoft. J’ai essayé de regarder les informations plus détaillées mais je ne sais pas comment les utiliser.

Erreur:

L’erreur est visible dans l’Observateur d’événements sous Applications et se présente comme suit:

Version du runtime .NET 2.0.50727.3607 – Erreur du moteur d’exécution irrécupérable (7A09795E) (80131506)

L’ordinateur qui l’exécute est Windows XP Professionnel SP 3. (Intel Core2Quad Q6600 2,4 GHz avec 2,0 Go de RAM).

Application:

L’application est écrite en C # /. NET 3.5 à l’aide de VS2008 et installée via un projet d’installation.

L’application est multi-thread et télécharge des données à partir de plusieurs serveurs Web en utilisant System.Net.HttpWebRequest et ses méthodes. J’ai déterminé que l’erreur .NET avait quelque chose à voir avec le threading ou HttpWebRequest, mais je n’ai pas été en mesure de me rapprocher car cette erreur particulière semble impossible à déboguer.

J’ai essayé de gérer les erreurs à plusieurs niveaux, notamment dans 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); 

Plus de notes et ce que j’ai essayé …

  • Installation de Visual Studio 2008 sur la machine cible et exécution de l’exécution en mode débogage, mais l’erreur persiste, sans indication de l’emplacement du code source.
  • Lors de l’exécution du programme à partir de sa version installée (version), l’erreur est plus fréquente, généralement quelques minutes après le lancement de l’application. Lors de l’exécution du programme en mode débogage à l’intérieur de VS2008, il peut être exécuté pendant des heures ou des jours avant de générer l’erreur.
  • Réinstallation de .NET 3.5 et vérification de l’application de toutes les mises à jour.
  • Brisé des objects cubiques aléatoires dans la frustration.
  • Les parties du code réécrites qui traitent des threads et des téléchargements lors des tentatives de capture et de journalisation des exceptions, bien que la journalisation semble aggraver le problème (et n’a jamais fourni de données).

Question:

Quelles mesures puis-je prendre pour dépanner ou déboguer ce type d’erreur? Les vidages de mémoire et autres semblent être la prochaine étape, mais je ne suis pas expérimenté dans leur interprétation. Il y a peut-être quelque chose de plus que je peux faire dans le code pour essayer de détecter les erreurs … Ce serait bien que “Fatal Execution Engine Error” soit plus informatif, mais les recherches sur Internet ne me disaient que Éléments liés à .NET.

    Eh bien, vous avez un gros problème. Cette exception est soulevée par le CLR lorsqu’il détecte que l’intégrité du tas récupéré est compromise. La corruption du tas, le fléau de tout programmeur qui a jamais écrit du code dans un langage non géré comme C ou C ++.

    Ces langages facilitent la corruption du tas, il suffit d’écrire après la fin d’un tableau alloué sur le tas. Ou en utilisant la mémoire après sa libération. Ou avoir une mauvaise valeur pour un pointeur. Le type de bugz inventé pour résoudre ce code géré.

    Mais vous utilisez du code managé, à en juger par votre question. Eh bien, la plupart du temps, votre code est géré. Mais vous exécutez beaucoup de code non géré. Tout le code de bas niveau qui fait réellement un travail HttpWebRequest n’est pas géré. De même que le CLR, il a été écrit en C ++, ce qui risque de corrompre le tas. Mais après plus de quatre mille révisions, et des millions de programmes l’utilisant, les probabilités qu’il souffre encore de compresses de tas sont très faibles.

    Le même n’est pas vrai pour tous les autres codes non gérés qui veulent un morceau de HttpWebRequest. Le code que vous ne connaissez pas parce que vous ne l’avez pas écrit et que Microsoft n’a pas documenté. Votre pare-feu Votre scanner de virus. Moniteur d’utilisation d’Internet de votre entreprise. Lord sait qui “accélérateur de téléchargement”.

    Isolez le problème, supposez que ce n’est ni votre code ni le code de Microsoft qui pose problème. Supposons que ce soit d’abord environnemental et que vous vous débarrassiez des articles de cuisine.

    Pour une histoire FEEE environnementale épique, lisez ce fil de discussion .

    Comme les suggestions précédentes sont de nature assez générique, j’ai pensé qu’il pourrait être utile de poster ma propre bataille contre cette exception avec des exemples de code spécifiques, les modifications d’arrière-plan que j’ai implémentées pour provoquer cette exception et comment je l’ai résolue.

    J’utilise une DLL non gérée développée en interne, écrite en C ++. Mon propre développement graphique est en C # .Net 4.0. J’appelle une variété de ces méthodes non gérées. Cette DLL agit effectivement comme ma source de données. Un exemple de définition externe de la DLL:

      [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); 

    J’emballe alors les méthodes dans ma propre interface pour les utiliser tout au long de mon projet:

      ///  /// Get the data for a ComboBox (Drop down selection). ///  ///  The parameter number ///  Message number  ///  The limits  ///  The maximum size of the memory buffer to /// allocate for the data  ///  0 - If successful, something else otherwise.  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; } 

    Un exemple d’appel de cette méthode:

      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); 

    Dans l’interface graphique, on navigue à travers différentes pages contenant une variété de graphiques et d’entrées utilisateur. La méthode précédente m’a permis d’obtenir les données pour remplir ComboBoxes . Un exemple de configuration de navigation et d’appel à la fois avant cette exception:

    Dans ma fenêtre hôte, j’ai configuré une propriété:

      ///  /// Gets or sets the User interface page ///  internal UserInterfacePage UserInterfacePageProperty { get { if (this.userInterfacePage == null) { this.userInterfacePage = new UserInterfacePage(); } return this.userInterfacePage; } set { this.userInterfacePage = value; } } 

    Ensuite, si nécessaire, je navigue vers la page:

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

    Tout fonctionnait bien, même si j’avais de sérieux problèmes de fluage. Lors de la navigation à l’aide de l’object ( Méthode NavigationService.Navigate (Object) ), le paramètre par défaut de la propriété IsKeepAlive est true . Mais le problème est plus grave que cela. Même si vous définissez IsKeepAlive valeur IsKeepAlive dans le constructeur de cette page sur false , le ramasse-miettes le laisse comme s’il était true . Maintenant, pour beaucoup de mes pages, ce n’était pas grave. Ils avaient de petites empreintes de mémoire avec pas tout ce qui se passait. Mais de nombreuses autres pages contenaient de grands graphiques très détaillés à des fins d’illustration. Il n’était pas trop long avant que l’utilisation normale de cette interface par les opérateurs de notre équipement n’entraîne des allocations de mémoire considérables qui ne se sont jamais effacées et ont finalement obstrué tous les processus de la machine. Après que le développement initial se soit apaisé d’un tsunami à un raz-de-marée, j’ai finalement décidé de faire face aux memory leaks une fois pour toutes. Je n’entrerai pas dans les détails de toutes les astuces que j’ai implémentées pour nettoyer la mémoire ( WeakReference s pour les images, décrochage des gestionnaires d’événement sur Unload (), utilisation d’une timer personnalisée implémentant l’interface IWeakEventListener , etc …). Le changement clé que j’ai apporté consistait à naviguer vers les pages à l’aide de l’URI au lieu de l’object ( Méthode NavigationService.Navigate (Uri) ). Il existe deux différences importantes lors de l’utilisation de ce type de navigation:

    1. IsKeepAlive est défini sur false par défaut.
    2. Le garbage collector va maintenant essayer de nettoyer l’object de navigation comme si IsKeepAlive était défini sur false .

    Alors maintenant, ma navigation ressemble à:

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

    Autre chose à noter ici: cela n’affecte pas seulement la façon dont les objects sont nettoyés par le ramasse-miettes, cela affecte la façon dont ils sont initialement alloués en mémoire , comme je le saurais bientôt.

    Tout semblait fonctionner très bien. Ma mémoire devenait rapidement proche de mon état initial au fur et à mesure que je parcourais les pages intensives en graphismes, jusqu’à ce que je reçoive cet appel particulier à la DLL dataSource pour remplir certaines comboBox. Puis j’ai eu cette méchante FatalEngineExecutionError . Après des jours de recherche et de recherche de suggestions vagues, ou de solutions très spécifiques qui ne s’appliquaient pas à moi, ainsi que le déchaînement de toutes les armes de débogage dans mon arsenal de programmation personnel, j’ai finalement décidé que down était la mesure extrême de reconstruire une copie exacte de cette page particulière, élément par élément, méthode par méthode, ligne par ligne, jusqu’à ce que je tombe finalement sur le code qui jette cette exception. C’était aussi pénible et douloureux que je le laisse entendre, mais j’ai finalement réussi à le retrouver.

    Cela s’est avéré être dans la façon dont la DLL non gérée allouait de la mémoire pour écrire des données dans les tableaux que je lui envoyais pour les remplir. Cette méthode particulière examinerait en fait le numéro du paramètre et, à partir de cette information, allouerait un tableau d’une taille particulière en fonction de la quantité de données qu’elle s’attendait à écrire dans le tableau que j’ai envoyé. Le code qui a planté:

      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); 

    Ce code peut sembler identique à l’exemple ci-dessus, mais il a une petite différence. La taille du tableau que j’alloue est 2 pas 3 . Je l’ai fait parce que je savais que cette ComboBox particulière n’aurait que deux éléments de sélection par opposition aux autres ComboBox sur la page qui avaient tous trois éléments de sélection. Cependant, le code non géré n’a pas vu les choses comme je l’ai vu. Il a obtenu le tableau que j’ai remis et a essayé d’écrire un tableau de taille [3] dans mon allocation de taille [2], et c’était tout. * bang! * * crash! * J’ai changé la taille de l’allocation à 3 et l’erreur a disparu.

    Maintenant, ce code particulier fonctionnait déjà sans cette erreur pour au moins un an. Mais le simple fait de naviguer sur cette page via un Uri par opposition à un Object provoqué l’apparition du crash. Cela implique que l’object initial doit être alloué différemment en raison de la méthode de navigation utilisée. Puisque, avec mon ancienne méthode de navigation, la mémoire était simplement mise en place et laissée à la charge de l’éternité, cela n’avait pas d’importance si elle était un peu corrompue dans un ou deux petits endroits. Une fois que le ramasse-miettes a dû faire quelque chose avec cette mémoire (comme le nettoyer), il a détecté la corruption de la mémoire et a lancé l’exception. Ironiquement, ma fuite de mémoire majeure couvrait une erreur de mémoire fatale!

    Évidemment, nous allons revoir cette interface pour éviter de telles hypothèses simples causant de tels accidents dans le futur. J’espère que cela aidera à guider d’autres personnes pour découvrir ce qui se passe dans leur propre code.

    Voici une présentation qui pourrait être un bon tutoriel sur les points suivants: Débogage de la production Hardcore dans .NET par Ingo Rammer .

    Je fais un peu de codage C ++ / CLI, et la corruption de tas ne provoque généralement pas cette erreur; En général, la corruption du tas provoque une corruption des données et une exception normale ultérieure ou une erreur de protection de la mémoire – ce qui ne signifie probablement rien.

    En plus d’essayer .net 4.0 (qui charge le code non géré différemment), vous devez comparer les éditions x86 et x64 du CLR – si possible – la version x64 a un plus grand espace d’adressage et donc un comportement malloc (+ fragmentation) complètement différent. peut avoir de la chance et avoir une erreur différente (plus débogable) là-bas (si cela se produit).

    En outre, avez-vous activé le débogage de code non géré dans le débogueur (une option de projet), lorsque vous exécutez Visual Studio? Et avez-vous des assistants de débogage gérés?

    Dans mon cas, j’avais installé un gestionnaire d’exceptions avec AppDomain.CurrentDomain.FirstChanceException . Ce gestionnaire enregistrait des exceptions et tout allait bien pendant quelques années (en fait, ce code de débogage n’aurait pas dû restr en production).

    Mais suite à une erreur de configuration, le logger a commencé à échouer et le gestionnaire lui-même a été lancé, ce qui a apparemment provoqué une FatalExecutionEngineError .

    Donc, toute personne rencontrant cette erreur pourrait passer quelques secondes à rechercher des occurrences de FirstChanceException dans le code et peut-être gagner quelques heures de gratte 🙂

    Si vous utilisez thread.sleep (), cela peut être la raison. Le code non géré peut uniquement être suspendu à partir de la fonction kernell.32 sleep ().