Vor kurzem haben wir einen Fehlerbericht von einem unserer Benutzer erhalten: Etwas auf dem Bildschirm wurde in unserer Software falsch angezeigt. Irgendwie konnten wir dies in unserer Entwicklungsumgebung nicht reproduzieren (Delphi 2007).
Nach einigen weiteren Untersuchungen scheint sich dieser Fehler nur dann zu manifestieren, wenn "Code-Optimierung" auf gestellt wird.
Gibt es hier jemanden mit Erfahrung in der Jagd auf einen solchen Heisenbug ? Irgendwelche spezifischen Konstrukte oder Codierungsfehler, die normalerweise ein solches Problem in Delphi-Software verursachen? Irgendwelche Orte, an denen Sie anfangen würden, zu suchen?
Ich werde auch anfangen, die ganze Sache auf die übliche Weise zu debuggen, aber irgendwelche Tipps, die speziell auf Optimierungsfehler (*) bezogen sind, wären mehr als willkommen!
(*) Hinweis: Ich will damit nicht sagen, dass der Fehler vom Optimierungsprogramm verursacht ist; Ich denke, es ist viel wahrscheinlicher, dass ein wackeliges Konstrukt im Code vom Optimierer irgendwie "über den Rand" geschoben wird.
Es scheint, dass der Fehler darauf hinausläuft, dass ein Datensatz vollständig mit Nullen initialisiert wird, wenn es keine Codeoptimierung gibt, und dass derselbe Datensatz zufällige Daten enthält, wenn -Optimierung ist. In diesem Fall scheinen die Zufallsdaten zu bewirken, dass ein Aufzählungstyp ungültige Daten enthält (zu meiner großen Überraschung!).
Es stellte sich heraus, dass die Lösung eine unitäre lokale Datensatzvariable irgendwo tief im Code einschloss. Ohne Optimierung wurde der Datensatz zurückgesetzt (heap?), Und mit der Optimierung auf wurde der Datensatz mit dem üblichen Müll gefüllt. Danke euch allen für eure Beiträge --- ich habe auf dem Weg viel gelernt!
In der Regel werden Fehler dieses Formats durch ungültigen Speicherzugriff (Lesen nicht initialisierter Daten, Ablesen des Endes eines Puffers ...) oder Thread-Race-Bedingungen verursacht.
Ersteres wird von Optimierungen beeinflusst, die dazu führen, dass das Datenlayout im Speicher neu angeordnet wird, und / oder möglicherweise durch Debug-Code, der neu zugewiesenen Speicher auf einen Wert initialisiert; verursacht den falschen Code "versehentlich arbeiten".
Letzteres wird aufgrund von Zeitpunkten beeinflusst, die zwischen den Optimierungsstufen wechseln. Ersteres ist in der Regel viel wahrscheinlicher.
Wenn Sie einen automatisierten Weg haben, frisch zugewiesenen Speicher mit einem konstanten Wert zu füllen, bevor er an das Programm übergeben wird, und dies den Absturz im Debug-Build aufhebt oder reproduzierbar macht, ist das ein guter Punkt um Dinge zu jagen.
Könnte sehr wohl ein Speicher-vs-Register-Problem sein: Programm läuft gut, basierend auf Speicherpersistenz nach einem freien.
Ich würde empfehlen, Ihre Anwendung mit FastMM4 im vollen Debug-Modus laufen zu lassen, um sich Ihrer Speicherverwaltung sicher zu sein.
Ein anderes (nicht kostenloses) Werkzeug, das in einem solchen Fall sehr nützlich sein kann, ist Eurekalog.
Eine weitere Sache, die ich gesehen habe: ein Absturz mit den FPU-Registern, die beim Aufruf von externem Code (DLL, COM ...) verpfuscht wurden, während mit dem Debugger alles in Ordnung war.
Ein Datensatz, der verschiedene Daten nach verschiedenen Compiler-Einstellungen enthält, sagt mir eines: Der Datensatz wird nicht explizit initialisiert.
Sie können feststellen, dass die Einstellung des Compiler-Optimierungsflags nur einen Faktor darstellt, der den Inhalt dieses Datensatzes beeinflussen könnte - bei nicht initialisierten Datenstrukturen können Sie sich darauf verlassen, dass Sie sich nicht auf den Initialwert verlassen können Inhalt der Struktur.
In einfachen Worten:
Klassenmitgliedsdaten werden für neue Instanzen der Klasse
Lokale Variablen (in Funktionen und Prozeduren) und Unit-Variablen werden NICHT initialisiert, außer in einigen speziellen Fällen: Interfacereferenzen, dynamische Arrays und Strings und ich denke (würde aber prüfen müssen) Datensätze, wenn sie einen oder enthalten mehr Felder dieser Typen, die initialisiert werden (Strings, Schnittstellenreferenzen usw.).
Die Frage ist, wie gesagt, ein wenig irreführend, denn es scheint, dass Sie Ihren "Heisenberg" ziemlich leicht gefunden haben. Jetzt geht es darum, wie man damit umgeht, und die Antwort besteht einfach darin, Ihren Datensatz explizit zu initialisieren, so dass Sie nicht auf das Verhalten oder die Nebenwirkung des Compilers angewiesen sind, das sich manchmal um Sie kümmert und manchmal nicht.
Besonders in rein einheimischen Sprachen, wie Delphi, solltest du mehr als vorsichtig sein, die Freiheit nicht zu missbrauchen, irgendetwas zu irgendetwas zu machen. IOW: Eine Sache, die ich gesehen habe, ist, dass jemand die Definition einer Klasse (z. B. aus dem Implementierungsabschnitt in RTL oder VCL) in seinen eigenen Code kopiert und dann Instanzen der ursprünglichen Klasse in seine Kopie überträgt. Jetzt, nach dem Upgrade der Bibliothek, woher die ursprüngliche Klasse kam, könnten Sie alle möglichen seltsamen Dinge erleben. Wie in falsche Methoden oder Pufferüberläufe zu springen.
Es gibt auch die Angewohnheit, Vorzeichen mit Vorzeichen als Zeiger zu verwenden und umgekehrt. (Anstelle von Kardinal) Das funktioniert einwandfrei, solange Ihr Prozess nur 2 GB Adressraum hat. Aber starte mit dem Schalter / 3GB und du wirst eine Menge Apps sehen, die verrückt werden. Jene haben die Annahme von "pointer = signed integer" zumindest irgendwo gemacht. Ihr Kunde benutzt ein 64Bit Windows? Wahrscheinlich hat er einen größeren Adressraum für 32Bit-Apps. Ziemlich schwer zu debuggen ohne ein solches Testsystem zur Verfügung zu haben.
Dann gibt es Rassenbedingungen. Wie 2 Fäden haben, wo man sehr, sehr langsam ist. Damit du instinktiv davon ausgibst, dass es immer der letzte sein wird, gibt es keinen Code, der das Szenario behandelt, in dem "Captn slow" zuerst beendet wird. Veränderungen in den zugrunde liegenden Technologien können diese Annahmen sehr falsch machen, sehr schnell. Werfen Sie einen Blick auf die bevorstehende Flash-basierte Super-Mega-Fast-Server-Speicher. Systeme, die Gigabytes pro Sekunde lesen und schreiben können. Anwendungen, die davon ausgehen, dass die I / O-Vorgänge wesentlich langsamer sind als einige Berechnungen für In-Memory-Werte, werden bei dieser schnellen Speicherung leicht fehlschlagen.
Ich könnte weiter und weiter machen, aber ich muss jetzt rennen ... Prost
Wenn es sich um einen Delphi-Geschäftscode mit Dataware-Komponenten usw. handelt, trifft das möglicherweise nicht zu.
Ich schreibe jedoch einen Computer-Vision-Code, der ein wenig rechnerisch ist. Die meisten Unittests basieren auf Konsolen. Ich bin auch mit FPC beschäftigt und habe im Laufe der Jahre viel mit FPC getestet. Teilweise aus Hobby, teilweise in verzweifelten Situationen, in denen ich irgendeine Ahnung haben wollte.
Einige Standardtricks, die ich ausprobiert habe (abnehmende Nützlichkeit)
Die 2 und 3 zusammen ermöglichen es Ihnen fast, die meisten, wenn nicht alle Initialisierungsprobleme zu finden.
Versuchen Sie, irgendwelche Hinweise zu finden, und gehen Sie dann zurück zu Delphi und suchen Sie genauer, debuggen usw.
Ich erkenne, dass das nicht einfach ist. Ich habe viel Erfahrung mit FPC und musste für diese Fälle nicht alles von Grund auf neu finden. Trotzdem wäre es vielleicht einen Versuch wert und könnte eine Motivation sein, mit der Einrichtung von nicht-visuellen Systemen und Unit-Tests zu beginnen, die FPC-kompatibel und plattformunabhängig sind. Der größte Teil dieser Arbeit wird ohnehin benötigt, siehe die Delphi-Roadmap.
Bei solchen Problemen rate ich immer zur Verwendung von Logfiles.
Frage: Können Sie die falsche Darstellung im Quellcode irgendwie feststellen?
Wenn nicht, wird meine Antwort Ihnen nicht helfen.
Falls ja, überprüfen Sie die Unrichtigkeit und legen Sie den Stack in eine Logdatei, sobald Sie ihn gefunden haben. (Einzelheiten zum Dumping und zur Resymbolisierung des Stacks finden Sie in Post Mortem Debugging .)
Wenn Sie sehen, dass einige Daten beschädigt sind, aber Sie nicht wissen, wie und dann dies passiert, extrahieren Sie eine Funktion, die einen solchen Test auf Gültigkeit (mit Protokollierung, wenn fehlgeschlagen), und rufen Sie diese Funktion von immer mehr Orten Programmausführung (dh nach jedem Menüanruf). Wenn Sie einen solchen Ansatz ein paar Mal wiederholen, haben Sie gute Chancen, das Problem zu finden.
Ist dies eine lokale Variable innerhalb einer Prozedur oder Funktion?
Wenn ja, dann lebt es auf dem Stapel und wird Müll enthalten. Je nach Ausführungspfad und den Einstellungen des Compilers ändert sich der Müll, was Ihre Logik möglicherweise "über den Rand" treibt.
- Jeroen
Angesichts Ihrer Beschreibung des Problems hatten Sie, wie ich meine, nicht initialisierte Daten, die Sie ohne das Optimierungsprogramm erhalten hatten, die aber mit der Optimierung aufgehört haben.