Softwarequalität So funktioniert statische Quellcodeanalyse

Autor / Redakteur: Benjamin Chelf* / Martina Hafner

Werkzeuge für die Analyse von Quellcodes waren bisher häufig in ihrem Können limitiert. Dieser Artikel beschreibt, wie Forschungsarbeiten der Stanford University die statische Quellcodeanalyse zu einem wirksameren Mittel für die Optimierung von Softwarequalität gemacht hat.

Anbieter zum Thema

Für die Optimierung von Software gibt es viele Möglichkeiten: Im Rahmen von Modultests (Unit Testing) werden einzelne Abschnitte der Software geprüft; bei Belastungstests werden komplette Projekte harten Prüfungen unterzogen und bei Feldtests können Kunden Vorab-Versionen von Software unter äußerst praxisnahen Bedingungen installieren und testen. Die kostengünstigste Art der Optimierung der Softwarequalität bildet jedoch die Behebung von Fehlern vor der eigentlichen Ausführung des Codes. Bei der Quellcodeanalyse werden im Rahmen einer Analyse des Quellcodes Defekte wie Speicherlecks, NULL-Pointer und Puffer-Überläufe ermittelt. In der Vergangenheit waren derartige Werkzeuge jedoch stark limitiert: Die Analyse von Millionen Zeilen Code war unmöglich oder ergab zu viele falsch positive Treffer. Durch neuere Forschungsergebnisse lassen sich viele Probleme der älteren Verfahren beseitigen. Aber wie funktioniert die Quellcodeanalyse konkret?

Für die Durchführung einer Quellcodeanalyse muss zunächst die elementare Ausführungseinheit im Programm ermittelt werden. Diese wird in diesem Zusammenhang als Basic Block oder Basisblock bezeichnet. Liste 1 zeigt ein einfaches Programm zur Veranschaulichung von Basisblöcken. Ein Basisblock ist ein Abschnitt des Programms, der sich nicht mit bedingten Verzweigungen, Schleifengrenzen oder anderen Programmsprüngen überlagert. Der Code in Liste 1 verdeutlicht dies. Die Anweisung in foo() lässt sich in vier Basisblöcke unterteilen (siehe Bild 1). Die Anweisungen in Zeile 4 und 6 sind nicht Bestandteil desselben Basisblocks, weil die Ausführung von Zeile 4 nicht zwangsläufig auch die Ausführung von Zeile 6 nach sich zieht. Zeile 4 wird bei Aufruf von foo() immer ausgeführt. Zeile 6 wird nur dann ausgeführt, wenn der an die Funktion übergebene Wert identisch mit dem Wert der lokalen Variablen j ist. Zeile 4 und 10 werden bei Aufruf von foo() zwar immer ausgeführt, gehören aber dennoch unterschiedlichen Basisblöcken an, weil sich zwischen ihnen Anweisungen befinden, die unter Umständen nicht zur Ausführung kommen.

Nach Unterteilung des Quellcodes in Basisblöcke lassen sich die einzelnen Blöcke in einem Control-Flow-Graph darstellen. Bei der Quellcodeanalyse werden mittels Control-Flow-Graphs alle möglichen Pfade durch einen Codeabschnitt untersucht. Herkömmliche Testverfahren schneiden im Vergleich dazu schlecht ab. Um ein Programm durch Ausführung vollständig zu testen, muss jeder der möglichen Pfade durch den Code einmal durchlaufen werden. Bei tausenden oder Millionen Zeilen Code gibt es buchstäblich Milliarden möglicher Pfade, deren Untersuchung Jahre dauern würden. Mit Hilfe der Quellcodeanalyse lassen sich Milliarden von Pfaden hingegen innerhalb von Minuten prüfen.

Warum geht das so schnell? Ein laufendes Programm muss 100 % seiner internen Vorgänge verfolgen können. Jedes Speicher-Byte muss aktualisiert werden, wenn das Programm die entsprechende Anweisung gibt, muss auf Anweisung des Programms gelesen werden usw. Bei der Quellcodeanalyse ist eine derartige Präzision nicht vonnöten. Viele Fehler lassen sich finden, ohne dazu jedes einzelne Byte erfassen zu müssen. Daher lassen sich per statischer Analyse alle Pfade eines Programms prüfen.

Arten von Softwarefehlern

Mittels Quellcodeanalyse lassen sich viele verschiedene Arten von Fehlern finden:

  • * NULL-POINTER-DEREFERENCES. Jedem Lese- und Schreibvorgang im Speicher ist eine Speicheradresse zugeordnet. Das Schreiben oder Lesen im Speicher muss über einen gültigen Speicherbereich erfolgen. Das Lesen oder Schreiben im Speicher kann mittels einer Pointer-Dereference vollzogen werden. Wenn die dereferenzierte Adresse 0 (oder NULL) ist, liegt eine Null-Pointer-Dereference vor. Daraufhin stürzt das Programm in der Regel sofort ab oder erzeugt eine Ausnahme.
  • * SPEICHERLECKS. Ein Speicherleck tritt auf, wenn ein Programm Speicher zuweist (über malloc oder new) und diesen Speicher dann, statt ihn ausdrücklich freizugeben, „aus den Augen verliert“. Diese Art Fehler kann besonders bei Programmen, die für die Ausführung über einen längeren Zeitraum vorgesehen sind, schwerwiegende Folgen haben. Dem Programm geht nämlich irgendwann der Speicher aus und es stürzt unter Umständen ab.
  • * BELEGUNG FREIGEGEBENEN SPEICHERS. Ein weiteres, häufig auftretendes Problem ist die Belegung von Speicher nach dessen Freigabe. Wenn ein Programm angibt, dass es keinen Speicher mehr benötigt, sollte es diesen auch nicht belegen, weil er für andere Zwecke beansprucht werden könnte. Die Belegung bereits freigegebenen Speichers kann zufällige Speicherzugriffsfehler (Memory Corruption) zur Folge haben. Speicherzugriffsfehler lassen sich nur äußerst schwer finden und beheben, weil sich der durch sie ausgelöste Programmabsturz nicht zwangsläufig auf die Ursache des Problems zurückführen lässt. Die Quellcodeanalyse kann direkt den Ort angeben, an dem der Fehler entstand, indem sie die erste Stelle angibt, an der freigegebener Speicher nach der Freigabe belegt wird.
  • * PUFFER-ÜBERLÄUFE. Bei der Zuweisung von Speicher geben Sie an, wie viele Bytes benötigt werden. Bei Belegung von mehr Speicher als ursprünglich angefordert, wird ein so genannter Puffer-Überlauf (Buffer Overflow) erzeugt. Viele über das Internet verbreitete Würmer und Viren nutzen Puffer-Überlaufe aus, weil diese spezielle Art von Speicherzugriffsfehler die Übernahme eines Programms ermöglicht. Mit Hilfe der Quellcodeanalyse lassen sich viele der Sicherheitslücken im Code aufspüren und schließen, bevor sie von Hackern ausgenutzt werden können!

Mittels Quellcodeanalyse lassen sich ferner andere Defekte beheben: u. a. nutzlose Anweisungen aufgrund „toten Codes“, die Verwendung nicht initialisierter Variablen und Instanzen, deren Rückgabewerte nicht ignoriert werden dürfen.

Wie funktioniert die Quellcodeanalyse?

Bei der Quellcodeanalyse werden nur die Informationen verfolgt, die für das Auffinden bestimmter Fehlersorten benötigt werden. Diese Informationen (State) bilden die Basis der Analyse. Bei der Quellcodeanalyse kommen vereinfachende Annahmen zum Einsatz, um die Anzahl der möglichen States oder Zustände in einem Programm möglichst gering zu halten.

Wenn der State-Space ausreichend verkleinert wurde und so Milliarden von Pfaden durch den Code verfolgt werden können, wird beispielsweise ein Pointer durch jeden gegebenen Punkt im Programm auf einem Pfad mittels einer State Machine verfolgt. Eine State Machine besteht aus States (d. h. „NULL“ bzw. „Nicht NULL“) sowie Übergängen zwischen diesen beiden States oder Zuständen. Übergänge zwischen den einzelnen Zuständen können durch verschiedene Anweisungen im Code bewirkt werden.

Das Auffinden der geschilderten Defektgruppen erfolgt unter Verwendung ähnlicher Verfahren für das Definieren des State-Space und der Übergänge für jede spezielle Fehlersorte und die anschließende Implementierung der State Machine in Form einer Analyse. Bei diesem Ansatz lässt sich die Quellcodeanalyse äußerst modular und erweiterbar anlegen. Um den vorhandenen Analysedefinitionen eine neue hinzuzufügen, muss man lediglich auf der Basis der State Machine die entsprechende Logik kodieren. Liegt eine gute Engine und ein Framework vor, gestaltet sich diese Aufgabe so einfach wie das Verfassen von 20 oder 30 Zeilen in einer Skriptsprache.

Bis vor kurzem erzeugte eine Quellcodeanalyse zu viele falsch positive Ergebnisse und war nicht skalierbar. Als falsch positives Ergebnis gilt ein gemeldeter Fehler, der kein tatsächlicher Fehler ist. Falsch positive Ergebnisse können durch zu starke Vereinfachung der für die Analyse erforderlichen Annahmen entstehen. Dank der in jüngster Zeit erzielten Fortschritte bei der Quellcodeanalyse lassen sich die in diesem Artikel beschriebenen Fehlersorten in sehr großen Code-Basen (Millionen Zeilen Code) mit einer Falsch-Positiv-Rate von unter 20 % ermitteln. Dies unterscheidet sich stark von den Ergebnissen älterer Analysetechniken, bei denen pro echtem Defekt 50 bis 100 falsche Berichte und mehr ausgegeben werden konnten.

Die Skalierbarkeit ist bei vielen Verfahren der Quellcodeanalyse ebenfalls ein Problem. Viele Ansätze arbeiten mit einer derartigen Genauigkeit, dass sich ihre Nutzbarkeit in der Praxis auf wenige kleine Projekte mit maximal einigen tausend Zeilen Code beschränkt. Diese Techniken sind nicht weit verbreitet, weil sie einfach zu zeitintensiv sind. Moderne Quellcodeanalysetechniken unterstützen den Entwickler wirksam bei der Suche nach kritischen Defekten in ihrem Code – und dies in einer frühen Phase des Entwicklungsprozesses. Sie sollten einen integralen Bestandteil der Arbeiten zur Optimierung der Softwarequalität bilden.

Übersicht: Auswahlkriterien für statische Analysetools

  • Moderne Quellcodeanalysetechniken sollten in der Lage sein, schon während der Coding-Phase bzw. des System-Build-Prozesses kritische und schwierig zu findende Softwaredefekte und Sicherheitsrisiken im Quellcode aufzudecken, um die Produktivität und Qualität verschiedener verteilter Entwicklungsteams, die komplexen Code schreiben, zu erhöhen und sich auf die Erhöhung des Marktanteils durch die Produktentwicklung zu konzentrieren statt Zeit für spätes Aufspüren von Bugs zu „verschwenden“.
  • Hier einige der wichtigsten Auswahlkriterien für statische Analysetools:
  • Genauigkeit: Wie hoch ist die „False-Positive-Rate“?
  • Analysetiefe: Wie tief ist die Analyse?
  • Analysebreite: Verfügt das Programm über eine Sammlung verschiedener Qualitäts- und Sicherheits-Checker?
  • Kostenvorteil: Wie integratiert sich das Produkt an die Entwicklungsumgebung und wie aufwendig sind Installation und Konfiguration?
  • Ausweitbarkeit: Ist das Programm ausweitbar und anpassbar?
  • Skalierbarkeit: Ist das Produkt skalierbar?

*Benjamin Chelf ist Chief Technology Officer bei Coverity. Kontakt: ben@coverity.com

Jetzt Newsletter abonnieren

Verpassen Sie nicht unsere besten Inhalte

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung

(ID:210512)