debugging

Fehler werden immer wieder gemacht. Auch oder gerade in der Software-Entwicklung. Hier werden sie liebevoll als Bugs, zu deutsch Käfer, bezeichnet. Es gibt nicht umsonst vor der Veröffentlichung viele Testphasen, beginnend mit dem Alpha-Stadium. Ziel hier ist eine grundlegend stabile Version zu erschaffen. In der anschließenden Betaphase werden noch die kleineren Probleme behoben. Und genau das bedeutet Debugging, Fehler in einer Software beheben.

In diesem Artikel lernst Du die Entstehung kennen, bekommst Hintergrundwissen zu den Fehlerarten und welche Fehler in der Praxis häufig auftreten. Außerdem lernst Du natürlich Methoden zur Fehlersuche kennen. Auch zur Problemlösung gibt es ein paar allgemeine Hinweise. Zusammengefasst ein umfassender Ratgeber, der Dir zeigt wie Du Deine Software debuggen kannst.


Bugs in der IT-Welt

Warum spricht man von Bugs bzw. Debugging?

Es gibt mehrere Theorien zur Entstehung des Begriffs. Auch schon vor der Blüte der Computer wurde er verwendet. Im Bezug auf die IT-Welt ist aber der wortwörtliche Käfer (englische Bezeichnung Bug) vermutlich Namensgeber. Die ersten Computer bestanden unter anderem aus Röhren. Und in genau einer solchen wurde am 9. September 1947 eine Motte gefunden und entfernt. Sie hatte einen Kurzschluss verursacht. Im Logbuch wurde sie zusammen mit dem Hinweis “First actual case of bug being found.” eingeklebt. Dieses historische Dokument wird heute in Museen ausgestellt. Das Synonym für Computerfehler war geboren. Der Begriff Debugging, das Ausmerzen von Fehlern, leitet sich von der Bezeichnung Bug ab.

Was ist ein Patch?

Bei Debugging bereits ausgelieferter Software wird häufig auch vom sogenannten “Patchen” gesprochen. Mit einem Patch wird ein Problem behoben. Übersetzt bedeutet es soviel wie flicken. Es geht in der Regel um Bugfixes. Über Patches werden häufig Sicherheitsprobleme und schwere Fehler behoben. Im Gegensatz zu kompletten Updates kann ein Patch auch ohne Veröffentlichung einer neuen Softwareversion herausgegeben werden. Das geht gerade bei komplexer Software schneller.

Es gibt zwei Varianten. Bei einem übersetzten Programm, wie einem Spiel oder auch einem Betriebssystem wie Microsoft Windows, enthält der Patch neue Versionen für betroffene Dateien. Es werden alle Dateien ausgetauscht, die sich geändert haben. Bei einem Patch für Quellcode enthält der Patch nur Informationen über die Änderungen an konkreten Dateien. Ein solcher Bugfix kann unter Unix zum Beispiel mit dem Tool diff erzeugt und mit dem Programm patch angewandt werden.

Bevor ein Patch ausgeliefert wird, führt kein Weg am Debugging vorbei. Erst muss ein Fehler gefunden und beseitigt werden.

Programmfehler in der Realität

Zu den bekannten Fehlern gehören Bugs, die durch ihre Tragweite Schlagzeilen gemacht haben. Sehr prominentes Beispiel ist die Ariane 5 Rakete. Die musste bei ihrem Jungfernflug schon wenige Kilometer nach dem Start gesprengt werden. Bei der Typumwandlung ist es zu einem Fehler gekommen! Der Schaden wird mit etwa 370 Millionen US-Dollar beziffert. Für Aufregung hat auch das sogenannte Jahr-2000-Problem gesorgt. Technisch gesehen wurde befürchtet, dass beim Jahreswechsel die Datumsangaben nach dem Schema 01.01.’00 sich auf 1900 beziehen würden. Dadurch wurde gerade bei der Bahn, an Flughäfen und zum Beispiel Banken die Angst vor Chaos groß. Zeitlicher Aufwand mit einem geschätzten Gegenwert von knapp 800 Mrd. US-Dollar hat die Prüfung und Beseitigung verursacht. Es gibt natürlich noch weitere, das sind aber zwei sehr bekannte Vertreter.

Bananaware?

Wieso kommt eigentlich soviel fehlerhafte Software auf den Markt? Eine berechtigte Frage. Immerhin müssten doch Branchenriesen in der Lage sein, das Debugging aus dem Effeff zu beherrschen! Außerdem gibt es zum Beispiel mit dem Test-Driven-Development auch Methoden, die Fehlerquoten senken. Komplett fehlerfrei ist schwer, aber deutlich stabiler als es teilweise den Eindruck erweckt, ginge es vielleicht schon.

Bösen Zungen unterstellen zumindest teilweise wirtschaftliches Kalkül. Je eher Software ausgeliefert wird, desto eher kann sie Umsätze generieren. Das hat den Begriff Bananaware geprägt, der auch Bananenprinzip genannt wird. Das Motto lautet “reift beim Kunden”.


Die Fehlerarten

Syntaktische Fehler

Dir begegnen beim Programmieren automatisch die syntaktischen Fehler zuerst. Wenn Du eine Klammer vergisst, zu viele davon setzt oder das Semikolon am Ende vergisst, ist das ein Fehler in der Syntax. Alle ungültigen Sonderzeichen oder fehlenden Zeichen spielen in diese Kategorie. Mit einer modernen Entwicklungsumgebung (IDE) ist es jedoch relativ einfach solche Probleme zu beheben. Meist teilt Dir der Editor bereits mit, wenn etwas nicht stimmt.

Lexikalische Fehler

Eine vergleichbare Rolle spielen lexikalische Fehler. Am Ende erzeugen auch sie eine ungültige Syntax. Sie entstehen, wenn Du unbekannte Schlüsselwörter eintippst. Eine Funktion oder Klasse wurde nicht geladen? Dann ist das Symbol (der Name) vielleicht nicht bekannt. Du tippst bei einer Schleife wihle statt while? Auch das erzeugt einen lexikalischen Fehler.

Logische Fehler

Wenn von Bugs gesprochen wird, sind logische Fehler gemeint. Die IDE zeigt keine Fehler an, die Syntax ist korrekt und das Programm startet. Wenn es sich unerwartet verhält, Fehler macht oder die Software ganz abstürzt, ist ein Denkfehler im Code. Die sind in der Regel am schwersten zu finden. Da die Software grundsätzlich funktioniert. Oft zeigen sie sich auch nur unter bestimmten Bedingungen. Das macht die Fehlersuche noch schwerer.

Seiteneffekte

Streng genommen sind Seiteneffekte keine eigene Fehlerklasse. Es sind auch logische Fehler. Sie entstehen aber auf einem speziellen Weg. Der Namen deutet es bereits an. Du änderst an einer Stelle etwas. An ganz anderer Stelle tritt nun durch Deine Änderung ein Seiteneffekt auf. Mit einem Beispiel wird es verständlicher.

Du betreibst eine Webseite. Sie ist öffentlich erreichbar. Unter anderem gibt es auch einen RSS-Feed. Jetzt wird die Webseite eines Tages für den öffentlichen Zugriff gesperrt. Ein Login ist vorab notwendig. In der entwickelten Lösung sorgst du dafür, dass alle nicht autorisierten Zugriffe zur Loginmaske gelenkt werden. Dabei vergisst Du eine Ausnahme für Ressourcen wie den RSS-Feed. Schon hättest Du einen Seiteneffekt erzeugt, da Du eine Auswirkung nicht komplett bedacht hast.

Performanceprobleme

Performanceoptimierung ist eine echte Herausforderung. Der genaue Flaschenhals ist nicht immer sofort sichtbar. Echte Messwerte erhältst Du nur mit einem Profiler. Der misst wie oft eine bestimmte Methode oder Funktion aufgerufen werden, wie lange die Aufrufe benötigen und womit die Anwendung die meiste Zeit verbraucht. Schließlich kannst Du an genau den Stellen ansetzen und etwas verbessern.

Neben der Laufzeit ist die zweite Disziplin das Aufspüren von Speicherproblemen. Auch dazu brauchst Du spezielle Tools, wie bspw. die Instruments von Xcode. Die helfen Dir bei einer genauen Analyse. Wenn Du nachvollziehen kannst welche Objekte den meisten Speicher belegen, kannst Du deren Speicherverbrauch sowie die Anzahl im Speicher weiter reduzieren.


Typische Fehler in der Praxis

Durch Null teilen

Es ist nicht zulässig durch Null zu teilen. Dies wird in vielen Sprachen entsprechend mit einer Fehlermeldung quittiert. In der Regel geschieht dies nicht bewusst. Stattdessen tritt so eine Rechnung auf, wenn der Teiler eine Variable ohne Wert ist. Ursachen gibt es viele. Vielleicht hat der Benutzer eine ungültige Eingabe gemacht? Vielleicht auch gar keine? Bevor Berechnungen anstehen, müssen die Werte daher unbedingt validiert werden.

division-by-zero

Zugriff auf nicht gesetzte Variablen

Eine Variable muss initalisiert werden, bevor Du auf sie zugreifst und Werte ausliest. Das gilt natürlich für primitive Datentypen. Aber auch bei der Objektorientierung spielt das eine große Rolle. Du musst Objekte validieren, bevor Du Methoden aufrufst. Ansonsten kann es schnell passieren, dass Du nicht vorhandene Objekte verwendest. Das führt zum Fehler.

not-existing-variable-java

Endlosschleifen

Auch Endlosschleifen treten häufig auf. Das sind Konstrukte, bei denen die Schleifenbedingung immer erfüllt ist. Vielleicht wird der Schleifenzähler nicht erhöht? Oder die Abbruchbedingung innerhalb der Schleife kann durch logische Fehler niemals erreicht werden?

Ungültige Arrayindizes

Zugriffe auf ungültige Arrayindizes können sehr schnell auftreten. Sprachen wie Java fangen dies ab und melden sich. In C kannst Du Dir mit solchen Fehlern schnell in den Fuß schießen. Daraus kann im schlimmsten Fall sogar eine Sicherheitslücke entstehen. Gerade Einsteigern vergessen häufig, dass der erste Index nicht 1 sondern 0 ist. Dementsprechend ist das fünfte Elemente auch unter Index 4 abgelegt. Wenn das fünfte Elemente gleichzeitig das letzte ist, führt dies zu Fehlern.

array-index-out-of-bounds

Nicht erfüllte Bedingungen

Auch if-Bedingungen können durchaus fehleranfällig sein. Schnell wird statt größer oder gleich (a >= b) nur auf größer als (a > b) geprüft. Schon führt das zu einem logischen Fehler, da ein Wert nicht berücksichtigt wird. In einem umfassenden Programm kann es schwer sein, solche Probleme aufzuspüren.

Deadlocks

Bei parallel laufenden Prozessen oder Threads, die auf mehreren CPU-Kernen gleichzeitig laufen, kann es zu Deadlocks kommen. Dabei wartet Thread A auf die Eingabe von Thread B, während Thread B auf Thread A wartet. Die Situation lässt sich nicht mehr auflösen. Sie muss durch geschickte Programmierung verhindert werden.


Debugging: Die Methoden zur Problemsuche

Es gibt mehrere Ansätze beim Debugging einer Software. Manche sind effektiver, manche etwas umfangreicher. Hier eine Übersicht über die wichtigsten Methoden.

Print-Debugging

Das Print-Debugging, auch als Printf-Debugging bekannt, hat den Namen von der Datenausgabe. Es ist die einfachste Form, die jeder Einsteiger zu Beginn automatisch ausprobiert. Die Grundidee ist in bestimmten Codebereichen eine Ausgabe zu erzeugen. Zum einen kann sichergestellt werden, dass der Quellcode überhaupt ausgeführt wird. Außerdem kann auch der Inhalt von Variablen geprüft werden. Hier ein Java-Beispiel, in dem die Idee ansatzweise deutlich wird:

Die Nutzdaten in dem Programm sind in den Variablen myResult und myNumbers gespeichert. Die Variable myId würde in der Praxis mit Sicherheit aus einer Benutzersession gelesen, die nach erfolgreichem Login erzeugt wird. Die Idee ist einfach zur Laufzeit den Inhalt von Variablen auszuwerten. Dafür gibt es viele verschiedene Gründe.

Debugger

Noch effektiver ist die Arbeit mit einem Debugger. Der ist in der Regel in einer IDE wie Xcode, PHPStorm, IntelliJ IDEA oder auch dem Android Studio integriert. Im Editor wird ein sogenannter Breakpoint gesetzt. An dem unterbricht der Debugger die Programmausführung. Das ermöglicht Dir zu prüfen ob der Code ausgeführt wird. Wenn dies nicht der Fall ist, greift der Breakpoint nicht. Außerdem kannst Du an der Stelle den Inhalt jeglicher Variablen genau untersuchen. Dieser Weg ist um ein vielfaches eleganter als über das Print-Debugging.

Jemandem das Problem erklären

Es gibt häufig Fehler, die scheinbar wie in einer Zwiebel versteckt liegen. Schon nach kurzer Zeit bist Du betriebsblind und übersiehst die Stelle. Bis Du solche Probleme alleine löst, vergeht einige Zeit. Zeichnet sich keine Lösung ab, lohnt es sich immer um Hilfe zu fragen. Das bedeutet nicht, dass Dir jemand die Arbeit abnehmen muss. Ein spannender Effekt lässt sich quer durch die IT-Welt beobachten. Sobald Du jemandem das Problem schilderst und Deinen Quellcode erklärst, fällt es Dir häufig von ganz allein auf. In 75-80% der Fälle reicht es bereits, wenn Dir jemand zuhört.

Rubber Duck Debugging

Rubber duck debugging

Im wunderbaren Buch The pragmatic Programmer wird diese Idee noch ein Stück weiter getrieben. Da es oft reicht, wenn Du das Problem und Deinen Code Zeile für Zeile erklärst, brauchst Du dafür nicht zwingend einen menschlichen Zuhörer. Wenn Du gar nicht im Team arbeitest, gerade im Homeoffice bist oder kein Kollege Zeit hat, kannst Du es auch einer kleine Gummiente erklären. Der helfende Effekt tritt auch dabei auf.

Post-mortem Debugging

Bei der Post-mortem Fehlersuche wird ein Bug nach dem Crash einer Software gesucht. Dazu werden unter bestimmten Bedingungen “Aufzeichnungen” vom Betriebssystem gespeichert, die core dumps oder memory dumps. Das sind Auszüge, die Speicherinhalt zur Laufzeit und den Aufrufen vor dem Absturz enthalten. So kann auch nach Programmende nachvollzogen werden, was zum Absturz geführt hat.

Automatisierte Software-Tests

Zum Debugging sind auch automatisierte Software-Tests sinnvoll, gerade wenn sich Fehler nur selten reproduzieren lassen. Dabei werden möglichst alle Klassen und deren Methoden über automatische Tests aufgerufen. Beim Aufruf werden Parameter übergeben und geprüft, ob das erwartete Ergebnis zurückgegeben wird. Ein zweiter Aufruf mit falschen Parametern kann außerdem sicherstellen, dass eine Fehlermeldung erzeugt wird. Auf diesem Weg lassen sich Fehlerquellen eingrenzen. Zugegeben ist dieser Ansatz aber sehr aufwendig. Er würde bedeuten, dass die Tests für eine bereits fertige Software geschrieben werden.


Die Problemlösung

Eine pauschale Formel zur Fehlersuche gibt es nicht. Je nach Sprache unterscheidet sich die Art und Weise der Fehlersuche. Aber das grundsätzliche Vorgehen ist fast immer gleich.

Fehler reproduzieren

Das wichtigste ist zunächst den Fehler zu erzeugen. Wenn Du durch gezielte Aktionen das Fehlverhalten auslösen kannst, nennt man dies den Bug reproduzieren. Das klingt logisch. Bei einem Absturz nach dem Klick auf den Speichern-Button ist das auch ein klarer Fall. Es gibt aber oft Fehler, die sich besser verstecken. Häufig treten sie nur unter bestimmten Bedingungen auf. Dann wird es schwieriger. Sobald Du den Fehler immer erzeugen kannst. Geht es auf die Suche.

Fehlermeldung vorhanden?

Die besten Fehler hinterlassen irgendwo eine Spur. Vielleicht bekommt der Benutzer eine Meldung? Vielleicht gibt es einen Logeintrag? Vielleicht wird auch eine E-Mail an den Administrator gesendet? Oder es wird ein Core Dump gespeichert? Möglichkeiten gibt es viele. Sobald Du den Fehler erzeugen kannst, musst Du an Details kommen. Sollte es noch keine Meldungen geben, versuch sie zu erzeugen. Vielleicht kannst Du Logging aktivieren? Oder zum Beispiel eine geworfene Exception im Code besser verarbeiten?

Den Fehler aufspüren

Im dritten Schritt musst Du den Fehler suchen. Das geht manchmal schnell, wenn klar ist was nicht funktioniert. Ansonsten musst Du Dich Schritt für Schritt herantasten. Grenz den Bereich logisch ein, versuch Teile der Software als Fehlerquelle auszuschließen. Gibt es Module oder Plugins, die Du deaktivieren kannst? Versuch nicht benötigte Funktionalität vorübergehend auszuklammern. Dann scheidet sie als Ursache aus.

Ein guter Startpunkt ist häufig bereits in der Fehlermeldung zu finden. Ansonsten kannst Du auch im Quellcode nach der Fehlermeldung suchen. Im besten Fall findest Du so die Stelle. Dort suchst Du dann gezielt nach dem Problem.

Fehler angemessen beheben

Es klingt fast zu offensichtlich. Natürlich muss der Fehler behoben werden. Keine neuen Fehler machen ist noch wichtiger als die reine Lösung des Problems. Das ist häufig schwerer als gedacht. Gerade Seiteneffekte können bei einer komplexeren Software schnell auftreten. Hier sind die automatisierten Tests wieder zu erwähnen. Sobald Du einen Fehler gelöst hast, kannst Du über die Tests sicherstellen, dass die sonstige Funktionalität noch gegeben ist.


Fehler vermeiden

Planung ist die halbe Miete

Fehlerfreie Software entsteht durch eine gut durchdachte Architektur. Wenn die im Vorfeld festgelegt und geplant wird, ist sie an die Anforderungen angepasst. Außerdem kann die Struktur später erweitert werden, ohne dass die Anpassung einer Neuentwicklung gleich kommt. Nur so bleibt eine Software dauerhaft erweiterbar. Wichtig ist nicht nur die jetzigen Anforderungen im Sinn zu behalten. Du musst einen Schritt weiter denken. So könntest Du zum Beispiel in einer Webseite Dich fest verdrahtet an MySQL koppeln. Besser wäre aber abstrakter zu denken. Wenn eine zusätzliche Abstraktion eingeführt wird, kannst Du die Datenbankanbindung generisch halten. So bleibst Du offen für andere Datenbanksysteme.

Test-Driven-Development

Automatisierte Tests sind sinnvoll. Wenn die Software erst einmal über Tage und Wochen entwickelt wurde, ist es aber schwer eine hohe Abdeckung (Coverage) zu erreichen. Das ist der prozentuale Anteil an Methoden, die durch automatische Tests aufgerufen werden. Das Test-Driven-Development eignet sich hervorragend um Fehler, ganz speziell Seiteneffekte, bereits im Vorfeld zu vermeiden. Auch hier wird auf automatisierte Tests gesetzt. Allerdings ist die Reihenfolge umgekehrt. Bevor Du eine Klasse und Methoden schreibst, legst Du bereits das Testszenario fest. Dies schlägt automatisch zu Beginn fehl. Immerhin hast Du die Methode ja noch nicht entwickelt. Nachdem Du fertig bist, muss der Test erfolgreich sein. Auf diesem Weg erreichst Du eine maximale Abdeckung.

Pair Programming

Ebenfalls hilfreich ist das Pair Programming. Dazu sitzen zwei Entwickler am gleichen Arbeitsplatz. Es wird gemeinsam programmiert. Die entstehende Lösung wird in direkter Kooperation umgesetzt. Es gilt das Vier-Augen-Prinzip. Die sehen einfach mehr als zwei. Das ist nicht nur für komplexere Aufgaben ein wunderbarer Weg. Pair-Programming eignet sich auch um Einsteiger, Auszubildende oder neue Kollegen einzuarbeiten.

Code Reviews

Bei Code-Reviews werden fertige Codeabschnitte im Nachhinein besprochen. Ähnlich wie beim Pair-Programming steht das Vier-Augen-Prinzip im Mittelpunkt. Dabei erklärst Du Deine Lösung einem anderen Entwickler. Der sollte sich in dem jeweiligen Projekt bereits auskennen. Zum einen werden Fehler aufgedeckt, die Du übersehen hast. Noch wichtiger ist aber, dass auch die Architektur noch einmal begutachtet wird. Vielleicht hast Du auch eine Methode implementiert, die es bereits gab? Auch Redundanz lässt sich mit Code Reviews vermeiden. Ein vergleichsweise einfaches Mittel, dass enorm viel Potenzial bietet.

Bugs, die Menschen gefährden

Spannend wird es, wenn von Software sogar Menschenleben abhängig sind. Stell Dir vor, Du schreibst Software für ein Passagierflugzeug. Durch einen Fehler in der Steuerungslogik verliert der Pilot die Kontrolle über die Maschine. Sie kann nicht mehr gesteuert werden. Das wäre fatal! Ein ganz neuer Ansporn, beim Testen und Debuggen doppelt aufzupassen? Ganz sicher.

Überragende Qualität ist keine Frage übermenschlicher Fähigkeiten der einzelnen Entwickler. Es ist viel mehr eine Frage von Prozessen und Qualitätskontrollen. Es müssen mehrere Menschen den Code sehr intensiv kontrollieren. Auch die Tests werden sehr genau definiert und von wieder anderen Personen gewissenhaft durchgeführt. Dies sind Maßnahmen, die natürlich für jede Software wünschenswert währen. Allerdings ist es natürlich ein unglaublicher Kostenfaktor. Wenn unmittelbar das Leben von Menschen an der Logik hängt, ist aber der Preis(hoffentlich) zweitrangig.

Schreib einen Kommentar

Your email address will not be published.