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_handlers
im Backtrace). Meistens handelt es sich um std::map
eine 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::map
Korruption, 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.
/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.