Tout d'abord, je réalise que ce n'est pas une question de style Q&A parfaite avec une réponse absolue, mais je ne peux penser à aucun libellé pour le faire fonctionner mieux. Je ne pense pas qu'il existe une solution absolue à cela et c'est l'une des raisons pour lesquelles je le poste ici au lieu de Stack Overflow.
Au cours du dernier mois, j'ai réécrit un morceau de code de serveur (mmorpg) assez ancien pour être plus moderne et plus facile à étendre / modifier. J'ai commencé avec la partie réseau et j'ai implémenté une bibliothèque tierce (libevent) pour gérer les choses pour moi. Avec tous les changements de refactorisation et de code, j'ai introduit une corruption de mémoire quelque part et j'ai eu du mal à savoir où cela se produit.
Je n'arrive pas à le reproduire de manière fiable sur mon environnement de développement / test, même lorsque j'implémente des bots primitifs pour simuler une charge, je ne reçois plus de plantages (j'ai résolu un problème de libevent qui a causé certaines choses)
J'ai essayé jusqu'à présent:
Valgrinding the hell out of it - Aucun invalide n'écrit jusqu'à ce que la chose se bloque (ce qui peut prendre 1+ jour de production .. ou juste une heure) ce qui me déroute vraiment, à un moment donné, il accèderait à une mémoire invalide et n'écraserait pas les choses par chance? (Existe-t-il un moyen de "répartir" la plage d'adresses?)
Outils d'analyse de code, à savoir couverture et cppcheck. Bien qu'ils aient souligné certains cas de méchanceté et de bord dans le code, il n'y avait rien de grave.
Enregistrer le processus jusqu'à ce qu'il se bloque avec gdb (via undodb), puis revenir en arrière. Cela / sonne / comme cela devrait être faisable, mais je finis par planter gdb en utilisant la fonction de remplissage automatique ou je me retrouve dans une structure de libevent interne où je me perds car il y a trop de branches possibles (une corruption en provoque une autre, etc.) sur). Je suppose que ce serait bien si je pouvais voir à quoi appartient un pointeur à l'origine / où il a été alloué, cela éliminerait la plupart des problèmes de branchement. Je ne peux pas exécuter valgrind avec undodb cependant, et je l'enregistrement gdb normal est anormalement lent (si cela fonctionne même en combinaison avec valgrind).
Revue de code! Par moi-même (à fond) et en demandant à quelques amis de regarder mon code, bien que je doute qu'il soit suffisamment approfondi. Je pensais peut-être embaucher un développeur pour faire un examen / débogage du code avec moi, mais je ne peux pas me permettre de mettre trop d'argent et je ne saurais pas où chercher quelqu'un qui serait prêt à travailler pour peu- pas d'argent s'il ne trouve pas le problème ou si quelqu'un est qualifié du tout.
Je dois également noter: j'obtiens généralement des backtraces cohérentes. Il y a quelques endroits où le crash se produit, principalement lié à la classe de socket corrompue d'une manière ou d'une autre. Que ce soit un pointeur invalide pointant vers quelque chose qui n'est pas un socket ou la classe de socket elle-même devenant écrasée (partiellement?) Avec du charabia. Bien que je soupçonne qu'il y plante le plus car c'est l'une des parties les plus utilisées, c'est donc la première mémoire corrompue qui est utilisée.
Dans l'ensemble, ce problème m'a occupé pendant près de 2 mois (allumé et éteint, plus un projet de loisir) et me frustre vraiment au point où je deviens grincheux IRL et que je pense à abandonner. Je ne peux pas penser à quoi d'autre je suis censé faire pour trouver le problème.
Y a-t-il des techniques utiles que j'ai manquées? Comment gérez-vous cela? (Cela ne peut pas être si courant car il n'y a pas beaucoup d'informations à ce sujet .. ou je suis vraiment vraiment aveugle?)
Éditer:
Quelques spécifications au cas où cela compte:
Utiliser c ++ (11) via gcc 4.7 (version fournie par debian wheezy)
La base de code est d'environ 150 000 lignes
Modifier en réponse au message de david.pfx: (désolé pour la réponse lente)
Conservez-vous des enregistrements minutieux des accidents, pour rechercher des modèles?
Oui, j'ai encore des décharges des récents accidents qui traînent
Les quelques endroits sont-ils vraiment similaires? De quelle manière?
Eh bien, dans la version la plus récente (ils semblent changer chaque fois que j'ajoute / supprime du code ou modifie des structures connexes), il serait toujours pris dans une méthode de minuterie d'élément. Fondamentalement, un élément a une heure spécifique après laquelle il expire et il envoie des informations mises à jour au client. Le pointeur de socket invalide serait dans la classe Player (toujours valide pour autant que je sache), principalement liée à cela. Je connais également des charges de plantage dans la phase de nettoyage, après l'arrêt normal où il détruit toutes les classes statiques qui n'ont pas été explicitement détruites ( __run_exit_handlers
dans le backtrace). Impliquant principalement std::map
une classe, devinant que ce n'est que la première chose qui arrive.
À quoi ressemblent les données corrompues? Zéros? Ascii? Motifs?
Je n'ai pas encore trouvé de motifs, cela me semble quelque peu aléatoire. C'est difficile à dire car je ne sais pas où la corruption a commencé.
Est-ce lié au tas?
C'est entièrement lié au tas (j'ai activé le garde de pile de gcc et cela n'a rien attrapé).
La corruption se produit-elle après un
free()
?
Vous devrez élaborer un peu là-dessus. Voulez-vous dire avoir des pointeurs d'objets déjà libres qui traînent? Je mets chaque référence à null une fois que l'objet est détruit, donc à moins que je manque quelque chose quelque part, non. Cela devrait apparaître dans Valgrind, mais ce n'est pas le cas.
Y a-t-il quelque chose de distinct dans le trafic réseau (taille du tampon, cycle de récupération)?
Le trafic réseau est constitué de données brutes. Donc, les tableaux char, (u) intX_t ou les structures emballées (pour supprimer le remplissage) pour les choses plus complexes, chaque paquet a un en-tête composé d'un identifiant et de la taille du paquet elle-même qui est validée par rapport à la taille attendue. Ils font environ 10 à 60 octets, le plus gros (paquet de démarrage interne, tiré une fois au démarrage) ayant une taille de quelques Mo.
Beaucoup, beaucoup d'affirmations de production. Crash précoce et prévisible avant que les dégâts ne se propagent.
J'ai eu une fois un crash lié à la std::map
corruption, chaque entité a une carte de sa "vue", chaque entité qui peut la voir et vice-versa est là-dedans. J'ai ajouté un tampon de 200 octets devant et après, l'ai rempli avec 0x33 et l'ai vérifié avant chaque accès. La corruption a disparu comme par magie, j'ai dû déplacer quelque chose qui l'a fait corrompre autre chose.
Journalisation stratégique, pour que vous sachiez exactement ce qui se passait juste avant. Ajoutez à la journalisation à mesure que vous vous rapprochez d'une réponse.
Cela fonctionne .. dans une large mesure.
En désespoir de cause, pouvez-vous enregistrer l'état et redémarrer automatiquement? Je peux penser à quelques logiciels de production qui font cela.
Je fais un peu ça. Le logiciel se compose d'un processus principal de "cache" et de certains autres processus de travail qui accèdent tous au cache pour obtenir et enregistrer des éléments. Donc, par crash, je ne perds pas beaucoup de progrès, cela déconnecte toujours tous les utilisateurs et ainsi de suite, ce n'est certainement pas une solution.
Concurrence: threading, conditions de concurrence, etc.
Il y a un thread mysql pour faire des requêtes "asynchrones", tout cela reste intact et ne partage les informations avec la classe de base de données que via des fonctions avec tous les verrous.
Les interruptions
Il y a une minuterie d'interruption pour l'empêcher de se verrouiller qui s'arrête juste si elle n'a pas terminé un cycle pendant 30 secondes, ce code devrait cependant être sûr:
if (!tics) {
abort();
} else
tics = 0;
tics est volatile int tics = 0;
augmenté chaque fois qu'un cycle est terminé. Ancien code aussi.
événements / rappels / exceptions: état de corruption ou pile imprévisible
De nombreux rappels sont utilisés (E / S réseau asynchrone, minuteries), mais ils ne devraient rien faire de mal.
Données inhabituelles: données d'entrée / calendrier / état inhabituels
J'ai eu quelques cas marginaux liés à cela. La déconnexion d'un socket pendant que les paquets sont en cours de traitement a permis d'accéder à un nullptr et autres, mais ceux-ci ont été faciles à repérer jusqu'à présent puisque chaque référence est nettoyée juste après avoir dit à la classe elle-même que c'est fait. (La destruction elle-même est gérée par une boucle supprimant tous les objets détruits à chaque cycle)
Dépendance à l'égard d'un processus externe asynchrone.
Envie d'élaborer? C'est un peu le cas, le processus de cache mentionné ci-dessus. La seule chose que je pouvais imaginer du haut de ma tête serait de ne pas terminer assez rapidement et d'utiliser des données poubelles, mais ce n'est pas le cas car cela utilise également le réseau. Même modèle de paquet.
/analyze
) de Microsoft et Malloc et Scribble d'Apple. Vous devez également utiliser autant de compilateurs que possible en utilisant autant de normes que possible, car les avertissements du compilateur sont un diagnostic et ils s'améliorent avec le temps. Il n'y a pas de solution miracle et une taille unique ne convient pas à tous. Plus vous utilisez d'outils et de compilateurs, plus la couverture est complète car chaque outil a ses forces et ses faiblesses.