Vier Tipps, wie Sie Software-Bugs den Garaus machen

Software Bugs

Obwohl Menschen schon seit Jahrzehnten Software entwickeln, ist und bleibt es ein langsamer und mühsamer Prozess, in den sich oft Fehler einschleichen. Trotz all der Mühe ist Software immer noch anfällig für Bugs. (Es gibt natürlich auch Ausnahmen: Zum Beispiel die Software für das Space-Shuttle-Programm: Einige Versionen davon hatte nur einen Fehler in 420.000 Zeilen Code.)

Mit anderen Worten: Egal, wie gut Ihr Coding-Team ist, irgendwann muss man immer “debuggen”. Hier finden Sie ein paar Tipps, wie Sie Bugs vernichten können:

Vermeiden Sie Flüchtigkeitsbugs

Wenn Sie nicht zum Abschluss eines jeden Projekts (oder nachdem ein User sich über Fehler beschwert hat) eine massive Bug-Jagd ausführen wollen, dann stellen Sie sicher, einen Prozess zu Beginn der Produktion zu implementieren, der Sie (hoffentlich) vor größeren Fehlern schützt. Es zahlt sich außerdem aus, den Code regelmäßig zu überprüfen, was bereits ein Teil Ihres agilen Workflows sein könnte (oder auch nicht).

Der Einfachheit halber zeige ich Ihnen ein Beispiel, das bereits 32 Jahre alt ist. Ich arbeitete damals auf einem CBM-64 an einem kurzen Teil eines 6502 Assembly-Code, der mit einem Kartuschen-Assembler entwickelt wurde. Bei der Ausführung des Maschinencodes, startete der Computer neu. Dies ist der Assembly-Code, der zwei Anweisungen enthält:

lda 16

jsr clr

Mit einfachen Worten: Es hat nicht geklappt. Und es hat einige Zeit gedauert herauszufinden, woran es lag. (Zu meiner Verteidigung: Ich hatte zu jener Zeit nur ein Jahr Erfahrung in 6502 Assembly Language.) Die Programmquelle musste nach jedem Crash komplett neu vom Band geladen werden, was die Dinge etwas verlangsamt hat. Und wie sich letztendlich herausstellte, war der Fehler ganz einfach. Hier die korrigierte Version:

lda 16

jsr cls

Es war einfach nur dumm von mir, “clr” und “cls” zu verwechseln: Aber zu meiner Verteidigung – der Kartuschen-Assembler erlaubte im Speicher immer nur eine Quelldatei. Aufgrund von Quellcode-Speicherbeschränkungen wurde das Programm auf mehrere Quelldateien aufgeteilt, wobei das Hauptprogramm Unterprogramme in allen anderen Dateien aufrief. Jede dieser anderen Dateien hatte am Anfang jeder Datei eine Jump-Tabelle zu ihren Unterprogrammen. Das Hauptprogramm hatte eine Kopie aller Jump-Tabellen der anderen Dateien sowie die physikalische Adresse, wo der Maschinencode jeder Datei in die RAM geladen wurde.

Obwohl diese Architektur vielleicht etwas eigenartig klingen mag, so war sie jedoch leicht zu programmieren. Und üblicherweise war es nicht erforderlich, andere Dateien wiederaufzubauen, um Änderungen an einer Datei durchzuführen.

Die “clr” Routine befand sich in einer Datei, die im Vergleich mit der Kopie in der Hauptdatei einen fehlenden Eintrag aus ihrer Jump-Tabelle hatte – also war sie falsch programmiert. Die “clr” sprang in den Code an einer Adresse, die Daten enthielt, und führte so zum Neustart. Die korrekte “cls” Routine war in einer Datei mit der richtigen Anzahl von Einträgen in der Jump-Tabelle. Und die zwei Unterroutinen mit ähnlichen Namen waren auch nicht gerade hilfreich.

Wie kann man also diese lästigen Fehler vermeiden? Es ist immer eine gute Idee, die Codierung so einfach wie möglich zu halten. Wenn Sie Ihren Code zu kompliziert machen, dann kreieren Sie nicht nur Bugs, sondern machen auch das Debuggen viel schwieriger.

Im Zweifelsfall sollten Sie sich stets auf bestehende Bibliotheken und Code-Snippets verlassen. Je nach verwendeter Sprache, stehen die Chancen gut, dass viele Entwickler diese Materialien bereits durchgearbeitet haben, wodurch die Chancen, schlimme Fehler zu erzeugen, reduziert werden. Eine effektive Nutzung der Bibliothek kann Ihnen viel Zeit und Mühe sparen.

Und wenn Sie Ihren Entwicklungsplan aufstellen, dann stellen Sie sicher, genügend Zeit einzuplanen, um die Dinge wirklich richtig zu machen. Dies beinhaltet, dass Kollegen Ihren Code überprüfen. Obwohl eng gesteckte Produktionszeitlinien diese Art von “Flex-Zeit” manchmal schier unmöglich machen, versuchen Sie, sich mehr Zeit zu erkämpfen. Sie können Ihren Manager darauf aufmerksam machen, dass die Zeit, die Sie heute für die präventive Bug-Jagd benötigen, später erhebliche Einsparungen in Sachen Geld (und Benutzerunzufriedenheit) zur Folge haben wird.

Stack Tracing und spezialisierte Tools

Die meisten Debugger lassen Sie die Stack-Trace verfolgen, um genau zu sehen, wo Ihr Code aufgerufen wurde. Je nach Programmiersprache können Function-Call-Parameter entweder über Register oder als Werte auf dem Stack übertragen werden. Bei 32-Bit-Intel-Chips gibt es mehrere Methoden, Parameter zu übertragen. Außerdem werden die Reihenfolge der Parameter und die Rücksendeadresse unterschiedlich gespeichert.

Ich habe mal ein paar Bibliotheken in Delphi mit Funktionen geschrieben, die aus Excel-Tabellen aufgerufen werden konnten. Der Delphi-Code musste über eine Excel4V() Funktion Anrufe in Excel machen und einen 32-Bit-Wert aus dem Stack platzieren, bevor er mit der Zeile asm pop sink;end; endete.  Es wäre ziemlich schwierig gewesen, dies ohne einen Debugger rauszufinden.

Eresult := Excel4V(xlfCaller,@xres,0,[nil]);

asm pop sink; end; // Never Remove

Ein Logging-Mechanismus wie DTrace kann Ihnen helfen, alles zu untersuchen: von Systemaufrufen bis zum virtuellen Speicher. Sie können auch Unit-Testing versuchen, wo Sie einzelne Einheiten von Quellcode kontrollieren, um zu überprüfen, ob sie effektiv arbeiten.

Manchmal stößt man auf Bugs, die dem Debugger entgangen sind. Für diese Arten von Fehlern kann man sich auf eine Vielzahl von spezialisierten Werkzeugen verlassen, einschließlich Print-Statements, Protokollierungen oder sogar stilles Outputting von Debug-Informationen.

Zum Beispiel: Windows unterstützt OutputDebugString(string msg) für C/C++ und .NET/C#. Sie können es in Ihr Programm einbauen. Wenn kein Debugger beigefügt ist, passiert nichts.

Hier ist der Code für ein sehr kleines Programm, das in Zweierschritten mit einer Dreiviertel-Sekunden-Verzögerung bis 100 zählt, und dabei für jede Loop eine Periode auf der Konsole ausgibt. Es gibt auch den Wert aus, wenn man Folgendes benutzt: System.Diagnostics.Debug.WriteLine(i) – dies sendet es zum Debugger Output. Ich habe Debugview ausgeführt – ein älteres, spezialisiertes Windows-Programm zur Überwachung der Debug-Ausgabe –, um es zu erfassen:

using System.Threading;

namespace debug

{

class Program

{

static void Main(string[] args)

 

{

for (var i = 0; i < 100; i += 2)

{

if (i%2 == 0)

{// even

System.Diagnostics.Debug.WriteLine(i);

Thread.Sleep(750);

System.Console.Write('.');

}

}

}

}

}

Ein Kunde hatte sich beschwert, dass die neueste Version eines Programms beim Start abgestürzt ist. Ich schrieb eine kurze Funktion (log(int num)), fügte log(1), log(2) calls (und so weiter) nach jedem Statement im Startup Code hinzu und schickte diese Version an den Kunden. Der Code schrieb eine Nummer in eine Text-Datei. Nachdem das Programm erneut abgestürzt war, bat ich den Kunden, sich die Datei anzusehen und mir die Zahl zu nennen, um das letzte Statement vor dem Absturz zu identifizieren.

Ziehen Sie keine voreiligen Schlüsse!

Gehen Sie nicht davon aus, dass Sie die Eigenschaften des Bugs kennen, ohne sich vorher genau zu informieren.

Mein schlimmster Bug trat in einem Delphi-3-Programm auf, das die Finanzdaten aus einer Access-Datenbank las, sie an eine Drittanbieter-DLL zur Berechnungen übergab und dann speicherte.

Eines Tages stürzte das Programm auf eigenartigste Art und Weise ab. Es fiel entweder bei einer trunc() Operation oder bei einem Datenbankzugriffsbefehl aus und killte das Programm. Sogar das Hinzufügen eines Codes für Ausnahmebehandlung (exception handling code) um den Absturzpunkt funktionierte nicht.

Ich verbrachte drei Wochen damit, den Bug zu jagen. Alles ohne Erfolg. Um die ganze Lage noch schlimmer zu machen: Das Programm stürzte nicht immer ab. Da es ein Forschungsprojekt war, ließen wir es durchgehen. Aber es ließ mir einfach keine Ruhe!

Drei Monate lang arbeitete ich mich durch den Debugger und untersuchte die Werte, die von der Drittanbieter-DLL irgendwo anders im Programm zurückgegeben wurden. Ich bemerkte, dass es in einem großen Array (5.000 x 10 doubles) zurückkehrte. Es gab es mehrere -INF-Werte. Ein Bug in der Drittanbieter-DLL speicherte manchmal -INF (negative Unendlichkeiten) im Array statt der gültigen numerischen Werte. Außerdem wichtig: Es generierte keine Division-durch-Null Ausnahmen.

Ich habe später herausgefunden, dass der Access Jet Engine-Treiber für Delphi die FPU-Ausnahmen stillschweigend ausgeschaltet hatte, so dass ein -INF auftreten und den Prozessor irgendwie beeinflussen würde (d.h. eine Zeitbombe startete). Als der Code versuchte, eine SQL-Operation oder eine trunc() durchzuführen, ging die Zeitbombe hoch. Dies geschah meist weit weg im Code, von wo aus sie erstellt wurde.

Es war ein sehr ungewöhnlicher Bug, den ich logischerweise im kürzlich ausgeführten Code angenommen hatte. Kein Wunder also, dass ich ihn nicht finden konnte! (Ich hatte mich außerdem auf den Drittanbieter-DLL-Writer verlassen, der nachher ziemlich großen Ärger bekam.) Überprüfen Sie immer Ihre Eingaben und ziehen Sie keine voreiligen Schlüsse. Schon allein diese Einstellung kann Ihnen helfen, verschiedene Bugs schneller zu beheben.

Sie sind schon ein professioneller Bug-Jäger? Die besten QA und Tester Stellen finden Sie auf Dice!

Weitere Artikel

Fünf Fähigkeiten, die jeder Superheld der mobilen Entwicklung haben sollte

 

4 Gründe, warum Hash Tables großartig sind

 

 

About David Bolton

David Bolton begann schon mit dem Programmieren, als er noch zur Schule ging und bevor es überhaupt PCs gab. Es gefiel ihm so sehr, dass er seinen Abschluss in Informatik machte. Er beschäftigt sich seit fast 35 Jahren mit dem Thema Programmieren – sowohl arbeitsbedingt als auch in seiner Freizeit. In dieser Zeit arbeitete er für Price Waterhouse, British Aerospace, MicroProse (wo Sid Maier Civilization kreiert hat), in einer gescheiterten Dotcom (HomeDirectory.com) und bei Morgan Stanley. Mehr als acht Jahre lange programmierte er Spiele, mehr als 12 Jahre Finanz-Software in London. Sein Leben beschreibt er als eine permanente Lernkurve. Er verfügt über umfangreiche Programmiererfahrung in 6502,Z80 und 68000 Assembler, Basic, Pascal, C, C++, C#, Java, PHP, SQL, Fortran, JavaScript und lernt zurzeit R. Außerdem entwickelt er heute mobile Apps in Xamarin (C#) und schreibt für Dice. Er lebt in New York (in Lincolnshire, England, nicht in den USA).
No comments yet.

Leave a Reply

WP-SpamFree by Pole Position Marketing