Fehlerbehebung bei Speicherbeschädigungen

23

Zunächst einmal stelle ich fest, dass dies keine perfekte Frage im Q & A-Stil mit einer absoluten Antwort ist, aber ich kann mir keine Formulierung vorstellen, mit der es besser funktioniert. Ich glaube nicht, dass es eine absolute Lösung dafür gibt, und dies ist einer der Gründe, warum ich es hier anstelle von Stack Overflow poste.

Im letzten Monat habe ich ein ziemlich altes Stück Servercode (mmorpg) umgeschrieben, um moderner zu sein und einfacher zu erweitern / modifizieren. Ich habe mit dem Netzwerkteil begonnen und eine Bibliothek von Drittanbietern (libevent) implementiert, um Dinge für mich zu erledigen. Bei all den Re-Factoring- und Code-Änderungen habe ich irgendwo eine Speicherbeschädigung eingeführt, und ich hatte Mühe herauszufinden, wo das passiert.

Ich kann es scheinbar nicht zuverlässig in meiner Entwicklungs- / Testumgebung reproduzieren, selbst wenn ich primitive Bots implementiere, um eine Last zu simulieren. Ich bekomme keine Abstürze mehr (Ich habe ein libevent-Problem behoben, das einige Dinge verursachte).

Ich habe bisher versucht:

Verdammt noch mal - Keine ungültigen Schreibvorgänge, bis das Ding abstürzt (was mehr als einen Tag in der Produktion dauern kann .. oder nur eine Stunde), was mich wirklich verblüfft. Irgendwann würde es auf ungültigen Speicher zugreifen und keine Inhalte von überschreiben Chance? (Gibt es eine Möglichkeit, den Adressbereich zu "verteilen"?)

Code-Analyse-Tools, nämlich Coverity und Cppcheck. Während sie auf einige Bösartigkeits- und Randfälle im Code hinwiesen, gab es nichts Ernstes.

Den Prozess aufzeichnen, bis er mit gdb abstürzt (via undodb) und dann rückwärts arbeiten. Dies / hört sich / so an, als ob es machbar wäre, aber entweder stürze ich gdb mit der Auto-Vervollständigungs-Funktion ab oder lande in einer internen libevent-Struktur, in der ich mich verliere, da es zu viele mögliche Zweige gibt (eine Beschädigung verursacht eine andere und so weiter) auf). Ich denke, es wäre schön, wenn ich sehen könnte, zu was ein Zeiger ursprünglich gehört / wohin er zugeordnet wurde, um die meisten Verzweigungsprobleme zu beseitigen. Ich kann Valgrind nicht mit Undodb ausführen, und der normale GdB-Record ist ungewöhnlich langsam (wenn das überhaupt in Kombination mit Valgrind funktioniert).

Code-Review! Alleine (gründlich) und mit ein paar Freunden meinen Code durchsehen, obwohl ich bezweifle, dass er gründlich genug war. Ich habe darüber nachgedacht, vielleicht einen Entwickler einzustellen, der mit mir eine Codeüberprüfung / ein Debugging durchführt, aber ich kann es mir nicht leisten, zu viel Geld darin zu stecken, und ich würde nicht wissen, wo ich jemanden suchen soll, der bereit wäre, für wenig Geld zu arbeiten. zu-kein Geld, wenn er die Ausgabe nicht findet oder überhaupt jemand qualifiziert ist.

Ich sollte auch beachten: Ich bekomme normalerweise konsistente Backtraces. Es gibt ein paar Stellen, an denen der Absturz auftritt, die meistens damit zusammenhängen, dass die Socket-Klasse irgendwie beschädigt wird. Sei es ein ungültiger Zeiger, der auf etwas zeigt, das kein Socket ist, oder die Socket-Klasse selbst, die (teilweise?) Mit Kauderwelsch überschrieben wird. Obwohl ich vermute, dass es dort am häufigsten abstürzt, da dies einer der am häufigsten verwendeten Teile ist, ist es der erste beschädigte Speicher, der verwendet wird.

Alles in allem hat mich diese Ausgabe fast 2 Monate beschäftigt (hin und wieder, eher ein Hobbyprojekt) und frustriert mich wirklich bis zu dem Punkt, an dem ich mürrisch werde und darüber nachdenke, einfach aufzugeben. Ich kann nur nicht darüber nachdenken, was ich sonst tun soll, um das Problem zu finden.

Gibt es nützliche Techniken, die ich vermisst habe? Wie gehst du damit um? (Es kann nicht so häufig sein, da es nicht viele Informationen dazu gibt. Oder bin ich einfach nur blind?)

Bearbeiten:

Einige Angaben für den Fall, dass es darauf ankommt:

Verwendung von c ++ (11) über gcc 4.7 (Version von debian wheezy)

Die Codebasis beträgt ca. 150k Zeilen

Bearbeiten als Antwort auf den Beitrag von david.pfx: (Entschuldigung für die langsame Antwort)

Führen Sie sorgfältige Aufzeichnungen über Abstürze, um nach Mustern zu suchen?

Ja, ich habe immer noch Müllhalden der letzten Abstürze herumliegen

Sind sich die wenigen Orte wirklich ähnlich? Inwiefern?

Nun, in der neuesten Version (sie scheinen sich zu ändern, wenn ich Code hinzufüge / entferne oder verwandte Strukturen ändere), wurde sie immer von einer Item-Timer-Methode erfasst. Grundsätzlich hat ein Artikel eine bestimmte Zeit, nach deren Ablauf er abläuft und sendet aktualisierte Informationen an den Kunden. Der ungültige Socket-Zeiger würde in der (meines Erachtens immer noch gültigen) Player-Klasse liegen, was hauptsächlich damit zusammenhängt. Ich habe auch viele Abstürze in der Bereinigungsphase, nach dem normalen Herunterfahren, wo alle statischen Klassen zerstört werden, die nicht explizit zerstört wurden ( __run_exit_handlersim Backtrace). Meistens handelt es sich um std::mapeine Klasse, aber das ist nur das Erste, was auftaucht.

Wie sehen die beschädigten Daten aus? Nullen? ASCII? Muster?

Ich habe noch keine Muster gefunden, scheint mir etwas zufällig. Es ist schwer zu sagen, da ich nicht weiß, wo die Korruption begann.

Handelt es sich um Haufen?

Es hat nichts mit Heap zu tun (ich habe gccs Stack Guard aktiviert und das hat nichts verstanden).

Kommt die Korruption nach einem free()?

Du wirst ein bisschen darüber nachdenken müssen. Meinen Sie damit Hinweise auf bereits frei liegende Objekte? Ich setze jeden Verweis auf null, sobald das Objekt zerstört wurde. Wenn ich also nicht irgendwo etwas verpasst habe, nein. Das sollte sich in valgrind zeigen, was es aber nicht tat.

Hat der Netzwerkverkehr etwas Besonderes (Puffergröße, Wiederherstellungszyklus)?

Der Netzwerkverkehr besteht aus Rohdaten. Daher hat jedes Paket einen Header, der aus einer ID und der Paketgröße selbst besteht und anhand der erwarteten Größe validiert wird. Sie sind ungefähr 10-60 Bytes groß, wobei das größte (interne 'Bootup'-Paket, das einmal beim Start ausgelöst wird) eine Größe von einigen MB hat.

Viele, viele Produktionsaussagen. Absturz früh und vorhersehbar, bevor sich der Schaden ausbreitet.

Ich hatte einmal einen Absturz im Zusammenhang mit std::mapKorruption, jede Entität hat eine Karte ihrer "Ansicht", jede Entität, die sie sehen kann und umgekehrt, ist darin. Ich fügte einen 200-Byte-Puffer vor und nach, füllte ihn mit 0x33 und überprüfte ihn vor jedem Zugriff. Die Korruption ist einfach auf magische Weise verschwunden. Ich muss etwas bewegt haben, das sie zu etwas anderem Korruptem gemacht hat.

Strategische Protokollierung, damit Sie genau wissen, was gerade passiert ist. Ergänzen Sie die Protokollierung, wenn Sie einer Antwort näher kommen.

Es funktioniert bis zu einem gewissen Grad.

Können Sie in Ihrer Verzweiflung den Status speichern und automatisch neu starten? Ich kann mir ein paar Teile der Produktionssoftware vorstellen, die das tun.

Ich mache das irgendwie. Die Software besteht aus einem Haupt- "Cache" -Prozess und einigen anderen Worker-Prozessen, die alle auf den Cache zugreifen, um Daten abzurufen und zu speichern. So verliere ich pro Absturz nicht viel Fortschritt, es trennt immer noch alle Benutzer und so weiter, es ist definitiv keine Lösung.

Parallelität: Threading, Racebedingungen usw

Es gibt einen MySQL-Thread, mit dem "asynchrone" Abfragen durchgeführt werden können. Dies ist jedoch alles unberührt und teilt der Datenbankklasse nur Informationen über Funktionen mit allen Sperren.

Interrupts

Es gibt einen Interrupt-Timer, der verhindert, dass es zu einem Absturz kommt, der nur abgebrochen wird, wenn 30 Sekunden lang kein Zyklus abgeschlossen wurde. Dieser Code sollte jedoch sicher sein:

if (!tics) {
    abort();
} else
    tics = 0;

Die Tics werden volatile int tics = 0;jedes Mal erhöht, wenn ein Zyklus abgeschlossen ist. Alter Code auch.

Ereignisse / Rückrufe / Ausnahmen: Der Status oder der Stack wird unvorhersehbar beschädigt

Viele Rückrufe werden verwendet (asynchrone Netzwerk-E / A, Timer), aber sie sollten nichts Schlechtes tun.

Ungewöhnliche Daten: ungewöhnliche Eingabedaten / Timing / Status

Ich habe ein paar Randfälle im Zusammenhang damit gehabt. Das Trennen eines Sockets, während Pakete noch verarbeitet werden, führte zum Zugriff auf einen Nullptr und dergleichen, aber diese waren bisher leicht zu erkennen, da jede Referenz sofort bereinigt wird, nachdem der Klasse selbst mitgeteilt wurde, dass sie fertig ist. (Die Zerstörung selbst wird durch eine Schleife behandelt, die alle zerstörten Objekte in jedem Zyklus löscht.)

Abhängigkeit von einem asynchronen externen Prozess.

Möchten Sie näher darauf eingehen? Dies ist etwas der Fall, der oben erwähnte Cache-Prozess. Das Einzige, was ich mir auf Anhieb vorstellen könnte, wäre, dass es nicht schnell genug fertig wird und Mülldaten verwendet, aber das ist nicht der Fall, da auch das Netzwerk verwendet wird. Gleiches Paketmodell.

Robin
quelle
7
Leider ist dies in nicht-trivialen C ++ - Apps üblich. Wenn Sie die Quellcodeverwaltung verwenden, können Sie verschiedene Änderungssätze testen, um einzugrenzen, durch welche Codeänderung das Problem verursacht wurde. In diesem Fall ist dies möglicherweise nicht möglich.
Telastyn
Ja, in meinem Fall ist das wirklich nicht machbar. Grundsätzlich ging ich für 2 Monate von der Arbeit zu einer vollständigen und völligen Unterbrechung über und dann zur Debugging-Phase, in der ich etwas funktionierenden Code habe. Das alte System erlaubte mir wirklich nicht, einen neuen, irgendwie flexiblen Netzwerkcode zu implementieren, ohne alles zu beschädigen.
Robin
2
An diesem Punkt müssen Sie möglicherweise versuchen, jedes Teil zu isolieren. Nehmen Sie jede Klasse / Teilmenge der Lösung, verspotten Sie sie, damit sie funktionieren kann, und testen Sie die Hölle, bis Sie den Abschnitt finden, der fehlschlägt.
Am
Beginnen Sie, indem Sie Teile des Codes auskommentieren, bis Sie den Absturz nicht mehr haben.
cpp81
1
Zusätzlich zu Valgrind, Coverity und Cppcheck sollten Sie Asan und UBsan zu Ihrem Testprogramm hinzufügen. Wenn Ihr Code corss-platofrm ist, fügen Sie auch Microsoft Enterprise Analysis ( /analyze) und Apples Malloc- und Scribble-Schutz hinzu. Sie sollten auch so viele Compiler wie möglich mit so vielen Standards wie möglich verwenden, da Compiler-Warnungen eine Diagnose darstellen und mit der Zeit besser werden. Es gibt keine Silberkugel und eine Größe passt nicht für alle. Je mehr Tools und Compiler Sie verwenden, desto vollständiger wird die Abdeckung, da jedes Tool seine Stärken und Schwächen aufweist.

Antworten:

21

Es ist ein herausforderndes Problem, aber ich vermute, dass in den Abstürzen, die Sie bereits gesehen haben, noch viel mehr Hinweise zu finden sind.

  • Führen Sie sorgfältige Aufzeichnungen über Abstürze, um nach Mustern zu suchen?
  • Sind sich die wenigen Orte wirklich ähnlich? Inwiefern?
  • Wie sehen die beschädigten Daten aus? Nullen? ASCII? Muster?
  • Handelt es sich um Multithreading? Könnte es eine Rennbedingung sein?
  • Handelt es sich um Haufen? Tritt die Korruption nach einem free () auf?
  • Ist es stapelbezogen? Wird der Stack beschädigt?
  • Ist eine baumelnde Referenz eine Möglichkeit? Ein Datenwert, der sich auf mysteriöse Weise geändert hat?
  • Hat der Netzwerkverkehr etwas Besonderes (Puffergröße, Wiederherstellungszyklus)?

Dinge, die wir in ähnlichen Situationen benutzt haben.

  • Viele, viele Produktionsaussagen. Absturz früh und vorhersehbar, bevor sich der Schaden ausbreitet.
  • Viele, viele Wachen. Zusätzliche Datenelemente vor und nach lokalen Variablen, Objekten und Mallocs () werden auf einen Wert gesetzt und dann häufig überprüft.
  • Strategische Protokollierung, damit Sie genau wissen, was gerade passiert ist. Ergänzen Sie die Protokollierung, wenn Sie einer Antwort näher kommen.

Können Sie in Ihrer Verzweiflung den Status speichern und automatisch neu starten? Ich kann mir ein paar Teile der Produktionssoftware vorstellen, die das tun.

Fühlen Sie sich frei, Details hinzuzufügen, wenn wir überhaupt helfen können.


Kann ich nur hinzufügen, dass solche ernsthaft unbestimmten Bugs nicht allzu häufig sind und es nicht viele Dinge gibt, die sie (normalerweise) verursachen können. Sie beinhalten:

  • Parallelität: Threading, Racebedingungen usw
  • Interrupts / Ereignisse / Rückrufe / Ausnahmen: Der Status oder der Stack wird unvorhersehbar beschädigt
  • Ungewöhnliche Daten: ungewöhnliche Eingabedaten / Timing / Status
  • Abhängigkeit von einem asynchronen externen Prozess.

Dies sind die Teile des Codes, auf die Sie sich konzentrieren müssen.

david.pfx
quelle
+1 Alle guten Vorschläge, insbesondere die Behauptungen, Wachen und Protokollierung.
andy256
Ich habe einige weitere Informationen in meiner Frage als Antwort auf Ihre Antwort bearbeitet. Das hat mich tatsächlich an die Abstürze beim Herunterfahren erinnert, die ich mir noch nicht so genau angesehen habe, also werde ich das wohl erst einmal durchziehen.
Robin
5

Verwenden Sie eine Debug-Version von malloc / free. Wickeln Sie sie ein und schreiben Sie bei Bedarf Ihre eigenen. Viel Spaß!

Die von mir verwendete Version fügt vor und nach jeder Zuweisung Schutzbytes hinzu und verwaltet eine "zugewiesene" Liste, anhand derer freigegebene Chunks überprüft werden. Dies fängt die meisten Pufferüberläufe und mehrfachen oder nicht autorisierten "freien" Fehler ab.

Eine der heimtückischsten Ursachen für Korruption besteht darin, einen freigelassenen Teil weiter zu verwenden. Free sollte den freigegebenen Speicher mit einem bekannten Muster füllen (traditionell 0xDEADBEEF). Es ist hilfreich, wenn zugewiesene Strukturen ein "magisches Zahlen" -Element enthalten und vor Verwendung einer Struktur großzügig nach der entsprechenden magischen Zahl suchen.

ddyer
quelle
1
Valgrind sollte doppelte Frees / Nutzung von Free'd-Daten fangen, nicht wahr?
Robin
Das Schreiben dieser Art von Überladungen für new / delete hat mir geholfen, zahlreiche Speicherbeschädigungsprobleme zu lokalisieren. Insbesondere die Guard-Bytes, die beim Löschen überprüft werden und einen vom Programm ausgelösten Haltepunkt verursachen, der mich automatisch in den Debugger versetzt.
Emily L.
3

Um zu paraphrasieren, was Sie in Ihrer Frage sagen, ist es nicht möglich, Ihnen eine endgültige Antwort zu geben. Das Beste, was wir tun können, ist, Vorschläge für Dinge zu machen, nach denen wir suchen, sowie Werkzeuge und Techniken.

Einige Vorschläge erscheinen naiv, andere mögen zutreffender sein, aber hoffentlich löst man einen Gedanken aus, dem Sie folgen können. Ich muss sagen, dass die Antwort von david.pfx fundierte Ratschläge und Vorschläge hat.

Aus den Symptomen

  • für mich klingt es wie ein Pufferüberlauf.

  • Ein verwandtes Problem ist die Verwendung nicht validierter Socket-Daten als Index oder Schlüssel usw.

  • Ist es möglich, dass Sie irgendwo eine globale Variable verwenden oder eine globale und eine lokale Variable mit demselben Namen haben oder dass die Daten eines Spielers einen anderen Spieler stören?

Wie bei vielen Bugs machen Sie wahrscheinlich irgendwo eine ungültige Annahme. Oder möglicherweise mehr als eine. Mehrere interagierende Fehler sind schwer zu erkennen.

  • Hat jede Variable eine Beschreibung? Und können Sie eine Gültigkeitserklärung definieren?
    Wenn Sie diese nicht hinzufügen, durchsuchen Sie den Code, um festzustellen, ob jede Variable korrekt verwendet wird. Fügen Sie diese Behauptung hinzu, wo immer es Sinn macht.

  • Der Vorschlag, Lots Assertion hinzuzufügen, ist gut: Der erste Ort, an dem Sie sie einfügen, ist an jedem Funktionseinstiegspunkt. Überprüfen Sie die Argumente und alle relevanten globalen Zustände.

  • Ich verwende viel Protokollierung zum Debuggen von lang laufenden / asynchronen / Echtzeit-Codes.
    Fügen Sie bei jedem Funktionsaufruf erneut ein Protokoll ein.
    Wenn die Protokolldateien zu groß werden, können die Protokollierungsfunktionen Dateien umbrechen / wechseln usw.
    Am nützlichsten ist es, wenn die Protokollnachrichten mit der Tiefe des Funktionsaufrufs eingerückt werden.
    Die Protokolldatei kann zeigen, wie sich ein Fehler ausbreitet. Nützlich, wenn ein Teil des Codes etwas nicht Richtiges tut, das als Bombe mit verzögerter Aktion fungiert.

Viele Menschen haben ihren eigenen eigenen Protokollierungscode. Ich habe irgendwo ein altes C-Makro-Log-System und vielleicht eine C ++ - Version ...

andy256
quelle
3

Alles, was in den anderen Antworten gesagt wurde, ist sehr relevant. Eine wichtige Sache, die von ddyer teilweise erwähnt wird, ist, dass das Verpacken von malloc / free Vorteile hat. Er erwähnt ein paar, aber ich möchte dem noch ein sehr wichtiges Debugging-Tool hinzufügen: Sie können jeden Malloc / Free zusammen mit ein paar Callstack-Zeilen (oder den vollständigen Callstack, wenn Sie das möchten) in eine externe Datei einloggen. Wenn Sie vorsichtig sind, können Sie dies schnell erledigen und in der Produktion einsetzen, wenn es darum geht.

Nach allem, was Sie beschreiben, ist meine persönliche Vermutung, dass Sie einen Verweis auf einen Zeiger irgendwo im freigegebenen Speicher behalten und möglicherweise einen Zeiger freigeben, der Ihnen nicht mehr gehört oder in den Sie schreiben. Wenn Sie mit der obigen Technik einen zu überwachenden Größenbereich ableiten können, sollten Sie in der Lage sein, die Protokollierung erheblich einzugrenzen. Andernfalls können Sie, sobald Sie feststellen, welcher Speicher beschädigt wurde, das Malloc / Free-Muster, das dazu geführt hat, ganz einfach aus den Protokollen ermitteln.

Ein wichtiger Hinweis ist, dass, wie Sie bereits erwähnt haben, das Problem durch Ändern des Speicherlayouts möglicherweise ausgeblendet wird. Es ist daher sehr wichtig, dass Ihre Protokollierung keine Zuordnungen (wenn Sie können!) Oder so wenig wie möglich macht. Dies verbessert die Reproduzierbarkeit, wenn es sich um Speicher handelt. Es hilft auch, wenn es so schnell wie möglich geht, wenn das Problem mit Multithreading zusammenhängt.

Es ist auch wichtig, dass Sie Zuordnungen aus Bibliotheken von Drittanbietern abfangen, damit Sie sie auch ordnungsgemäß protokollieren können. Man weiß nie, woher es kommen könnte.

Als letzte Alternative können Sie auch einen benutzerdefinierten Zuweiser erstellen, in dem Sie für jede Zuordnung mindestens zwei Seiten zuweisen und die Zuordnung aufheben, wenn Sie sie freigeben (die Zuordnung an einer Seitengrenze ausrichten, eine Seite zuvor zuweisen und sie als nicht zugänglich markieren oder die Zuordnung aufheben) am Ende einer Seite zuordnen und eine Seite danach zuordnen und als nicht zugänglich markieren). Stellen Sie sicher, dass Sie diese virtuellen Speicheradressen für einige Zeit nicht für neue Zuordnungen verwenden. Dies bedeutet, dass Sie Ihren virtuellen Speicher selbst verwalten müssen (reservieren und verwenden, wie Sie möchten). Beachten Sie, dass dies Ihre Leistung beeinträchtigt und abhängig von der Anzahl der Zuweisungen, die Sie vornehmen, möglicherweise erhebliche Mengen an virtuellem Speicher belegt. Um dies zu mildern, ist es hilfreich, wenn Sie 64-Bit verwenden und / oder den Bereich der Zuweisungen verringern können, für die dies erforderlich ist (basierend auf der Größe). Valgrind tut dies möglicherweise bereits, aber es ist möglicherweise zu langsam, um das Problem damit zu lösen. Wenn Sie dies nur für einige wenige Größen oder Objekte tun (wenn Sie wissen, welche, können Sie den speziellen Zuweiser nur für diese Objekte verwenden), wird die Leistung nur minimal beeinträchtigt.

Nicholas Frechette
quelle
0

Versuchen Sie, einen Überwachungspunkt für die Speicheradresse festzulegen, bei der es abstürzt. GDB bricht bei der Anweisung ab, die den ungültigen Speicher verursacht hat. Mit der Rückverfolgung können Sie dann Ihren Code sehen, der die Beschädigung verursacht. Dies ist möglicherweise nicht die Ursache für Beschädigungen, aber das Wiederholen des Überwachungspunkts bei jeder Beschädigung kann zur Ursache des Problems führen.

Da die Frage übrigens mit C ++ gekennzeichnet ist, sollten Sie gemeinsame Zeiger verwenden, die sich um den Besitz kümmern, indem Sie einen Referenzzähler beibehalten und den Speicher sicher löschen, nachdem der Zeiger den Gültigkeitsbereich verlässt. Verwenden Sie sie jedoch mit Vorsicht, da sie in einer seltenen Verwendung von zirkulären Abhängigkeiten zu einem Deadlock führen können.

Mohammad Azim
quelle