it-swarm.com.de

Speicherbeschädigung debuggen

Zunächst einmal ist mir klar, dass dies keine perfekte Frage im Q & A-Stil mit einer absoluten Antwort ist, aber ich kann mir keinen Wortlaut vorstellen, damit 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 veröffentliche.

Im letzten Monat habe ich einen ziemlich alten Servercode (mmorpg) neu geschrieben, um moderner und einfacher zu erweitern/modifizieren zu können. Ich habe mit dem Netzwerkteil begonnen und eine Drittanbieter-Bibliothek (libevent) implementiert, um Dinge für mich zu erledigen. Bei all dem Re-Factoring und den Codeänderungen habe ich irgendwo eine Speicherbeschädigung eingeführt, und ich hatte Mühe herauszufinden, wo dies geschieht.

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

Ich habe es bisher versucht:

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

Tools zur Code-Analyse, nämlich Coverity und Cppcheck. Während sie auf einige .. böse und Edge-Fälle im Code hinwiesen, gab es nichts Ernstes.

Den Prozess aufzeichnen, bis er mit gdb abstürzt (via undodb) und mich dann rückwärts arbeiten. Dies/klingt/scheint machbar zu sein, aber entweder stürze ich gdb mit der Auto-Vervollständigungs-Funktion ab oder ich 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, wozu ein Zeiger ursprünglich gehört/wo er zugewiesen wurde, wodurch die meisten Verzweigungsprobleme beseitigt würden. Ich kann Valgrind jedoch nicht mit Undodb ausführen, und ich bin der normale GDB-Datensatz ungewöhnlich langsam (wenn das sogar 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 dachte darüber nach, vielleicht einen Entwickler einzustellen, der mit mir Codeüberprüfungen/Debugging durchführt, aber ich kann es mir nicht leisten, zu viel Geld in sie zu stecken, und ich würde nicht wissen, wo ich jemanden suchen soll, der bereit wäre, für wenig zu arbeiten. zu kein Geld, wenn er das Problem nicht findet oder jemand überhaupt qualifiziert ist.

Ich sollte auch beachten: Ich bekomme normalerweise konsistente Rückverfolgungen. Es gibt einige Stellen, an denen der Absturz passiert, hauptsächlich im Zusammenhang mit der Beschädigung der Socket-Klasse. Sei es ein ungültiger Zeiger, der auf etwas zeigt, das kein Socket ist, oder die Socket-Klasse selbst wird (teilweise?) Mit Kauderwelsch überschrieben. Obwohl ich vermute, dass es dort am meisten abstürzt, da dies eines 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 lang beschäftigt (ein und aus, 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 mir einfach nicht überlegen, was ich sonst noch tun soll, um das Problem zu finden.

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

Bearbeiten:

Einige Spezifikationen für den Fall, dass es wichtig ist:

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

Die Codebasis besteht aus ca. 150.000 Zeilen

Bearbeiten als Antwort auf david.pfx Beitrag: (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üllkippen der letzten Abstürze herumliegen

Sind 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) würde es immer in einer Item-Timer-Methode hängen bleiben. Grundsätzlich hat ein Artikel eine bestimmte Zeit, nach der er abläuft und aktualisierte Informationen an den Kunden sendet. Der ungültige Socket-Zeiger würde sich in der (soweit ich das beurteilen kann) Player-Klasse befinden, die größtenteils damit zusammenhängt. Ich habe auch viele Abstürze in der Bereinigungsphase nach dem normalen Herunterfahren, bei dem alle statischen Klassen zerstört werden, die nicht explizit zerstört wurden (__run_exit_handlers Im Backtrace). Meistens mit std::map Einer Klasse, aber das ist wohl 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.

Ist es haufenbezogen?

Es ist völlig Heap-bezogen (ich habe den Stack Guard von gcc aktiviert und das hat nichts aufgefangen).

Tritt die Beschädigung nach einem free() auf?

Sie müssen etwas näher darauf eingehen. Meinen Sie damit, dass Zeiger auf bereits frei gewordene Objekte herumliegen? Ich setze jeden Verweis auf null, sobald das Objekt zerstört wird. Wenn ich also nicht irgendwo etwas verpasst habe, nein. Das sollte sich in valgrind zeigen, was es aber nicht tat.

Gibt es etwas Besonderes am Netzwerkverkehr (Puffergröße, Wiederherstellungszyklus)?

Der Netzwerkverkehr besteht aus Rohdaten. Also Char-Arrays, (u) intX_t oder gepackte (um Padding-Strukturen zu entfernen) Strukturen für komplexere Dinge, jedes Paket hat einen Header, der aus einer ID und der Paketgröße selbst besteht, die gegen die erwartete Größe validiert wird. Sie sind ungefähr 10-60 Bytes groß, wobei das größte (internes 'Bootup'-Paket, das beim Start einmal ausgelöst wird) eine Größe von einigen MB hat.

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

Ich hatte einmal einen Absturz im Zusammenhang mit std::map Korruption, jede Entität hat eine Karte ihrer "Ansicht", jede Entität, die sie sehen kann und umgekehrt, ist darin. Ich habe vorne und danach einen 200-Byte-Puffer hinzugefügt, ihn mit 0x33 gefüllt und vor jedem Zugriff überprüft. Die Korruption ist einfach auf magische Weise verschwunden. Ich muss etwas bewegt haben, wodurch es etwas anderes korrumpiert hat.

Strategische Protokollierung, damit Sie genau wissen, was gerade passiert ist. Fügen Sie der Protokollierung hinzu, wenn Sie sich einer Antwort nähern.

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 Produktionssoftware vorstellen, die das tun.

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

Parallelität: Threading, Rennbedingungen usw.

Es gibt einen MySQL-Thread, der "asynchrone" Abfragen ausführt. Dies ist jedoch alles unberührt und gibt Informationen nur über Funktionen mit allen Sperren an die Datenbankklasse weiter.

Unterbricht

Es gibt einen Interrupt-Timer, der verhindert, dass er blockiert, der nur abgebrochen wird, wenn ein Zyklus 30 Sekunden lang nicht abgeschlossen wurde. Dieser Code sollte jedoch sicher sein:

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

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

ereignisse/Rückrufe/Ausnahmen: Status oder Stapel werden unvorhersehbar beschädigt

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

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

Ich hatte ein paar Edge-Fälle im Zusammenhang damit. Das Trennen eines Sockets, während Pakete noch verarbeitet werden, führte zum Zugriff auf ein Nullptr und dergleichen. Diese waren jedoch bisher leicht zu erkennen, da jede Referenz direkt bereinigt wird, nachdem der Klasse selbst mitgeteilt wurde, dass sie fertig ist. (Die Zerstörung selbst wird von einer Schleife behandelt, die alle zerstörten Objekte in jedem Zyklus löscht.)

Abhängigkeit von einem asynchronen externen Prozess.

Möchtest du das näher erläutern? Dies ist etwas der Fall, der oben erwähnte Cache-Prozess. Das Einzige, was ich mir vorstellen kann, ist, dass es nicht schnell genug fertig ist und Mülldaten verwendet, aber das ist nicht der Fall, da dies auch das Netzwerk verwendet. Gleiches Paketmodell.

23
Robin

Es ist ein herausforderndes Problem, aber ich vermute, dass die Abstürze, die Sie bereits gesehen haben, noch viel mehr Hinweise enthalten.

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

Dinge, die wir in ähnlichen Situationen verwendet haben.

  • Viele, viele Produktionssicherungen. 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. Fügen Sie der Protokollierung hinzu, wenn Sie sich einer Antwort nähern.

Können Sie in Ihrer Verzweiflung den Status speichern und automatisch neu starten? Ich kann mir ein paar 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 schwerwiegende unbestimmte Fehler wie diese nicht allzu häufig sind und es nicht viele Dinge gibt, die sie (normalerweise) verursachen können. Sie beinhalten:

  • Parallelität: Threading, Rennbedingungen usw.
  • Interrupts/Ereignisse/Rückrufe/Ausnahmen: Status oder Stapel werden unvorhersehbar beschädigt
  • Ungewöhnliche Daten: ungewöhnliche Eingabedaten/Timing/Zustand
  • Abhängigkeit von einem asynchronen externen Prozess.

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

21
david.pfx

Verwenden Sie eine Debugging-Version von malloc/free. Wickeln Sie sie ein und schreiben Sie gegebenenfalls 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 kostenlos überprüft werden. Dies fängt die meisten Pufferüberläufe und mehrere oder unerwünschte "freie" Fehler ab.

Eine der heimtückischsten Korruptionsquellen ist die weitere Verwendung eines Teils, nachdem es befreit wurde. Free sollte den freigegebenen Speicher mit einem bekannten Muster füllen (traditionell 0xDEADBEEF). Es ist hilfreich, wenn zugewiesene Strukturen ein "Magic Number" -Element enthalten und vor der Verwendung einer Struktur großzügig nach der entsprechenden Magic Number suchen.

6
ddyer

Alles, was in den anderen Antworten gesagt wurde, ist sehr relevant. Eine wichtige Sache, die teilweise von ddyer erwähnt wird, ist, dass das Verpacken von malloc/free Vorteile hat. Er erwähnt einige, aber ich möchte dem ein sehr wichtiges Debugging-Tool hinzufügen: Sie können jedes Malloc/Free zusammen mit einigen Zeilen Callstack (oder dem vollständigen Callstack, wenn Sie sich darum kümmern) in einer externen Datei protokollieren. Wenn Sie vorsichtig sind, können Sie dies ganz einfach machen und in der Produktion verwenden, wenn es darum geht.

Nach Ihrer persönlichen Vermutung ist meine persönliche Vermutung, dass Sie möglicherweise irgendwo auf einen Zeiger verweisen, um Speicher freizugeben, und am Ende möglicherweise einen Zeiger freigeben, der Ihnen nicht mehr gehört, oder darauf schreiben. Wenn Sie mit der oben beschriebenen Technik auf einen zu überwachenden Größenbereich schließen können, sollten Sie in der Lage sein, die Protokollierung erheblich einzugrenzen. Andernfalls können Sie, sobald Sie herausgefunden haben, welcher Speicher beschädigt wurde, das malloc/free-Muster, das dazu geführt hat, ganz einfach aus den Protokollen herausfinden.

Ein wichtiger Hinweis ist, dass das Problem möglicherweise durch Ändern des Speicherlayouts ausgeblendet wird. Es ist daher sehr wichtig, dass Ihre Protokollierung keine Zuordnungen (wenn Sie können!) Oder so wenig wie möglich vornimmt. Dies hilft bei der Reproduzierbarkeit, wenn es sich um einen Speicher handelt. Es ist auch hilfreich, 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. Sie wissen nie, woher es kommen könnte.

Als letzte Alternative können Sie auch einen benutzerdefinierten Zuweiser erstellen, in dem Sie mindestens 2 Seiten für jede Zuordnung zuweisen und diese beim Freigeben aufheben (die Zuordnung an einer Seitengrenze ausrichten, eine Seite zuvor zuweisen und als nicht zugänglich markieren oder die Ausrichtung ausrichten) am Ende einer Seite zuordnen und eine Seite danach zuweisen und markieren ist als nicht zugänglich). Stellen Sie sicher, dass Sie diese virtuellen Speicheradressen mindestens einige Zeit nicht für neue Zuordnungen wiederverwenden. Dies bedeutet, dass Sie Ihren virtuellen Speicher selbst verwalten müssen (reservieren Sie ihn und verwenden Sie ihn nach Ihren Wünschen). Beachten Sie, dass dies Ihre Leistung beeinträchtigt und möglicherweise erhebliche Mengen an virtuellem Speicher benötigt, je nachdem, wie viele Zuordnungen Sie ihm zuführen. Um dies zu mildern, ist es hilfreich, wenn Sie in 64-Bit arbeiten und/oder den Bereich der Zuweisungen reduzieren können, die dies benötigen (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 Größen oder Objekte tun (wenn Sie wissen, welche, können Sie den speziellen Allokator nur für diese Objekte verwenden), wird sichergestellt, dass die Leistung nur minimal beeinträchtigt wird.

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 gesucht werden muss, sowie Werkzeuge und Techniken.

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

Von den Symptomen

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

  • ein damit verbundenes 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 die eines anderen Spielers stören?

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

  • Hat jede Variable eine Beschreibung? Und können Sie eine Gültigkeitsbehauptung 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, eine Lots-Behauptung hinzuzufügen, ist gut: Der erste Ort, an dem sie platziert werden, ist an jedem Funktionseinstiegspunkt. Überprüfen Sie die Argumente und alle relevanten globalen Status.

  • Ich verwende viel Protokollierung zum Debuggen von lang laufenden/asynchronen/Echtzeitcodes.
    Fügen Sie bei jedem Funktionsaufruf erneut einen Protokollschreibvorgang ein.
    Wenn die Protokolldateien zu groß werden, können die Protokollierungsfunktionen Dateien umschließen/wechseln/usw.
    Es ist am nützlichsten, wenn die Protokollnachrichten mit der Tiefe des Funktionsaufrufs eingerückt sind.
    Die Protokolldatei kann zeigen, wie sich ein Fehler ausbreitet. Nützlich, wenn ein Code etwas nicht ganz Richtiges tut, das als Bombe mit verzögerter Aktion fungiert.

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

3
andy256

Versuchen Sie, einen Überwachungspunkt für die Speicheradresse festzulegen, an der es abstürzt. GDB wird bei der Anweisung unterbrochen, 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 Quelle der Beschädigung, aber das Wiederholen des Überwachungspunkts bei jeder Beschädigung kann zur Ursache des Problems führen.

Übrigens, da die Frage mit C++ gekennzeichnet ist, sollten Sie gemeinsam genutzte Zeiger verwenden, die den Besitz gewährleisten, indem Sie einen Referenzzähler beibehalten und den Speicher sicher löschen, nachdem der Zeiger den Gültigkeitsbereich verlassen hat. Verwenden Sie sie jedoch mit Vorsicht, da sie bei einer seltenen Verwendung von zirkulären Abhängigkeiten zu einem Deadlock führen können.

0
Mohammad Azim