Statische Softwareanalyse Wie sich Software ohne Testfälle testen lässt

Autor / Redakteur: Frank Büchner * / Hendrik Härter

In unserem Beitrag zeigen wir, wie sich mit statischer Analyse ohne großen Aufwand Fehler finden lassen. Der Code wird nicht ausgeführt, sondern mit Model Checking überprüft.

Firmen zum Thema

Softwaretest: Goanna analysiert Programme in C und C++ und arbeitet mit Eclipse/CDT und Visual Studio zusammen.
Softwaretest: Goanna analysiert Programme in C und C++ und arbeitet mit Eclipse/CDT und Visual Studio zusammen.
(Bild: Hitex)

Der größte Aufwand beim klassischen, dynamischen Testen entsteht beim Erstellen von guten Testfällen. Bei der werkzeuggestützten statischen Analyse entfällt dieser Aufwand. Bei der statischen Analyse wird der Programmcode nicht ausgeführt, sondern beispielsweise rein syntaktisch auf die Verletzung von vorgegebenen Regeln geprüft und/oder mit modernen Methoden, wie beispielsweise Model Checking, untersucht.

Begonnen hat die statische Analyse mit Programmen, die beispielweise überprüfen, ob die Anzahl der Parameter beim Aufruf einer C-Funktion dieselbe ist wie bei der Definition der Funktion. Der klassische Vertreter ist das Program lint. Die dabei aufgedeckten Probleme lassen sich überwiegend als Fehler einstufen, die dafür sorgen, dass das Programm nicht fehlerfrei abläuft. Wenn eine Funktion mit weniger Parametern aufgerufen wird als diese Funktion erwartet, kann das nicht gut gehen.

Bildergalerie

Mittlerweile werden solche Probleme größtenteils von Compilern erkannt. Während die Compiler immer mehr Aufgaben der klassischen Prüfprogramme übernahmen, fielen diesen Programmen neue Aufgaben zu: Sie überprüfen Programmierregeln und Programmiervorgaben. Ursprünglich waren das meist firmenspezifische Standards, wie die Forderung nach einheitlicher Benennung von Variablen in Bezug auf Groß-/Kleinschreibung oder nach der Angabe des Datentyps im Variablennamen anhand der Umgekehrten Polnischen Notation oder nach einem vorgegebenen Anteil von Kommentaren am Programmtext.

Wie sich Metriken in der Software bestimmen lassen

Letzteres oder das Ermitteln von Komplexitätsmaßen für einen Programmtext fällt genaugenommen unter das Thema „Bestimmung von Metriken“. Hier werden statistische Kennzahlen für das Programm ermittelt und es wird geprüft, ob diese Kennzahlen den Vorgaben entsprechen oder nicht. Das ist natürlich ebenfalls statische Analyse. Werden allerdings Metriken bestimmt, deckt dies keine funktionalen Fehler des Programms auf, sondern Metriken bewerten Eigenschaften wie Wartbarkeit oder Komplexität des Programms.

Werden Codierregeln überprüft, so wird versucht, potentielle Fehler des Programms zu vermeiden. Dabei wird zusätzlich versucht, verdächtige Programmkonstrukte zu finden, bei denen der Programmierer wahrscheinlich nicht das Programmverhalten erzielen wollte, das er tatsächlich programmiert hat. Gegenwärtig existieren verschiedene Programmierregelsätze für unterschiedliche Sprachen wie beispielsweise für C, C++, Java, Perl mit unterschiedlichen Schwerpunkten.

Einfache Muster erkennen nach MISRA-C:2004 oder MISRA-C++:2008

Die Mutter aller Programmierstandards sind die von MIRA (UK) herausgegebenen „Guidelines for the use of the C language in critical systems“. Die erste Ausgabe der Regeln für die Programmiersprache C stammt aus dem Jahr 1998. Eine überarbeitete Fassung kam im Jahr 2004 heraus. Vier Jahre später wurde ein Regelwerk für die Programmiersprache C++ herausgegeben. Eine Gemeinsamkeit haben alle Programmierstandards in allen Programmierregelsätzen: Viele Regeln lassen sich durch eine einfache, oft lokale Analyse des Programmtexts prüfen.

Es reicht häufig eine einfache Mustererkennung oder „Pattern Matching“ aus. Es müssen manchmal etwas aufwändigere syntaktische Muster erkannt werden. Die Regel 7.1 der MISRA-C:2004 oder Regel 2-13-2 der MISRA-C++:2008 Guidelines ist ein Beispiel für die einfache Mustererkennung. Die Regel verbietet den Gebrauch von oktalen Konstanten und auch von oktalen Escape-Sequenzen. Eine oktale Konstante in der Programmiersprache C ist eine Zahl, die mit der Ziffer 0 beginnt. Die Null ist genaugenommen selbst eine oktale Konstante, diese ist aber explizit von der Regel ausgenommen. Die Regel soll Missverständnisse bei der Bedeutung des Programmcodes vermeiden.

Im Bild 1 sind „001“ und „010“ beides oktale Konstanten. Während „001“ tatsächlich den Wert eins darstellt, ergibt „010“ den Wert acht und nicht zehn, wie man auf den ersten Blick meinen könnte. Ein Werkzeug kann relativ einfach prüfen, ob ein Programmcode eine oktale Konstante enthält oder nicht. Noch einfacher zu prüfen ist Regel 14.4, die die Benutzung des Schlüsselworts goto verbietet.

Nicht alle Regeln sind so leicht zu prüfen wie MISRA-Regel 7.1. Beispielsweise fordert MISRA-Regel 14.7, dass seine Funktion nur einen Ausgang haben darf, und zwar am Ende der Funktion („A function shall have a single point of exit at the end of the function“). Es darf also höchstens eine Return-Anweisung in einer Funktion geben und diese Return-Anweisung muss am Ende der Funktion stehen. Die Prüfung dieser Regel erfordert syntaktische Mustererkennung, denn die return-Anweisungen in einer Funktion müssen gezählt werden, was zwar ein gewisses Gedächtnis der Prüffunktion voraussetzt, aber alles in allem nicht schwierig ist, weil kein tiefergehendes Verständnis des Programms erforderlich ist wie beispielsweise eine Analyse der möglichen Pfade, die die Programmausführung nehmen kann.

Fortgeschrittene Methoden finden knifflige Fehler

Es gibt allerdings auch (wenige) MISRA-Regeln, bei der die Prüfung Analysen erfordern, die über die syntaktische Mustererkennung hinausgehen. Beispielsweise fordert die MISRA-Regel 14.1, dass kein unerreichbarere Code vorhanden sein darf („There shall be no unreachable code“). Auch hier gibt es einfache Fälle: Folgt beispielsweise eine Anweisung direkt auf eine break-Anweisung, so wird diese Anweisung offensichtlich und leicht zu erkennen niemals ausgeführt werden.

Aber es gibt auch viel schwieriger zu erkennende Situationen von unerreichbarem Code und anderen potentiellen Programmfehlern. Um diese zu erkennen, benötigt man moderne Methoden der Programmanalyse. Im Programmauszug im Bild 2 verbirgt sich ein sogenanntes „memory leak“. Das bedeutet, dass nicht aller allokierter Speicher auch wieder freigeben wird. Zu Beginn der Funktion func1() im Bild 2 wird zunächst mit dem Schlüsselwort „new“ Speicher allokiert, auf den der Zeiger p zeigt. Am Ende der Funktion func1() wird der Speicher wieder freigeben, aber nicht auf jedem möglichen Pfad. Goanna, ein Werkzeug zur statischen Analyse von Software, zeigt dies durch eine Meldung zu Zeile 17 des Programmauszugs in Bild 2 an.

Um eine solche Meldung anzeigen zu können, muss Goanna erkennen, dass die Freigabe des Speichers, auf den der Zeiger p zeigt, in der Funktion dealloc() erfolgt. Diese Fähigkeit nennt sich „interprozedurale Analyse“. In unserem Fall steht die Funktion dealloc() direkt vor func1(), was diese Gegebenheit für einen Menschen offensichtlich macht. Allerdings könnte dealloc() auch in einer anderen Quelldatei definiert sein, was es für Menschen schwerer macht, für ein Werkzeug jedoch nicht.

Goanna behauptet nun, dass es mindestens einen Pfad durch das Ende von func1() gibt, auf dem kein Aufruf der Funktion dealloc() erfolgt. Der Einfachheit halber gibt Goanna diesen Pfad dann auch an (siehe Bild 3). Der Pfad beginnt in Zeile 17, in der der Zeiger p definiert und zugewiesen wird. Wenn dann in Zeile 21 die Variable ok unwahr ist; in Zeile 23 die Variable error1 wahr ist; in Zeile 24 die Variable keypressed unwahr ist; und in Zeile 26 die Variable error2 ebenfalls unwahr ist: Dann gibt es keinen Aufruf von dealloc() und der Speicher wird nicht freigeben.

Software mit Model Checking überprüfen

Um das herauszufinden, könnte man alle möglichen Pfade auflisten und für jeden Pfad untersuchen, ob in ihm dealloc() aufgerufen wird oder nicht. Im Beispiel könnte das sogar mit vertretbarem Aufwand durchgeführt werden, weil die Anzahl der Pfade überschaubar ist. Leider wächst jedoch die Anzahl der Pfade exponentiell mit der Anzahl der Bedingungen. Deshalb verwendet Goanna für Prüfungen dieser Art das „Model Checking“.

Bei diesem Verfahren wächst der Aufwand nur linear mit der Größe des Kontrollflussgraphen. Man kann Vorher-/Nachher-Bedingungen prüfen oder beispielsweise sicherstellen, dass auf jedes new in jedem Pfad mindestens ein delete erfolgt. Aber es gibt Einschränkungen: Beispielsweise lässt sich nicht zählen und prüfen, ob auf jedem Pfad genau so viel new wie delete vorkommen.

Goanna ist ein Werkzeug für Windows und Linux, das auf die neusten Methoden aufsetzt. Dabei handelt es sich um Model Checking, abstrakte Datenwertverfolgung und interprozedurale Analyse. Es werden Programme in C und C++ analysiert. Goanna Studio arbeitet mit Eclipse/CDT und Visual Studio zusammen; Goanna Central ist als Kommandozeilenversion für den Build-Prozess gedacht.

* Frank Büchner arbeitet als Senior Test Engineer bei Hitex Development Tools in Karlsruhe.

Artikelfiles und Artikellinks

(ID:46497561)