Tutorial Debugging
Inhaltsverzeichnis
- 1 Debugger benutzen und andere praktische Tipps
- 2 Ich habe nichts geändert, aber jetzt funktionierts nicht mehr
- 3 Einführung in den Delphi Debugger
- 4 Grundkurs im Käfer entfernen
- 4.1 Kuscheltier Zweckentfremdung
- 4.2 Bugs aufspüren
- 4.2.1 Böses Blut: logische Fehler
- 4.2.2 Verdammte Strichpunkte
- 4.2.3 Scopes missachtet/Versteckter Seiteneffekt
- 4.2.4 Initialisieren
- 4.2.5 Memory is running out
- 4.2.6 Speicherlecks
- 4.2.7 Sieh, dort - oder Zeigerprobleme die Erste
- 4.2.8 It's magic, real magic - oder Zeigerprobleme die Zweite
- 4.2.9 Knack knack: Access Violation bei Arrays oder wer kann hier nicht zählen?
- 5 Weitere Funktionen des Delphi Debugger
Debugger benutzen und andere praktische Tipps
Ich muss im DGL und anderen Foren immer wieder feststellen, dass Fragen zu Problemen gestellt werden, die sich mit etwas Know How stressfrei bewältigen ließen. Die Titel solcher Themen sind meist: "Hilfe, kann Fehler nicht finden", "Kann mir einer bei dem Problem helfen?", "Was ist falsch an diesem Code", ...
Delphi und die meisten anderen Entwicklungsumgebungen bringen bereits das meiste mit, was man zum Debuggen braucht. Der Delphi Debugger ist meiner Meinung nach sogar einer der besten.
Ich habe nichts geändert, aber jetzt funktionierts nicht mehr
Dieses Problem läuft einem so verdammt oft über den Weg. Ob ihr es glaubt oder nicht, meistens hat man doch etwas geändert, man erinnert sich nur nicht, oder verbindet die Änderung nicht mit seinem Problem.
In der Professionellen Programmierung gibt es ein einfaches System, das einem solche Probleme vom Leibe hält, warum sollte man also als Hobbyprogrammierer darauf verzichten? Ich rede von Version Controll Systemen. Ein freies, das auch gut in die Delphi IDE integriert ist Free VCS. Leider wurde dieses Projekt eingestellt, wird aber von den Jedis weitergeführt: Jedi VCS. Solange Jedi VCS allerdings noch keine stable Versionen veröffentlicht hat, langt Free VCS mit Sicherheit.
Nun was bringen einem VCS Systeme? Für den einzelnen Hobbyprogrammierer eher unwichtig, für Gruppen allerdings umso wichtiger: Jeder Nutzer kann sich Dateien vom Server laden und den Schreibzugriff für sich reservieren lassen. Wenn er fertig ist, schickt er die veränderte Datei zurück an den Server. Der Server Archiviert die neue Datei. Er überschreibt aber die Datei nicht wild, sondern behält immer eine ganze Reihe an Dateien. Und hier liegt der Vorteil für den Einzelnen: Man kann die unterschiedlichen Dateiversionen mit dem beiliegendem Diff Tool des jeweiligen VCS Systems einfach vergleichen und bekommt je nach System gleich eine hübsche Gegenüberstellung der beiden Versionen aus dem Archiv. Wenn man sich mal etwas Code richtig zerschossen hat, kann man leicht mal eine ältere Version aus dem Archiv nehmen und damit weiterarbeiten.
Tipp:Man lasse den Server am besten auf einem anderen Rechner laufen, oder die Archivdatenbank zumindest auf einer anderen Platte speichern. Stürzt einem jetzt die Entwicklungsmaschine ab, bekommt man meist weniger Probleme mit Zerschossenen Dateisystemen, Dateien selbst, etc, denn die liegen ja gut geschützt anderswo vergraben. Auto Backups der Archive sind auch zu empfehlen.
Einführung in den Delphi Debugger
Delphi bietet einen der besten Debugger überhaupt. Alle Delphi Programmierer haben diesen Vorteil gegenüber dem Rest der Programmierer, sofern sie überhaupt wissen, wie man alles aus ihm rausholt. Zum guten Debuggen bracht man v.a.: Sehr viel Übung und Erfahrung, etwas Geduld und Zeit, Kenntnisse im Umgang mit dem Debugger und natürlich ein Problem.
Ich kann aus euch keinen Top Debugger machen, das ist eure Sache. Ich kann euch aber Tipps im Umgang geben und die Werkzeuge des Delphi Debuggers erläutern. Danach seid ihr gefragt:
Üben, üben, üben... Vielleicht löst ihr dann ja auch mal unsere Probleme, wenn wir unsere Fehler nicht finden. Und lasst euch nicht entmutigen, wenn ihr mal einen Nachmittag nur am Debuggen seid, das passiert manchmal und liegt meist an irgendeiner saudummen Zeile, nach der man wahrlich lange sucht.
Gute Debugging Fähigkeiten verleiten einen aber auch... Einfach etwas Code ohne groß Nachzudenken hinklatschen, dann mit dem Debugger drüber und alle Fehler ausmerzen - das ist nicht nur schlechter Stil, sondern frisst meist mehr Zeit, als sich vorher 10 Minuten mehr Zeit zu nehmen um etwas zu überlegen und so erst gar keine Probleme zu bekommen. Kurz: Wer nur noch mit dem Debugger rumwurschtelt macht was falsch.
Wichtige Voreinstellungen
Alle meine bisherigen Delphi- und Pascal-Installationen brachten nie anständige Einstellungen mit, mit denen man ordentlich Fehler finden würde. Leider verändert sich die Position der Schalter mit neuen Versionen immer mal wieder. Dieser Beschreibung liegt Delphi 5 zugrunde, wer Optionen nicht findet, suche doch bitte erst einmal in der Delphi Hilfe danach, bevor er zu fragen anfängt.
Unter Projekt/Optionen/Compiler/Laufzeitfehler finden sich ein paar Optionen, die tunlichst aktiviert werden sollten:
Wenn man nun beispielsweise über die Grenzen eines Arrays hinauslangt, wird man von Delphi freundlich darauf aufmerksam gemacht: Datei:rangecheck.gif
procedure TForm1.FormCreate(Sender: TObject); var P : Array[0..3] of Integer; I : Integer; begin Randomize; for i := 0 to 4 do P[i] := 15- Random(30); end;
Der Debugger springt an die fehlerhafte Zeile und man kann das Problem lösen, aber dazu später mehr. In diesem Falle ist der Fehler natürlich sehr offensichtlich und auch ohne Debugger problemlos zu finden.
Haltepunkte, Haltebedingungen und andere nützliche Werkzeuge
Eines der wichtigsten Werkzeuge beim Debuggen sind die
sog. Breakpoints. Werden sie erreicht, springt der Debugger
am Breakpoint in den Code und ihr könnt euch genau anschauen
was passiert. Beginnen wir zuerst mit der Bedienung:
1. Compiliert eure Quelldateien (Strg+F9)
2. Editor anschauen:
Die blauen Punkte am Rand bedeuten, dass an dieser Zeile ein Breakpoint gesetzt werden kann. Eine Rote Zeile steht für einen aktiven Breakpoint. Klickt man auf einen blauen Punkt, so setzt man einen Breakpoint.
Startet man nun das Programm, hält das Programm immer an, wenn der Breakpoint im Programmablauf erreicht wird. Aber Breakpoints können noch einiges mehr. Klickt man mit rechts auf den roten Punkt, kann man einen Breakpoint deaktivieren (Aktiviert abwählen -> Breakpoint wird grün umgefärbt), auf diese Weise findet man ihn schnell wieder, falls man ihn nochmal braucht, denn so befindet er sich weiterhin in der Liste der Haltepunkte (CTRL+ALT+B):
Sie zeigt eine schöne Übersicht aller gesetzten Breakpoints. Das kleine linke Symbol zeigt den Zustand an, grau z.B. für deaktiviert. Andere wichtige Information, wie Quelldatei, Lage, Bedingungen usw. kann man auch gleich einsehen.
Breakpoints können aber nicht nur stupide den Code anhalten, man kann sie auch so bearbeiten, dass sie nur in bestimmten Situationen ausschlagen, das ist besonders wertvoll, wenn man mit Schleifen arbeitet, die mehrere hundert Durchläufe machen.
Hit-Count Breakpoints
Klickt man rechts auf einen Breakpoint, so findet man im erscheinenden Menü auch den Eintrag "Haltepunkt Eigenschaften".
Klicken wir einmal darauf: Datei:breakpoint settings.gif
Gibt man eine Zahl in der Zeile Durchlaufzähler ein, so schlägt der Debugger nur an, wenn der Breakpoint ebenso oft aufgerufen wurde.
Bedingte Breakpoints per Debugger
Der Debugger bietet noch die Möglichkeit, den Anschlag des Debuggers bei einem Breakpoint an eine Bedingung zu knüpfen. Eine solche Bedingung wird einfach in das Feld Bedingung eingegeben. Diese sind ganz normale Pascal Ausdrücke, wie sie auch bei boolschen Ausdrücken, etwa in if Anweisungen vorkommen.
Datei:breakpoint condition.gif
Die Bedingung für diesen Breakpoint war beispielsweise "(y=3) AND (x=4)". Wer Breakpoints mit Durchlaufzähler und Bedingung einsetzen will, lese hierzu doch bitte die Beschreibung in der Delphi Hilfe.
Bedingte Breakpoints in Eigenbau
Ein Nachteil der bedingten Breakpoints ist, dass sie besonders bei vielen Durchläufen verdammt langsam sind. Eine Möglichkeit dieses Problem zu umgehen ist, diesen selber zu bauen. Man schreibe einfach eine If Anweisung, die bei eben dieser Bedingung ihren Code ausführt. In diese If Anweisung setze man eine Zeile Code, die nichts weiter interessantes macht, aber darauf einen Breakpoint - voila.
Ungültige Breakpoints
Ob ihr es nun glaubt oder nicht, es gibt auch Breakpoints, die niemals den Code anhalten werden, sog. ungültige Breakpoints:
Der hübsche olivegefärbte Breakpoint wird niemals aufgerufen werden, das hat Delphi folgerichtig festgestellt. Solcher Code wird normalerweise als toter Code bezeichnet und im gesamten Programmablauf niemals aufgerufen. Der Delphi Compiler hat diesen Code gar hinausoptimiert.
Merke: Ungültige Breakpoints werden von Delphi nur als solche markiert, wenn man die Quelldatei nach dem Setzen des Breakpoints einmal compiliert hat!
Tipp: Meistens hat man in seinem Code irgendwo einen Fehler, wenn Delphi Code hinausoptimiert, da er nie aufgerufen werden wird. Entsprechend genau sollte man die Sache untersuchen. Aber auch ohne ungültige Breakpoints lassen sich solche Stellen identifizieren: Hat man den Code compiliert, so erscheinen am linken Rand nicht die gewöhnlichen blauen Punkte. Manchmal hilft einem bereits das, sein Problem zu lokalisieren.
Bringen wir Bewegung in die Sache
Bislang bringen euch Breakpoints noch nicht viel. Sie halten nur das Programm an, was auf Dauer ein wenig nervig sein dürfte. Ein Debugger muss also noch ein wenig mehr beherrschen als nur bei Breakpoints anzuhalten.
Vorwärts... Langsam, schnell, turbo?
Der Debugger bietet die Möglichkeit unterschiedlich schnell durch den Code zu manövrieren. Beispielsweise gäbe es da den Turbo(F9 oder Start), dieser bringt den Debugger dazu die Programmausführung wieder vollständig aufzunehmen. Das ganze läuft dann weiter, bis das Programm beendet wurde, oder ein Haltepunkt anschlägt. Manchmal ist das allerdings ein wenig zu viel des guten.
Aber bevor wir loslegen, sollten wir einmal einen Breakpoint setzen und das Programm laufen lassen:
Datei:breakpoint condition.gif
Der kleine grüne Pfeil an der linken Seite zeigt die Stelle an, an der der Debugger gerade angehalten hat. Ist in dieser Zeile kein Breakpoint, so ist die Zeile blau.
Um nun weiter durch den Code zu schreiten, muss man einfach F7 oder F8 drücken. Mit F8 bleibt man in der aktuellen Routine und bewegt sich Zeilenweise vorwärts. F7 dagegen veranlasst den Debugger in etwaige Funktionsaufrufe hineinzuspringen und gibt einem damit die Möglichkeit diese auch einer genaueren Betrachtung zu unterziehen. Voraussetzung ist natürlich, man hat den Quelltext der Datei in einem für den Debugger erreichbarem Verzeichnis und die Datei wurde mit debug Informationen kompiliert.
Ansichtssache
Wer hat noch nie jemanden bei einer Tätigkeit beobachtet? Etwa, wie er sich etwas zu essen macht? Seh ich da keine Hand? Lügner! Jedenfalls sind wir alle Voyeure. Leute die Debugger benutzen sogar in höchster Ausprägung, denn die schauen ständig Variablen auch bei ihren intimsten Tätigkeiten zu. Egal wie unwohl sie sich dabei fühlen, das muss einfach sein, ob sie wollen oder nicht - auch beim Akt der Vereinigung ;-).
Mit Delphi geht das ganz einfach, man braucht nicht einmal ein Fernglas. Wenn das Programm vom Debugger angehalten wurde, bewege man im Quelltext einfach die Maus über eine Variable und siehe da: Die enthaltene Information erscheint. Manchmal will man aber auch etwas genauer hinschauen, etwa den Inhalt eines Arrays an der Stelle, die gerade gelesen wird. Hier langt es, die entsprechende Stelle zu markieren:
Sicher wird es ein paar freuen - das ist nicht das einzige Guckloch, das Delphi einem auf unsere Variablen eröffnet. Es gibt noch einiges mehr zu Fenstern und zur nahen Betrachtung, z.B. die unterschiedlichen Varianten des Waches Fensters(CTRL+ALT+W). In dieses kann man einfach ein paar Variablennamen eingeben und schon kann man die besten Blicke auf seine Lieblinge erhaschen:
Delphi hält diese Liste wenn möglich auf dem aktuellsten Stand. Eine neue Variable kann mittels INS oder CTRL+A eingefügt werden. Manche Variablen sind aus dem aktuellen Kontext heraus nicht zu erreichen, sondern nur die, die in der derzeitigen Funktion sichtbar sind.
Tricky ist auch die Watchlist für die lokalen Variablen(CTRL+ALT+L). In sie kann man von Hand keine Variablen einfügen, das übernimmt Delphi für einen - man findet darin die zu einer Funktion lokalen Variablen, man muss also nicht für jede zu debuggende Funktion im Watchfenster neue Variablen eintragen. In vielen Fällen ist die Liste der lokalen Variablen vollkommen ausreichend.
Grundkurs im Käfer entfernen
Jetzt kennen wir alle Elementaren Fähigkeiten des Delphi Debuggers. Wesentlich wichtiger ist jedoch, damit umgehen zu können. Jeder Programmierer entwickelt mit der Zeit seinen eigenen Programmier- und Debugstil. Das bedeutet allerdings, dass ich hier nur Tipps zum Debuggen geben kann, nicht jedoch Erfolgsrezepte. Debugging besteht nun mal aus Intuition und Erfahrung, aber man kann sich die Sache ganz schön erleichtern.
Worauf man immer achten kann ist, wie schwer man sich das Fehlerfinden macht. Muss man älteren Code debuggen, oder den eines anderen Programmierers, so sind Kommentare mit das wertvollste, was es gibt. Andererseits sollte man bei seinem Code auch darauf achten, dass er sich später leicht debuggen lässt, etwa seine Programme so Modular designen, dass man später leicht einen einzelnen Bereich des Programms alleine begutachten kann, ohne gleich einen riesigen Schwanz an anderen Funktionen ständig mitrumschleppen zu müssen - sprich: Debuggen wird umso leichter, je besser man sich auf den Code konzentrieren kann, in dem der Fehler vermutet wird, ohne dass man durchgehend von anderem Code abgelenkt wird. Das entsprechende Stichwort ist strukturierte Programmierung.
Daneben sollte man in Funktionen sog. Assertions einfügen. Sie sind Annahmen. Etwa, dass die Funktion niemals mit dem Parameter I < 0 aufgerufen werden darf, oder ihr Ergebnis niemals 0 sein darf, da es dann an anderer Stelle knackt, oder Speicherbereiche überschrieben werden, die eben nicht überschrieben werden sollten. Entsprechende Annahmen kann man mit dem Befehl Assert(Bedingung) in seinen Code einpflegen. Ist die Bedingung nicht erfüllt, wird man darauf aufmerksam gemacht.
Hat man das alles gemacht, kann man seine Algorithmen leicht mit Spezialfällen füttern und damit ausprobieren, ob sie auch das richtige Ergebnis liefern. Manchmal hilft mir bereits ein Taschenrechner um die Funktion des Übels aufzuspüren. Manchmal versucht man auch einen Fehler zu finden, in einem Bereich, in dem gar kein Fehler ist. Das passiert leicht, wenn man sich nicht ganz im klaren ist, was genau passiert oder man einfach etwas verwirrt ist. Hier hilft ein kleiner Trick:
Kuscheltier Zweckentfremdung
Kuscheltiere sind leidensfähige Zuhörer, sie quengeln nicht und motzen nicht rum, wenn man sie mit langweiligen Dingen quält. Das macht sie zu einem idealen Gesprächspartner. Probiert es aus: Erklärt eurem Kuscheltier, dass ihr euch neben den Monitor gesetzt habt euer Problem in allen Details, was ihr eigentlich haben wolltet und eure Vermutungen. Versucht euch in anständiger Sprache auszudrücken - was das bringt fragt ihr? Ganz einfach, drückt man seine Gedanken in Worten aus, so ordnen sich die Gedanken im Hirn und die Lösung springt einen meist regelrecht an. Und hier ist der nächste Vorteil eines Kuscheltiers - auch wenn der Fehler noch so lächerlich ist, das Kuscheltier wird euch nicht auslachen. Es wird auch nicht zu grinsen anfangen, wenn ihr voll Freude in die Luft springt, weil ihr den lang gesuchten Fehler endlich gefunden habt.
Sollte euch einmal kein Kuscheltier zur Verfügung stehen, versucht euch an Eltern, Kollegen, Lehrern... Die sind zwar nicht ganz so gut geeignet, aber wenn man einen hartnäckigen Fehler sucht, muss man auch mal Opfer bringen ;-)
Bugs aufspüren
Nun wenn das nicht schon geholfen hat, den Fehler zu erkennen und zu beheben, beginnt nun die richtige Arbeit. Das finden des Fehlers.
Böses Blut: logische Fehler
Der Begriff logischer Fehler ist nicht ganz klar definierbar. Sie lassen sich jedoch ganz gut eingrenzen: Ihre Ursache liegt nicht in Fehlerhaften Code an sich(etwa eine wichtige Zeile vergessen), sondern darin, dass eine Annahme oder Idee einfach Falsch ist oder nicht funktionieren kann. Genau an dieser Stelle bietet sich die Kuscheltiermethode an. Andere Tipps sind: Eine Nacht drüber schlafen und erneut nachdenken und alle Annahmen genau überprüfen. Auch den Code ab und zu im Einzelschritt durchgehen, irgendwann fällt einem der Fehler hoffentlich auf. Und eine kleine Warnung: nach einem logischen Fehler sucht man schon mal eine ganze Nacht lang durch, ohne ihn zu finden - leider.
Verdammte Strichpunkte
Delphi ermöglicht Anweisungen, die einfach leer sind und nur aus einem Semikolon bestehen. Das führt schon mal zu Problemen:
var i, k : Integer; ... Result := 1; for i := k downto 2 do ; begin Result := Result * i end; ...
Anders als wahrscheinlich erwartet, wird dieser Code nicht die Fakultät aus k errechnen. Stattdessen wird die leere Anweisung ";" (k-1)mal ausgeführt. Ein Entfernen des Strichpunktes und alles läuft wieder wie gewollt. Finden würde man diesen Fehler im Einzelschritt: Die rechnende Zeile würde nämlich einfach nur 1 mal ausgeführt werden.
Scopes missachtet/Versteckter Seiteneffekt
Manchmal benutzt man Variablen aus einem falschen Gültigkeitsbereich, die eigentlich für einen anderen Zweck bestimmt waren. Besonders häufig beschädigt man Zählvariablen, etwa das von vielen sehr geliebte i als Variablenname:
var i : Integer; procedure DoSomething(k : Integer); begin i := k; while i >= 1 do begin Write('*'); Dec(i) end; WriteLn end; begin for i := 1 to 10 do DoSomething(i); end.
Dieses Programm wird nie ein Ende finden und nur massenhaft Sternchen auf den Bildschirm zeichnen, da die Variable i durch DoSomething immer schön zurückgesetzt wird und die for Schleife damit nie ans Ende kommt. Beim Debuggen sollte einem auffallen, dass nach DoSomething die Zählvariable i immer einen falschen Wert hat. Ein entsprechender Programmierstil würde helfen, solche Probleme in Zukunft zu vermeiden: Variablen die nur lokal benötigt werden, wirklich nur für den entsprechenden Abschnitt zu definieren und damit den Zugriff durch andere Funktionen zu unterbinden.
Initialisieren
Für hochgradig interessante Effekte sorgen immer wieder nicht initialisierte Variablen, die dadurch irgendwelche zufälligen Werte enthalten und dadurch den Ablauf des Programms erheblich stören können. Wer Variablen verwendet, denen nie ein Wert zugewiesen wurde, darf sich nicht wundern - das ganze kann aber richtig knackig werden: Manche Debugger initialisieren neue Variablen während dem Debuggen mit 0, während ohne Debugger die Variable eben nicht initialisiert wird - spätestens wenn sich ein Programm im Debugger anders verhält, als wie in freier Wildbahn, sollten die Alarmglocken läuten. Wer sich angewöhnt, nachdem er eine Variable definiert hat, ihr auch gleich eine Initialisierungszeile zu spendieren wird dieses Problem nie wieder haben - es sei denn er hat sie mit einem falschen Wert initialisiert... Glücklicherweise wird man von Delphi über Hints auf so etwas meist aufmerksam gemacht - den Grund solcher Warnmeldungen sollte man in jedem Falle einmal nachgehen.
Memory is running out
Manchmal verhalten sich Programme ganz seltsam - eigentlich laufen sie fehlerfrei. Wenn man allerdings länger mit ihnen arbeitet, fliegen einem Exceptions um die Ohren, das Betriebssystem beschwert sich über zuwenig virtuellen Speicher, etc. Ein guter Zeitpunkt sich das Programm Memproof zu besorgen und nach Stellen zu suchen, an denen zwar Speicher allokiert wird, allerdings nirgends wieder zurückgegeben wird.
Speicherlecks
Tropfende Speicherlecks sind eine andere Bösartigkeit. Manchmal langt man durch sie in den Speicher eines anderen Programms - unter Windows NT gibt es einen Abbruch, unter Win9x wird einfach überschrieben, was daraus auch immer am Ende resultiert. Das wäre noch nicht einmal das Problem, denn dann findet man den Fehler meist schnell.
Unangenehmer wird es schon, wenn man seinen eigenen Speicher überschreibt. Das Betriebssystem beschwert sich nicht, aber die Werte der Variablen, die an entsprechenden Speicherstellen standen sind nun mit unsäglichem Unsinn überschrieben. Besonders anfällig sind Funktionen wie Move oder FillChar, aber auch Arrays, wenn die Bereichsprüfung deaktiviert ist. Besonders bei Objekt ähnlichen Konstrukten überschreibt man manchmal den Pointer, nicht aber die Daten(Siehe später: It's magic, real magic - oder Zeigerprobleme die Zweite).
Sieh, dort - oder Zeigerprobleme die Erste
Das Initialisierungsproblem muss bei den Zeigern nocheinmal aufgewärmt werden. Besonders bei Bäumen oder verketteten Listen. Sobald man sich mit New Heap geholt hat, sollte man alle Zeigerreferenzen als erstes auf nil setzen, damit es richtig knallt und man sich nicht im Wirrwarr des Speichers irgendwo verläuft(Wer Windows 9x benutzt hat wird den schweren Ausnahmefehler 0E kennen: Da konnte wohl jemand nicht Programmieren und wollte auf einen nil Pointer zugreifen. Man wird bei Zugriffen auf die Adresse 0x00000000 also auf einen Fehler aufmerksam gemacht, auch unter Win NT, Linux und anderen - unterschiedlich freundlich natürlich). Tut man das nicht, läuft man Gefahr, seinen Baum über seine eigentlichen Grenzen hinweg abzugrasen, ohne dass eine Exception ausgelöst wird -> meist werden die Programme dadurch unheimlich langsam. Bei Verwendung von Pointern und Debuggern gilt also: neue Pointer immer auf ihre Richtigkeit hin überprüfen und unbedingt verifizieren, ob auch alle Referenzen auf die Speicherstelle entfernt wurden, wenn der Speicher freigegeben wurde.
It's magic, real magic - oder Zeigerprobleme die Zweite
Objektorientierte Programmierung. Ein schönes Wort. Jedoch mit vielen Tücken. Eine Variable auf ein Form : TForm ist z.B. nichts anderes als ein Pointer. Delphi versteckt das Pointerproblem vor dem Entwickler recht gut, so dass er nicht ständig und überall dereferenzieren muss. Das hat aber auch seine Nachteile, besonders bei Objekten, die nicht nach Objekten aussehen(oder gar keine sind, sich aber so ähnlich verhalten):
procedure LoadCompressedTGA(...); var ... ColorBuffer : Array of Char; begin with TGA do begin ... SetLength(ColorBuffer, BytesPerPixel); ... BlockRead(fTGA, ColorBuffer, BytesPerPixel); // Try to read 1 pixel ... end; (*LoadCompressedTGA*)
Hier liegt die Krux im BlockRead begraben. Was daran falsch ist? ColorBuffer ist als solches nur ein Pointer, der auf die Informationen des dynamischen Arrays zeigt. Wendet man das Blockread nun tatsächlich so an, wird nur der Pointer überschrieben, nicht aber die Daten, die
überschrieben werden sollten. Wir geben BlockRead also den falschen Pointer mit - wir müssten eigentlich diesen "Pointer" derart dereferenzieren, dass er auf die Daten zeigt, was z.B. mittels ColorBuffer[0] ginge. Dieser Fehler wird sehr häufig begangen, besonders im Umgang mit Dateien. Allerdings sind nicht nur dynamische Arrays betroffen, sondern die von Delphi als Standard eingestellten Huge Strings und Ähnliche.
Knack knack: Access Violation bei Arrays oder wer kann hier nicht zählen?
Wenn einem Access Violations im Zusammenhang mit Arrays um die Ohren fliegen, so hat das so gut wie immer einen Grund: Man hat über die Arraygrenzen hinausgelangt. Dies passiert besonders unerfahrenen Programmierern häufig, die ihre Arrays immer bei 1 beginnen lassen, dann aber mit dynamischen Arrays arbeiten und das Selbe erwarten. Allerdings sind sie nicht die Einzigen, die von diesem Problem betroffen sind.
Wenn es an einer Zugriffszeile knallt und die Bereichsprüfung anspringt, so sollte man sich zuerst einmal über die Arraygrenzen schlau machen. Die Zugriffsvariablen nach dem Unfall im Debugger zu betrachten hilft meist nicht mehr weiter, da diese bei dieser Aktion oft zerstört werden. Man setzte sich also seine Breakpoints, u.U. mit Durchlaufzähler oder Bedingung und beobachte, ob die Zugriffsvariablen die Bereichsgrenzen überschreiten. Wenn ja ist eigentlich der Grund schon gefunden, die Stelle müsst ihr dann aber selbst lokalisieren, etwa eine fehlerhafte for oder while Schleife, oder einen fehlerhaften Aufruf einer Funktion mit falschen Parametern(remeber: Assertions, High, Low).
Diese Tipps sollten mal über die gröbsten Probleme hinweghelfen. Wer noch gute, standard Probleme kennt, soll sie mir möglichst ausformuliert einfach mailen, ich werde mich bemühen diese dann in den bestehenden Text einzuarbeiten(Credits gibts natürlich).
Weitere Funktionen des Delphi Debugger
Das wichtigste ist erledigt, aber der Delphi Debugger kann noch einiges mehr. Ein paar interessante Funktionen möchte ich euch nicht vorenthalten, ihr solltet aber auch mal einen Blick in die online Hilfe werfen, um auch das letzte herausholen zu können.
Aufruf Stack
An das Fenster des Aufrufstacks gelangt man über die Kombination CTRL+ALT+S:
In ihm kann man die Funktionsaufrufe bis hin zur aktuellen Funktion verfolgen und durch einen beherzten Doppelklick auf die jeweilige Funktion auch zur Sprungstelle gelangen und untersuchen. Nebenbei zeigt er wenn möglich auch die Aufrufparameter an.
Thread Status
An das Thread Status Fenster kommt man mittels CTRL+ALT+T. Man kann darin den Zustand aller Threads überprüfen, etwa ob ein Thread gerade angehalten wurde.
Debug Desktops
Eigentlich kein Feature des Debuggers, sondern vielmehr der IDE sind die Debug Desktops. Delphi bietet die Möglichkeit die Positionen von Debug Fenstern, Dockingpositionen u.ä. in einzelnen Profilen abzuspeichern und leicht mittels einer Dropdownlist auszuwählen(Man schaue einmal etwas rechts von der 1. Delphi Menüleiste oder Ansicht/Desktops). Besonders auf kleineren Schirmen kann es sinnvoll sein, mehrere Profile abzulegen. Wer die wichtigsten Debug-Fenster sowieso vollständig anordnen kann, nutzt diese Funktion am besten um diese Einstellung einmal zu speichern und falls sich aus welchem Grund auch immer die Fensterpositionen verschieben, diese schnell wiederherstellen zu können.
So, das sollte es erst einmal sein. Viel Spaß beim Entwanzen
Euer Delphic