Comment déboguer les erreurs de corruption de tas?

165

Je débogue une application C ++ multithread (native) sous Visual Studio 2008. À des occasions apparemment aléatoires, j'obtiens une erreur "Windows a déclenché un point d'arrêt ..." avec une note indiquant que cela pourrait être dû à une corruption dans le tas. Ces erreurs ne planteront pas toujours l'application immédiatement, même si elles risquent de se bloquer peu de temps après.

Le gros problème avec ces erreurs est qu'elles n'apparaissent qu'après la corruption, ce qui les rend très difficiles à suivre et à déboguer, en particulier sur une application multithread.

  • Quel genre de choses peuvent causer ces erreurs?

  • Comment les déboguer?

Astuces, outils, méthodes, éclairages ... sont les bienvenus.

Peter Mortensen
la source

Réponses:

128

Le vérificateur d'application combiné aux outils de débogage pour Windows est une configuration incroyable. Vous pouvez obtenir les deux dans le cadre du Kit de pilotes Windows ou du SDK Windows plus léger . (Découvrez Application Verifier lors de la recherche d'une question précédente sur un problème de corruption de tas .) J'ai également utilisé BoundsChecker et Insure ++ (mentionnés dans d'autres réponses) dans le passé, même si j'ai été surpris de la quantité de fonctionnalités d'Application Verifier.

Clôture électrique (alias "efence"), dmalloc , valgrind , etc. méritent tous d'être mentionnés, mais la plupart d'entre eux sont beaucoup plus faciles à utiliser sous * nix que sous Windows. Valgrind est ridiculement flexible: j'ai débogué de gros logiciels serveur avec de nombreux problèmes de tas en l'utilisant.

Lorsque tout le reste échoue, vous pouvez fournir à votre propre opérateur global les surcharges new / delete et malloc / calloc / realloc - la manière de procéder varie un peu en fonction du compilateur et de la plate-forme - et ce sera un peu un investissement - mais cela peut porter ses fruits à long terme. La liste des fonctionnalités souhaitables devrait sembler familière à partir de dmalloc et electricfence, et du livre étonnamment excellent Writing Solid Code :

  • valeurs de sentinelle : laisser un peu plus d'espace avant et après chaque allocation, en respectant l'exigence d'alignement maximum; remplir avec des nombres magiques (aide à détecter les débordements et les sous-débordements de la mémoire tampon, et le pointeur «sauvage» occasionnel)
  • alloc fill : remplissez les nouvelles allocations avec une valeur magique non-0 - Visual C ++ le fera déjà pour vous dans les versions de débogage (aide à détecter l'utilisation des variables non initialisées)
  • remplissage libre : remplissez la mémoire libérée avec une valeur magique non-0, conçue pour déclencher un segfault s'il est déréférencé dans la plupart des cas (aide à attraper les pointeurs pendants)
  • gratuit retardé : ne retournez pas la mémoire libérée dans le tas pendant un certain temps, gardez-la libre remplie mais non disponible (aide à attraper plus de pointeurs pendants, capture à proximité des doubles-libres)
  • suivi : pouvoir enregistrer où une allocation a été faite peut parfois être utile

Notez que dans notre système homebrew local (pour une cible intégrée), nous gardons le suivi séparé de la plupart des autres éléments, car la surcharge d'exécution est beaucoup plus élevée.


Si vous êtes intéressé par plus de raisons de surcharger ces fonctions / opérateurs d'allocation, jetez un œil à ma réponse à "Une raison de surcharger l'opérateur global nouveau et supprimer?" ; Outre l'auto-promotion éhontée, elle répertorie d'autres techniques utiles pour suivre les erreurs de corruption de tas, ainsi que d'autres outils applicables.


Parce que je continue de trouver ma propre réponse ici lors de la recherche de valeurs alloc / free / fence que MS utilise, voici une autre réponse qui couvre les valeurs de remplissage Microsoft dbgheap .

plus maigre
la source
3
Une petite chose à noter à propos d'Application Verifier: vous devez enregistrer les symboles d'Application Verifier avant les symboles du serveur de symboles Microsoft dans votre chemin de recherche de symboles, si vous utilisez cela ... Il m'a fallu un peu de recherche pour comprendre pourquoi! Avrf n'était pas trouver les symboles dont il avait besoin.
maigre
Application Verifier m'a beaucoup aidé et, combiné à quelques suppositions, j'ai pu résoudre le problème! Merci beaucoup, et à tous les autres aussi, d'avoir soulevé des points utiles.
Application Verifier doit-il être utilisé avec WinDbg ou doit-il fonctionner avec le débogueur Visual Studio? J'ai essayé de l'utiliser, mais cela ne soulève aucune erreur ou ne fait apparemment rien lorsque je débogue dans VS2012.
Nathan Reed
@NathanReed: Je pense que cela fonctionne également avec VS - voir msdn.microsoft.com/en-us/library/ms220944(v=vs.90).aspx - bien que notez que ce lien est pour VS2008, je ne le suis pas sûr des versions ultérieures. La mémoire est un peu floue, mais je crois que lorsque j'ai eu le problème dans le lien "question précédente", j'ai juste lancé Application Verifier et enregistré les options, exécuté le programme, et quand il a planté, j'ai choisi VS pour déboguer. AV l'a fait planter / affirmer plus tôt. La commande! Avrf est spécifique à WinDbg pour autant que je sache, cependant. J'espère que d'autres pourront fournir plus d'informations!
maigre
Merci. En fait, j'ai résolu mon problème d'origine et il s'est avéré qu'il ne s'agissait pas d'une corruption de tas après tout, mais d'autre chose, ce qui explique probablement pourquoi App Verifier n'a rien trouvé. :)
Nathan Reed
35

Vous pouvez détecter de nombreux problèmes de corruption de tas en activant Page Heap pour votre application. Pour ce faire, vous devez utiliser gflags.exe qui fait partie des outils de débogage pour Windows

Exécutez Gflags.exe et dans les options de fichier image pour votre exécutable, cochez l'option "Activer le tas de pages".

Maintenant, redémarrez votre exe et attachez-vous à un débogueur. Avec le tas de pages activé, l'application entrera dans le débogueur chaque fois qu'une corruption de tas se produit.

Canopus
la source
oui mais une fois que je reçois cet appel de fonction dans mon vidage de la pile d'appels (après un crash de corruption de mémoire): wow64! Wow64NotifyDebugger, que puis-je faire? Je ne sais toujours pas ce qui ne va pas dans ma candidature
Guillaume07
Je viens d'essayer gflags pour déboguer la corruption de tas ici, petit outil TRÈS utile, fortement recommandé. Il s'est avéré que j'accédais à la mémoire libérée, qui, lorsqu'elle est instrumentée avec des gflags, s'introduit immédiatement dans le débogueur ... Pratique!
Dave F
Excellent outil! Je viens de trouver un bogue, que je chassais depuis des jours, parce que Windows ne dit pas l'adresse de la corruption, seulement que "quelque chose" ne va pas, ce qui n'est pas vraiment utile.
Devolus
Un peu tard à la fête, mais j'ai remarqué une augmentation significative de l'utilisation de la mémoire dans l'application que je débogue lorsque j'ai activé le tas de pages. Malheureusement, jusqu'à ce que l'application (32 bits) soit à court de mémoire avant que la détection de corruption du tas ne soit déclenchée. Des idées sur la façon de résoudre ce problème?
uceumern
13

Pour vraiment ralentir les choses et effectuer de nombreuses vérifications à l'exécution, essayez d'ajouter ce qui suit en haut de votre main()ou équivalent dans Microsoft Visual Studio C ++

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );
Dave Van Wagner
la source
8

Quel genre de choses peuvent causer ces erreurs?

Faire des choses vilaines avec la mémoire, par exemple écrire après la fin d'un tampon, ou écrire dans un tampon après qu'il a été libéré dans le tas.

Comment les déboguer?

Utilisez un instrument qui ajoute une vérification automatique des limites à votre exécutable: c'est-à-dire valgrind sous Unix, ou un outil comme BoundsChecker (Wikipedia suggère également Purify et Insure ++) sous Windows.

Sachez que ceux-ci ralentiront votre application, ils peuvent donc être inutilisables si la vôtre est une application en temps réel doux.

Une autre aide / outil de débogage possible pourrait être HeapAgent de MicroQuill.

ChrisW
la source
1
La reconstruction de l'application avec le runtime de débogage (/ MDd ou / MTd flag) serait ma première étape. Celles-ci effectuent des vérifications supplémentaires chez malloc et free, et sont souvent efficaces pour réduire l'emplacement du ou des bogues.
Employé russe le
HeapAgent de MicroQuill: il n'y a pas beaucoup d'écrit ou entendu à ce sujet, mais pour la corruption de tas, il devrait être sur votre liste.
Samrat Patil
1
BoundsChecker fonctionne bien comme un test de fumée, mais ne pensez même pas à exécuter un programme sous celui-ci tout en essayant d'exécuter ce programme en production également. Le ralentissement peut être compris entre 60x et 300x, selon les options que vous utilisez et si vous utilisez ou non la fonction d'instrumentation du compilateur. Clause de non-responsabilité: je suis l'un des gars qui assure la maintenance du produit pour Micro Focus.
Rick Papo
8

Un conseil rapide, que j'ai obtenu de Détecter l'accès à la mémoire libérée est le suivant:

Si vous souhaitez localiser rapidement l'erreur, sans vérifier chaque instruction qui accède au bloc de mémoire, vous pouvez définir le pointeur de mémoire sur une valeur non valide après avoir libéré le bloc:

#ifdef _DEBUG // detect the access to freed memory
#undef free
#define free(p) _free_dbg(p, _NORMAL_BLOCK); *(int*)&p = 0x666;
#endif
EmpiléCrooked
la source
5

Le meilleur outil que j'ai trouvé utile et que j'ai travaillé à chaque fois est la révision du code (avec de bons réviseurs de code).

Autre que la révision du code, je commencerais par essayer le tas de pages . La mise en place de Page Heap prend quelques secondes et, avec un peu de chance, il peut identifier votre problème.

Si vous n'avez pas de chance avec Page Heap, téléchargez les outils de débogage pour Windows de Microsoft et apprenez à utiliser WinDbg. Désolé, impossible de vous donner une aide plus spécifique, mais le débogage de la corruption de tas multi-thread est plus un art que de la science. Google pour "WinDbg heap corruption" et vous devriez trouver de nombreux articles sur le sujet.

Shing Yip
la source
4

Vous pouvez également vérifier si vous établissez un lien avec la bibliothèque d'exécution C dynamique ou statique. Si vos fichiers DLL sont liés à la bibliothèque d'exécution statique C, les fichiers DLL ont des tas séparés.

Par conséquent, si vous deviez créer un objet dans une DLL et essayer de le libérer dans une autre DLL, vous obtiendrez le même message que vous voyez ci-dessus. Ce problème est référencé dans une autre question de débordement de pile, Libération de mémoire allouée dans une DLL différente .

dreadpirateryan
la source
3

Quel type de fonctions d'allocation utilisez-vous? J'ai récemment rencontré une erreur similaire en utilisant les fonctions d'allocation de style Heap *.

Il s'est avéré que je créais par erreur le tas avec le HEAP_NO_SERIALIZE option. Cela rend essentiellement les fonctions de tas exécutées sans sécurité des threads. C'est une amélioration des performances si elle est utilisée correctement, mais ne devrait jamais être utilisée si vous utilisez HeapAlloc dans un programme multi-thread [1]. Je ne le mentionne que parce que votre message mentionne que vous avez une application multithread. Si vous utilisez HEAP_NO_SERIALIZE n'importe où, supprimez-le et cela résoudra probablement votre problème.

[1] Dans certaines situations, cela est légal, mais cela vous oblige à sérialiser les appels à Heap * et ce n'est généralement pas le cas pour les programmes multi-threads.

JaredPar
la source
Oui: regardez les options du compilateur / build de l'application, et assurez-vous qu'elle est construite pour être liée à une version "multithread" de la bibliothèque d'exécution C.
ChrisW
@ChrisW pour les API de style HeapAlloc, c'est différent. C'est en fait un paramètre qui peut être modifié au moment de la création du tas, pas au moment de la liaison.
JaredPar
Oh. Il ne m'est pas venu à l'esprit que l'OP pourrait parler de ce tas, et non du tas dans le CRT.
ChrisW
@ChrisW, la question est plutôt vague mais je viens de toucher le problème que j'ai détaillé il y a ~ 1 semaine, donc c'est frais dans mon esprit.
JaredPar
3

Si ces erreurs se produisent au hasard, il y a une forte probabilité que vous rencontriez des courses de données. Veuillez vérifier: modifiez-vous les pointeurs de mémoire partagée de différents threads? Intel Thread Checker peut aider à détecter de tels problèmes dans un programme multithread.

Vladimir Obrizan
la source
1

En plus de rechercher des outils, pensez à rechercher un coupable probable. Y a-t-il un composant que vous utilisez, peut-être non écrit par vous, qui n'a peut-être pas été conçu et testé pour fonctionner dans un environnement multithread? Ou simplement un que vous ne connaissez pas a fonctionné dans un tel environnement.

La dernière fois que cela m'est arrivé, c'était un package natif qui avait été utilisé avec succès à partir de jobs batch pendant des années. Mais c'était la première fois dans cette entreprise qu'il était utilisé à partir d'un service Web .NET (qui est multithread). C'était tout - ils avaient menti sur le fait que le code était thread-safe.

John Saunders
la source
1

Vous pouvez utiliser les macros VC CRT Heap-Check pour _CrtSetDbgFlag : _CRTDBG_CHECK_ALWAYS_DF ou _CRTDBG_CHECK_EVERY_16_DF .. _CRTDBG_CHECK_EVERY_1024_DF .

KindDragon
la source
0

J'aimerais ajouter mon expérience. Au cours des derniers jours, j'ai résolu une instance de cette erreur dans mon application. Dans mon cas particulier, les erreurs dans le code étaient:

  • Suppression d'éléments d'une collection STL tout en l'itérant (je crois qu'il existe des indicateurs de débogage dans Visual Studio pour attraper ces choses; je l'ai attrapé lors de la révision du code)
  • Celui-ci est plus complexe, je vais le diviser en étapes:
    • À partir d'un thread C ++ natif, rappelez le code managé
    • En terrain géré, appelez Control.Invoke et supprimez un objet géré qui encapsule l'objet natif auquel appartient le rappel.
    • Puisque l'objet est toujours vivant dans le thread natif (il restera bloqué dans l'appel de rappel jusqu'à la Control.Invokefin). Je devrais préciser que j'utilise boost::thread, donc j'utilise une fonction membre comme fonction de thread.
    • Solution : utilisez Control.BeginInvokeplutôt (mon interface graphique est faite avec Winforms) afin que le thread natif puisse se terminer avant que l'objet ne soit détruit (le but du rappel est précisément de notifier que le thread s'est terminé et que l'objet peut être détruit).
dario_ramos
la source
0

J'ai eu un problème similaire - et il est apparu assez au hasard. Peut-être que quelque chose était corrompu dans les fichiers de construction, mais j'ai fini par le réparer en nettoyant d'abord le projet, puis en le reconstruisant.

Donc, en plus des autres réponses données:

Quel genre de choses peuvent causer ces erreurs? Quelque chose de corrompu dans le fichier de construction.

Comment les déboguer? Nettoyage du projet et reconstruction. S'il est résolu, c'était probablement le problème.

Marty
la source