Question

je travaille sur un multithread Application C++ qui corrompt le tas.Les outils habituels pour localiser cette corruption semblent inapplicables.Les anciennes versions (18 mois) du code source présentent le même comportement que la version la plus récente, donc cela existe depuis longtemps et n'a tout simplement pas été remarqué ;par contre, les deltas des sources ne peuvent pas être utilisés pour identifier le moment où le bug a été introduit - il y a beaucoup des changements de code dans le référentiel.

L'invite pour un comportement de plantage est de générer un débit dans ce système - un transfert de données par socket qui est intégré dans une représentation interne.J'ai un ensemble de données de test qui provoqueront périodiquement une exception de l'application (divers endroits, diverses causes - y compris l'échec de l'allocation de tas, ainsi :corruption de masse).

Le comportement semble lié à la puissance du processeur ou à la bande passante mémoire ;plus la machine en possède, plus il est facile de tomber en panne.La désactivation d'un cœur hyper-threading ou d'un cœur double cœur réduit le taux de corruption (mais n'élimine pas).Cela suggère un problème lié au timing.

Maintenant, voici le problème :
Lorsqu'il est exécuté dans un environnement de débogage léger (par exemple Visual Studio 98 / AKA MSVC6) la corruption du tas est raisonnablement facile à reproduire - dix ou quinze minutes s'écoulent avant que quelque chose échoue horriblement et des exceptions, comme un alloc; lors de l'exécution dans un environnement de débogage sophistiqué (Rational Purify, VS2008/MSVC9 ou même Microsoft Application Verifier), le système devient limité à la vitesse de la mémoire et ne plante pas (Memory-bound :Le processeur ne dépasse pas 50%, le voyant du disque n'est pas allumé, le programme va aussi vite qu'il peut, la boîte consomme 1.3G de 2G de RAM).Donc, J'ai le choix entre pouvoir reproduire le problème (mais pas identifier la cause) ou pouvoir identifier la cause ou un problème que je ne peux pas reproduire.

Mes meilleures suppositions actuelles quant à la prochaine étape sont :

  1. Obtenez une boîte incroyablement grincheuse (pour remplacer la boîte de développement actuelle :2 Go de RAM dans un E6550 Core2 Duo);cela permettra de reproduire le crash provoquant un mauvais comportement lors de l'exécution dans un environnement de débogage puissant ;ou
  2. Opérateurs de réécriture new et delete utiliser VirtualAlloc et VirtualProtect pour marquer la mémoire en lecture seule dès que c'est terminé.Courir sous MSVC6 et demandez au système d'exploitation d'attraper le méchant qui écrit dans la mémoire libérée.Oui, c'est un signe de désespoir :qui diable réécrit new et delete?!Je me demande si cela va le rendre aussi lent que sous Purify et al.

Et non:L’expédition avec l’instrument Purify intégré n’est pas une option.

Un collègue vient de passer et demande « Stack Overflow ?Sommes-nous confrontés à des débordements de pile maintenant ?!?"

Et maintenant, la question : Comment localiser le corrupteur de tas ?


Mise à jour:équilibrage new[] et delete[] Il semble que l'on ait fait un long chemin vers la résolution du problème.Au lieu de 15 minutes, l'application met désormais environ deux heures avant de planter.Pas encore là.D'autres suggestions ?La corruption du tas persiste.

Mise à jour:une version sous Visual Studio 2008 semble nettement meilleure ;les soupçons actuels reposent sur STL implémentation fournie avec VS98.


  1. Reproduisez le problème. Dr Watson produira un dump qui pourrait être utile dans une analyse plus approfondie.

J'en prends note, mais je crains que le Dr Watson ne trébuche qu'après coup, et non lorsque le tas est piétiné.

Un autre essai pourrait être d'utiliser WinDebug comme un outil de débogage assez puissant et en même temps léger.

J'ai ça en cours pour le moment, encore une fois :pas beaucoup d'aide jusqu'à ce que quelque chose ne va pas.Je veux attraper le vandale en flagrant délit.

Peut-être que ces outils vous permettront au moins de limiter le problème à certains composants.

Je n'ai pas beaucoup d'espoir, mais les temps désespérés appellent...

Et êtes-vous sûr que tous les composants du projet disposent des paramètres de bibliothèque d'exécution corrects (C/C++ tab, catégorie Génération de code dans les paramètres du projet VS 6.0) ?

Non, ce n'est pas le cas, et je passerai quelques heures demain à parcourir l'espace de travail (58 projets dedans) et à vérifier qu'ils sont tous compilés et liés avec les indicateurs appropriés.


Mise à jour:Cela a pris 30 secondes.Sélectionnez tous les projets dans le Settings Dans la boîte de dialogue, désélectionnez jusqu'à ce que vous trouviez le(s) projet(s) qui n'ont pas les bons paramètres (ils avaient tous les bons paramètres).

Était-ce utile?

La solution

Mon premier choix serait un outil de tas dédié tel que pageheap.exe.

Réécrire new et delete peut être utile, mais cela ne prend pas en compte les allocations validées par le code de niveau inférieur.Si c'est ce que vous désirez, mieux vaut faire un détour par low-level alloc APIs en utilisant Microsoft Detours.

Également des contrôles d'intégrité tels que :vérifiez que vos bibliothèques d'exécution correspondent (version vs.débogage, multithread vs.monothread, dll vs.static lib), recherchez les mauvaises suppressions (par exemple, delete là où delete [] aurait dû être utilisé), assurez-vous de ne pas mélanger et faire correspondre vos allocations.

Essayez également de désactiver sélectivement les threads et voyez quand/si le problème disparaît.

À quoi ressemble la pile d'appels, etc. au moment de la première exception ?

Autres conseils

J'ai les mêmes problèmes dans mon travail (nous utilisons également VC6 parfois).Et il n’existe pas de solution simple à ce problème.Je n'ai que quelques indices :

  • Essayez avec des vidages sur incident automatiques sur la machine de production (voir Dumper de processus).Mon expérience dit le Dr.Watson est pas parfait pour le dumping.
  • Enlever tout attraper(...) à partir de votre code.Ils cachent souvent de graves exceptions de mémoire.
  • Vérifier Débogage Windows avancé - il existe de nombreux conseils utiles pour des problèmes comme le vôtre.Je le recommande de tout mon cœur.
  • Si tu utilises STL essayer STLPort et vérifié les builds.Les itérateurs invalides sont un enfer.

Bonne chance.Des problèmes comme le vôtre nous prennent des mois à résoudre.Soyez prêt pour ça...

Exécutez l'application d'origine avec ADplus -crash -pn appnename.exeLorsque le problème de mémoire apparaît, vous obtenez un gros dump.

Vous pouvez analyser le dump pour déterminer quel emplacement mémoire a été corrompu.Si vous êtes chanceux, la mémoire d'écrasement est une chaîne unique, vous pouvez déterminer d'où elle vient.Si vous n'avez pas de chance, vous devrez creuser win32 tas et figurez quelles étaient les caractéristiques de la mémoire originale.(heap -x pourrait aider)

Une fois que vous savez ce qui a gâché, vous pouvez restreindre l'utilisation du vérificateur d'application avec des paramètres de tas spéciaux.c'est à dire.vous pouvez préciser quoi DLL vous surveillez, ou quelle taille d’allocation surveiller.

Espérons que cela accélérera suffisamment la surveillance pour attraper le coupable.

D'après mon expérience, je n'ai jamais eu besoin du mode de vérification du tas complet, mais j'ai passé beaucoup de temps à analyser les vidages sur incident et à parcourir les sources.

P.S :Vous pouvez utiliser DébogageDiag pour analyser les décharges.Il peut souligner le DLL propriétaire du tas corrompu et vous donne d'autres détails utiles.

Nous avons eu beaucoup de chance en écrivant nos propres fonctions malloc et gratuites.En production, ils appellent simplement le standard malloc et free, mais en débogage, ils peuvent faire ce que vous voulez.Nous avons également une classe de base simple qui ne fait que remplacer les opérateurs new et delete pour utiliser ces fonctions, puis toute classe que vous écrivez peut simplement hériter de cette classe.Si vous avez une tonne de code, cela peut être un gros travail de remplacer les appels à malloc et free par le nouveau malloc et free (n'oubliez pas realloc !), mais à long terme, c'est très utile.

Dans le livre de Steve Maguire Écrire du code solide (fortement recommandé), il existe des exemples de tâches de débogage que vous pouvez effectuer dans ces routines, comme :

  • Gardez une trace des allocations pour trouver les fuites
  • Allouez plus de mémoire que nécessaire et placez des marqueurs au début et à la fin de la mémoire. Pendant la routine libre, vous pouvez vous assurer que ces marqueurs sont toujours là.
  • memset la mémoire avec un marqueur sur l'allocation (pour trouver l'utilisation de la mémoire non initialisée) et sur libre (pour trouver l'utilisation de la mémoire libérée)

Une autre bonne idée est de jamais utiliser des choses comme strcpy, strcat, ou sprintf -- utilisez toujours strncpy, strncat, et snprintf.Nous en avons également écrit nos propres versions, pour nous assurer de ne pas effacer la fin d'un tampon, et celles-ci ont également rencontré de nombreux problèmes.

Vous devez attaquer ce problème avec à la fois une analyse d'exécution et une analyse statique.

Pour l'analyse statique, envisagez de compiler avec PREfast (cl.exe /analyze).Il détecte les erreurs delete et delete[], des dépassements de tampon et une foule d'autres problèmes.Soyez prêt, cependant, à parcourir plusieurs kilo-octets d'avertissement L6, surtout si votre projet a encore L4 pas réparé.

PREfast est disponible avec Visual Studio Team System et, apparemment, dans le cadre du SDK Windows.

Le caractère apparemment aléatoire de la corruption de la mémoire ressemble beaucoup à un problème de synchronisation des threads : un bug est reproduit en fonction de la vitesse de la machine.Si les objets (morceaux de mémoire) sont partagés entre les threads et que les primitives de synchronisation (section critique, mutex, sémaphore, autre) ne sont pas par classe (par objet, par classe), alors il est possible d'arriver à une situation où la classe (morceau de mémoire) est supprimée/libérée pendant son utilisation, ou utilisée après sa suppression/libération.

Pour tester cela, vous pouvez ajouter des primitives de synchronisation à chaque classe et méthode.Cela ralentira votre code car de nombreux objets devront s'attendre les uns les autres, mais si cela élimine la corruption du tas, votre problème de corruption du tas deviendra un problème d'optimisation du code.

Est-ce dans des conditions de mémoire faible ?Si c'est le cas, il se pourrait que du nouveau revienne NULL plutôt que de lancer std::bad_alloc.Plus vieux VC++ les compilateurs ne l'ont pas correctement implémenté.Il y a un article sur Échecs d’allocation de mémoire hérités s'écraser STL applications créées avec VC6.

Vous avez essayé d'anciennes versions, mais y a-t-il une raison pour laquelle vous ne pouvez pas remonter plus loin dans l'historique du référentiel et voir exactement quand le bug a été introduit ?

Sinon, je suggérerais d'ajouter une journalisation simple pour aider à localiser le problème, même si je ne sais pas exactement ce que vous pourriez vouloir enregistrer.

Si vous pouvez découvrir ce qui PEUT exactement causer ce problème, via Google et la documentation des exceptions que vous obtenez, cela vous donnera peut-être plus d'informations sur ce qu'il faut rechercher dans le code.

Ma première action serait la suivante :

  1. Construisez les binaires en version "Release" mais en créant un fichier d'informations de débogage (vous trouverez cette possibilité dans les paramètres du projet).
  2. Utilisez Dr Watson comme débogueur par défaut (DrWtsn32 -I) sur une machine sur laquelle vous souhaitez reproduire le problème.
  3. Reproduisez le problème.Le Dr Watson produira un dump qui pourrait être utile pour une analyse plus approfondie.

Une autre tentative pourrait consister à utiliser WinDebug comme outil de débogage qui est à la fois assez puissant et léger.

Peut-être que ces outils vous permettront au moins de limiter le problème à certains composants.

Et êtes-vous sûr que tous les composants du projet ont des paramètres de bibliothèque d'exécution corrects (onglet C/C++, catégorie Génération de code dans les paramètres du projet VS 6.0) ?

Ainsi, à partir des informations limitées dont vous disposez, cela peut être une combinaison d’une ou plusieurs choses :

  • Mauvaise utilisation du tas, c'est-à-dire double libération, lecture après libération, écriture après libération, définition de l'indicateur HEAP_NO_SERIALIZE avec allocations et libérations de plusieurs threads sur le même tas
  • Mémoire insuffisante
  • Mauvais code (c'est-à-dire débordements de tampon, dépassements insuffisants de tampon, etc.)
  • Problèmes de « timing »

S'il s'agit des deux premiers mais pas du dernier, vous devriez déjà l'avoir détecté avec l'un ou l'autre de pageheap.exe.

Ce qui signifie très probablement que cela est dû à la manière dont le code accède à la mémoire partagée.Malheureusement, retrouver cela va être plutôt pénible.L'accès non synchronisé à la mémoire partagée se manifeste souvent par d'étranges problèmes de « timing ».Des choses comme ne pas utiliser la sémantique d'acquisition/libération pour synchroniser l'accès à la mémoire partagée avec un indicateur, ne pas utiliser les verrous de manière appropriée, etc.

À tout le moins, il serait utile de pouvoir suivre d’une manière ou d’une autre les allocations, comme cela a été suggéré plus tôt.Au moins, vous pourrez voir ce qui s'est réellement passé jusqu'à la corruption du tas et tenter d'établir un diagnostic à partir de cela.

De plus, si vous pouvez facilement rediriger les allocations vers plusieurs tas, vous voudrez peut-être essayer cela pour voir si cela résout le problème ou entraîne un comportement bogué plus reproductible.

Lorsque vous avez testé avec VS2008, avez-vous exécuté avec HeapVerifier avec Conserve Memory défini sur Oui ?Cela pourrait réduire l'impact sur les performances de l'allocateur de tas.(De plus, vous devez l'exécuter Debug->Start with Application Verifier, mais vous le savez peut-être déjà.)

Vous pouvez également essayer le débogage avec Windbg et diverses utilisations de la commande !heap.

MSN

Si vous choisissez de réécrire nouveau/supprimer, je l'ai fait et j'ai un code source simple à l'adresse :

http://gandolf.homelinux.org/~smhanov/blog/?id=10

Cela détecte les fuites de mémoire et insère également des données de garde avant et après le bloc de mémoire pour capturer la corruption du tas.Vous pouvez simplement l'intégrer en plaçant #include "debug.h" en haut de chaque fichier CPP et en définissant DEBUG et DEBUG_MEM.

La suggestion de Graeme de custom malloc/free est une bonne idée.Voyez si vous pouvez caractériser un modèle de corruption pour vous donner une idée sur laquelle tirer parti.

Par exemple, s'il se trouve toujours dans un bloc de la même taille (disons 64 octets), modifiez votre paire malloc/free pour toujours allouer des morceaux de 64 octets dans leur propre page.Lorsque vous libérez un morceau de 64 octets, définissez les bits de protection de la mémoire sur cette page pour empêcher les lectures et les écritures (à l'aide de VirtualQuery).Ensuite, toute personne tentant d'accéder à cette mémoire générera une exception plutôt que de corrompre le tas.

Cela suppose que le nombre de morceaux de 64 octets en attente n'est que modéré ou que vous avez beaucoup de mémoire à graver dans la boîte !

Le peu de temps dont je disposais pour résoudre un problème similaire.Si le problème persiste, je vous suggère de faire ceci :Surveillez tous les appels vers new/delete et malloc/calloc/realloc/free.Je crée une seule DLL en exportant une fonction pour enregistrer tous les appels.Cette fonction reçoit un paramètre permettant d'identifier votre source de code, un pointeur vers la zone allouée et le type d'appel en enregistrant ces informations dans un tableau.Toute paire allouée/libérée est éliminée.À la fin ou après avoir besoin, vous appelez une autre fonction pour créer un rapport pour les données restantes.Avec cela, vous pouvez identifier les mauvais appels (nouveaux/gratuits ou malloc/supprimer) ou manquants.Si un tampon est écrasé dans votre code, les informations enregistrées peuvent être erronées mais chaque test peut détecter/découvrir/inclure une solution à l'échec identifié.De nombreuses exécutions pour aider à identifier les erreurs.Bonne chance.

Pensez-vous qu'il s'agit d'une condition de concurrence ?Plusieurs threads partagent-ils un seul tas ?Pouvez-vous donner à chaque thread un tas privé avec HeapCreate, ils pourront alors s'exécuter rapidement avec HEAP_NO_SERIALIZE.Sinon, un tas doit être thread-safe, si vous utilisez la version multithread des bibliothèques système.

Quelques suggestions.Vous mentionnez les nombreux avertissements de W4 - je suggérerais de prendre le temps de corriger votre code pour le compiler proprement au niveau d'avertissement 4 - cela contribuera grandement à empêcher les bogues subtils et difficiles à trouver.

Deuxièmement - pour le commutateur /analyze - il génère effectivement de nombreux avertissements.Pour utiliser ce commutateur dans mon propre projet, j'ai créé un nouveau fichier d'en-tête utilisant l'avertissement #pragma pour désactiver tous les avertissements supplémentaires générés par /analyze.Ensuite, plus bas dans le fichier, j'active uniquement les avertissements qui m'intéressent.Utilisez ensuite le commutateur du compilateur /FI pour forcer ce fichier d'en-tête à être inclus en premier dans toutes vos unités de compilation.Cela devrait vous permettre d'utiliser le commutateur /analyze tout en contrôlant la sortie

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top