Debuggen
Als Debuggen bezeichnet man in der Informatik den Vorgang, in einem Computerprogramm Fehler oder unerwartetes Verhalten zu diagnostizieren und zu beheben.[1] Die Suche von Programmfehlern (sogenannten Bugs) ist eine der wichtigsten und anspruchsvollsten Aufgaben der Softwareentwicklung und nimmt einen großen Teil der Entwicklungszeit in Anspruch. Ein guter Programmierer muss daher auch das Debuggen beherrschen und umgekehrt. Nicht jeder gute Programmierer ist aber auch ein guter Debugger.[2]
Die möglichen Ansätze, um Fehler zu finden, sind vielschichtig und hängen vom vorhandenen Problem ab. Es kommen Werkzeuge für interaktives Debuggen (sogenannte Debugger) zum Einsatz, es werden Datenflussdiagramme analysiert, Unittests geschrieben, Speicherdumps analysiert und mittels Profilern die Laufzeit der Programme optimiert.
Begriffsherkunft
Die Begriffe Debuggen und Debugger stammen vom englischen Wort bug, das für „Käfer“ oder „Wanze“ steht, denn die ersten elektronischen Rechenmaschinen mussten tatsächlich von solchem Ungeziefer befreit werden. Der Begriff selber ist aber deutlich älter und taucht bereits im 19. Jahrhundert auf.
Der Begriff Debugger kann sowohl das Programm als auch die Person bezeichnen, die diese Tätigkeit ausführt.
Umfang
Computersysteme und die darauf laufende Software sind nie perfekt. Irgendjemand möchte immer, dass die Software mehr macht, als sie derzeit kann, oder dass sie etwas anders macht als aktuell. Daher muss bei der Meldung oder Beobachtung eines Programmfehlers zunächst einmal beurteilt werden, ob und inwieweit das beobachtete Verhalten ein Fehler darstellt und gegebenenfalls mit welcher Methode der Fehler am besten analysiert werden sollte. Aus diesem Grund wird auch oft nicht mehr von „Fehlern“ oder „Bugs“ gesprochen, sondern es werden neutralere Begriffe verwendet, etwa der englische Ausdruck „Issue“ (Aufgabe, Sachverhalt).
Der erste Schritt beim Debuggen ist in der Regel das unerwünschte Verhalten zu reproduzieren. Das kann teilweise bereits ein erhebliches Problem darstellen, etwa wenn der Fehlerbericht – beispielsweise von einem Kunden – ungenau („ich kann mich nicht mehr an die Fehlermeldung erinnern“) oder viel zu allgemein ist („das blöde Programm funktioniert nicht!“). Andere Fehler sind aus technischen Gründen schwer zu reproduzieren, etwa wenn in einer Multithreading-Umgebung der Fehler aufgrund einer zeitlichen Abhängigkeit zwischen zwei Threads nur zufällig auftaucht. Solche Fehler können manchmal verschwinden, wenn man versucht, sie einzugrenzen. Sie werden deswegen auch Heisenbugs (nach Werner Heisenberg) genannt. In weiteren Fällen kann das Fehlverhalten subjektiv sein („das Programm ist viel zu langsam“) oder das Laufzeitverhalten hängt von unbekannten Faktoren der Hardware ab.
Ist der Fehler analysiert beziehungsweise gefunden, muss beurteilt werden, wie er zu beheben ist. Auch das ist nicht immer einfach, denn es ist etwa möglich, dass einige Programmteile nur deshalb korrekt funktionieren, weil der Fehler vorhanden ist. Wenn man ihn nun behebt, können andere Funktionen plötzlich nicht mehr funktionieren. Oder es stellt sich heraus, dass der Fehler Folge eines Designfehlers ist, also bei der Entwicklung komplett vergessen wurde, dass die zum Fehler führende Situation überhaupt auftreten kann. In solchen Fällen kann die Behebung sehr viel Zeit in Anspruch nehmen oder man entscheidet sich gar dafür, den Fehler nicht zu beheben, weil das Risiko, dadurch mehr Schaden anzurichten als Nutzen, zu groß ist.
Techniken
Weil das Feld der möglichen Softwareprobleme sehr groß ist, gibt es auch vielfältige Möglichkeiten, sie zu analysieren. Zunächst werden die Möglichkeiten für die Analyse von „klassischen“ Bugs aufgezeigt, also solchen, bei denen ein Ergebnis offensichtlich falsch ist oder das Programm reproduzierbar bei einer bestimmten Eingabe abstürzt. Eingabe und Ausgabe müssen sich dabei aber nicht auf Tastatur oder Bildschirm beschränken, sondern es sind darunter alle möglichen Datenein- und Ausgänge zu verstehen (Dateien, Netzwerkverbindungen, Drucker, Scanner etc.).
Der primitivste Ansatz, der allerdings auch nur bei klassischen Bugs funktioniert, ist, das Program mit zusätzlichen Programmausgaben zu instrumentieren. Es werden also Programmzeilen hinzugefügt, die Zwischenergebnisse ausgeben, entweder auf den Bildschirm oder in eine Logdatei. So kann herausgefunden werden, in welchem Schritt einer Berechnung ein Problem auftaucht, oder an welcher Stelle das Programm unerwartet abbricht. Man kann dabei eine Divide-and-Conquer-Taktik oder eine Binäre Suche anwenden, um das Problem einzugrenzen. Diese Art der Fehlersuche wird manchmal auch printf-Debugging genannt, in Anlehnung an die C-Funktion printf, die dafür häufig benützt wird.
Sobald die Programme komplexer werden, kommt zur Suche klassischer Fehler meistens ein eigentlicher Debugger zur Anwendung. Ein moderner Debugger ist häufig Teil einer IDE, in der auch das Programm entwickelt wurde. Der Programmierer kann direkt im Quelltext sogenannte Breakpoints setzen, die das Programm unterbrechen. Wenn das Programm unterbrochen ist, kann der Zustand überprüft werden, in dem der Aufrufstapel untersucht oder Variablen überprüft werden. Das Programm kann auch Zeile für Zeile ausgeführt werden, um schnell unerwartete Sprünge zu finden. Der Debugger ist heute neben dem Compiler das wichtigste Werkzeug eines Programmierers.
Mittels Logdateien oder Crash Dumps ist es manchmal möglich, die Ursache eines Fehlers zu finden, obwohl das Programm bereits abgestürzt ist und obwohl der Fehler nicht einfach reproduzierbar ist oder der Pfad, der zum Fehler führte, nicht bekannt ist.[3] Der gespeicherte Aufrufstapel zusammen mit der Exception können einen hinreichenden Hinweis darüber geben, welche Invariante nicht erfüllt war oder welche Operation unerwartete Ergebnisse lieferte.
Ein Profiler ist ein Computerprogramm, das ein zu untersuchendes Programm während des Ausführens analysiert und Informationen darüber liefert, welche Funktionen im Programm wie viel Zeit – meist relativ zur insgesamt verbrauchten Zeit – verwendet. So kann herausgefunden werden, welche Operationen länger dauern als nötig oder wo sich eine Optimierung lohnt. Es ist weder möglich noch sinnvoll, bereits bei der Entwicklung alle möglichen Optimierungsschritte einzubauen, weil erst beim Testen klar wird, welche Funktionen wie oft ausgeführt werden und wo eine Verzögerung stört. Alternativ kann mit den gleichen Tools oft auch der Speicherverbrauch untersucht werden.
Gegenmaßnahmen
Mit einem Debugger ist es grundsätzlich auch möglich, Programme zu untersuchen, die man nicht selber geschrieben hat oder zu denen man den Quelltext nicht besitzt. Letzteres erschwert die Sache zwar wesentlich, macht die Untersuchung aber nicht unmöglich. Der Zweck solcher Operationen ist oft illegal oder unstatthaft: Es wird etwa versucht, die Funktionsweise von Algorithmen zu analysieren, um sie in eigenen Programmen verwenden zu können oder es wird eine Lizenzabfrage („Kopierschutz“) umgangen oder ausgebaut.[4] Bei (Mehrspieler-)Computerspielen wird zuweilen versucht zu betrügen, indem das Programm manipuliert wird, etwa um Gegner durch Wände hindurch sehen zu können oder so, dass die eigene Spielfigur bei einem Treffer keinen Schaden erleidet. Maßnahmen, die dies verhindern sollen, werden Anti-Debugging-Methoden genannt.[5]
Um solche Angriffe zu detektieren, kann ein Programm versuchen, die Präsenz eines Debuggers zu ermitteln.[6] Ein guter Debugger ist jedoch für das Programm möglichst transparent, denn im Normalfall soll durch das Debuggen die Funktion des Programms nicht verändert werden. Ganz verhindern lassen sich Hackerangriffe damit also nicht – unter dem Strich ist es eine Abwägung, den Aufwand für die Sicherung des Programms ins Verhältnis zu stellen mit dem geschätzten Aufwand, den ein Hacker hätte, die Maßnahmen zu umgehen sowie dem damit eventuell einhergehenden wirtschaftlichen Nachteil. Auch muss die Anti-Debugging-Maßnahme geeignet sein, das illegitime Verfahren zu vermeiden oder zu verhindern. So ist es informationstheoretisch falsch, den Quelltext eines Webservers geheim zu halten, um Hackern das Aufdecken von Sicherheitsmängeln zu verunmöglichen (beispielsweise beim Vorliegen eines Pufferüberlauf-Angriffsvektors). Es sollte stattdessen mit geeigneten Tools direkt versucht werden, Angriffsvektoren zu finden oder zu verhindern.
Literatur
- Michaeli, Tilman. Debugging Im Informatikunterricht (2021). doi:10.17169/refubium-28811
Einzelnachweise
- Michaeli, Tilman. Debugging Im Informatikunterricht (2021). doi:10.17169/refubium-28811, Seite 20. Zitiert aus ISO, IEC (2010). „IEEE, Systems and Software Engineering–Vocabulary“. In: IEEE computer society, Piscataway, NJ 8, S. 9.
- Michaeli, Tilman. Debugging Im Informatikunterricht (2021). Seite 22f.
- Postmortem Debugging.
- Software Protection through Anti-Debugging Michael N Gagnon, Stephen Taylor, Anup Ghosh. Archiviert vom Original am 1. Oktober 2011. Abgerufen am 25. Oktober 2010.
- Tyler Shields: Anti-Debugging Series – Part I. In: Veracode. 2. Dezember 2008. Abgerufen am 17. März 2009.
- Common Anti-Debugging Techniques in the Malware Landscape. 27. Dezember 2017. Abgerufen am 10. Juli 2021.