Einblick in die statische Code-Analyse: Auf verschlungenen Pfaden

Statische Code-Analyse ist ein wichtiges Hilfsmittel, um Software-­Qualität zu sichern. Schon bei einfachen Programmen ist die Zahl der Programmzustände schier unüberblickbar. Keine Analyse-Tool kann ­alle Zustände lückenlos analysieren. Die Nutzer müssen deshalb die ­Arbeitsweise dieser Tools kennen und die Analyseparameter ­passend abstimmen.

Werkzeuge der statischen Analyse zum Auffinden von Programmfehlern kommen seit Jahrzehnten zum Einsatz. Die Werkzeuge der ersten Generationen wie Lint und dessen Nachfolger arbeiten mit oberflächlichen Techniken, um recht einfache Probleme wie nicht verträgliche Typen und Verletzungen der empfohlenen Programmrichtlinien zu finden. Die Tools der neuesten Generation, auch bekannt als fortgeschrittene Statische-Analyse-Werkzeuge, nutzen deutlich ausgefeiltere Techniken. Sie können alle Probleme finden, die für die frühen Werkzeuge auffindbar waren, und dazu entdecken sie auch versteckte Programmierfehler, die anderweitig schwer zu erkennen sind.

Genauigkeit, Trefferquote und ­Leistung

Die wahrscheinlich wichtigste Eigenschaft eines Statische-Analyse-Werkzeugs ist dessen Fähigkeit, echte Fehler bzw. True Positives zu finden. Weil die meisten Benutzer eine derart starke Abneigung gegen falsch-positive Treffer (False Positives) haben, kommt ein Tool mit zu vielen Fehlanzeigen kaum zum Einsatz. Also ist es wichtig, diese so niedrig wie möglich zu halten. Dasselbe gilt für Tools, die für die Ausführung der Aufgabe zu viel Zeit oder übermäßig viel Speicher brauchen.

Die Fähigkeit eines Werkzeugs zum Aufspüren echter Fehler wird als Trefferquote bezeichnet. Sie beschreibt den Anteil echter Fehler, den ein Werkzeug auffinden kann. Ein Tool mit einer Trefferquote von 100 % erkennt alle Fehler.

Der Begriff Genauigkeit bezieht sich auf die Fähigkeit eines Werkzeugs, echte statt falscher Probleme zu melden. Sie definiert sich über den Anteil der echten Fehler an der Gesamtzahl der gemeldeten Probleme. Ein Tool mit einer Genauigkeit von 100 % meldet keine falsch-positiven Treffer.

Das perfekte Werkzeug verfügt über die ideale Trefferquote, die ideale Genauigkeit und braucht nur wenig Ressourcen. Da sich diese drei Faktoren jedoch mehr oder weniger gegenseitig ausschließen, ist ein solches Tool mit der heutigen Technologie nicht umsetzbar. Eine höhere Genauigkeit wirkt sich negativ auf die Trefferquote oder die Leistung aus. Daher bewegen sich die Tools in einem Bereich, in dem alle drei Faktoren möglichst ausgeglichen sind. Über eine Veränderung der Parameter kann der Nutzer den Wirkungsbereich des Tools in die von ihm gewünschte Richtung verschieben. Der optimale Bereich hängt von der jeweiligen Anwendung ab. Bei sicherheitskritischer Software wird der Benutzer sich eher für eine Konfiguration mit hoher Trefferquote entscheiden, damit mehr echte Fehler gefunden werden, auch wenn das einen erhöhten Zeitaufwand und die Anzeige von mehr falsch-positiven Treffern bedeutet. Weil es nicht die eine perfekte Lösung für alle Anwender gibt, sollten diese Faktoren auf jeden Fall sorgfältig abgewogen werden. Bild 1 zeigt eine Methode zum rationalen Vergleich der Werkzeuge.

Zerlegung des Quellcodes

Grob gesagt lesen Werkzeuge der statischen Analyse den Text eines Codes und erstellen ein Programmmodell, an dem sie anschließend mehrere Abfragen durchführen, um Fehler aufzudecken. Das funktioniert so ähnlich wie bei einem Compiler. Diese parsen den Code und legen Zwischendarstellungen an, aus denen anschließend der Maschinencode generiert wird. Ein Statische-Analyse-Werkzeug verfährt genauso, allerdings werden die Zwischendarstellungen nicht zur Generierung eines Codes genutzt, sondern gespeichert und für die Erstellung des Programmmodells verwendet. Diese Darstellungen umfassen

  • den abstrakten Syntaxbaum (ASB), eine hierarchische Darstellung der textuellen Struktur des Codes – Beispieldarstellung in Bild 1.
  • die Symboltabelle, die Informationen über alle im Programm benannten Einheiten enthält, u.a. Makros, Dateien, Typen und Funktionen. Bei einem in C++ geschriebenen Programm gehören dazu auch Elemente wie Vorlagen, Klassen und Ausnahmen.
  • ein Kontrollflussdiagramm für jede Funktion. In dieser Darstellung gibt es Knoten, die Elemente wie Anweisungen oder Ausdrücke darstellen, und Verbindungen, die anzeigen, dass die Ausführung von einem Teil zum nächsten fließen kann. Die Verbindungen lassen sich benennen, sodass von einer IF-Anweisung zwei Verbindungen abgehen. Eine führt zu der Anweisung, die ausgeführt wird, wenn die Bedingung wahr ist, die andere zu der Anweisung für eine falsche Bedingung. Die Semantik eines jeden Knotens wird durch einen einfachen Ausdruck modelliert, der mit jedem Knoten in Zusammenhang steht. Bild 2 zeigt ein Beispiel.

Diese Darstellungen werden für die Inhalte jeder Übersetzungseinheit erzeugt. Nach Verarbeitung aller Übersetzungseinheiten werden diese Darstellungen zusammengeführt, um ein Modell für das gesamte Programm zu erstellen. Diese Programmdarstellungen umfassen

  • das Aufrufdiagramm, das einen Knoten für jede Funktion sowie die Verbindungen zwischen den Knoten enthält und die Aufrufbeziehungen darstellt.
  • das Typenabhängigkeitsdiagramm, das die Beziehungen zwischen den verschiedenen Typen im Programm aufzeigt. Beispielsweise kann ein Struct-Typ Felder enthalten, die einen anderen Typ verwenden, oder eine Klasse von einer anderen erben.

Einmal erstellt, kann die Analysemaschine dieses Programmmodell auf verschiedene Weise nutzen.