Statische Analyse Programme besser verstehen

Ein Bild sagt mehr als tausend Worte. Diese Volksweisheit erklärt zum großen Teil den Erfolg von vorrangig grafikorientierten formellen Notationen wie UML. Zwar eignen sich solche Diagramme für das Design sehr gut, haben aber gravierende Nachteile, wenn sie in einer späteren Phase des Entwicklungsprozesses den Entwicklern beim Verstehen von existierendem Code helfen sollen.

Bilder eignen sich meist besser als Texte, um Entwicklern komplexe Programme verständlich zu machen. Dementsprechend oft werden in der Softwareentwicklung Programme visualisiert. UML und andere vorrangig grafikorientierte formelle Design-Notationen gelten mittlerweile als der beste Standardmechanismus zum Kommunizieren verschiedener Aspekte eines Softwaredesigns. Einige modellbasierte Designwerkzeuge können Code direkt aus grafischen Darstellungen generieren. Am informellen Ende des Spektrums zeichnen Entwickler häufig Flussdiagramme (Flow Charts) oder Aufrufdiagramme
(Call Graphs), um wichtige Aspekte der Software zu dokumentieren - sei es für sich selbst oder für andere.

UML-Diagramme mögen sich für das Design sehr gut eignen, haben aber zwei gravierende Nachteile, wenn sie in einer späteren Phase des Entwicklungsprozesses den Entwicklern beim Verstehen von existierendem Code helfen sollen. Der erste Nachteil besteht darin, dass
UML-Diagramme ihrer Rolle als Designabstraktionen gemäß einige
Implementierungsdetails weglas-sen. Um die finale Software verstehen zu können, sind diese Einzelheiten jedoch wichtig. Der zweite
nachteilige Aspekt ist, dass Design-diagramme bezüglich der Implementierung häufig nicht auf dem neuesten Stand sind, wodurch das System in seiner tatsächlichen Form ungenau oder unvollständig wiedergegeben wird. Informelle Visualisierungen andererseits sind tendenziell kurzlebig und werden nur selten in die offizielle Dokumentation eines Programms übernommen.

Sehr häufig ist der Code selbst das einzige Artefakt, mit dem ein Entwickler arbeiten muss. Ungünstigerweise waren Code-Visualisierungsprogramme in der Vergangenheit häufig mit Problemen behaftet. Dazu gehörten beispielsweise verwirrende Diagramme oder Schwierigkeiten bei der Skalierung auf umfangreiche Programme. Inzwischen aber gibt es neue Tools, die damit beginnen, diese Probleme
zu lösen. Der entscheidende Vorteil dieser Werkzeuge ist ihre Fähigkeit, direkt aus dem Code nützliche Visualisierungen zu generieren. Dementsprechend wird für die Genauigkeit und Aktualität dieser Visualisierungen garantiert.

Die Mehrzahl der gängigen Programmvisualisierungen bedient sich ein und desselben Graphen-basierten Paradigmas. Elemente wie etwa Dateien, Variablen, Funktionen usw. werden durch Umrisse (Knoten) dargestellt, während Linien (Kanten) dazu dienen, Beziehungen wie etwa Abhängigkeiten oder Inklusionen zu symbolisieren. Knoten lassen sich in weitere Knoten schachteln, um hierarchische Beziehungen wie etwa ein Containment anzudeuten. Größe, Form und Farbe eines Knotens lassen sich überdies nutzen, um weitere interessierende Eigenschaften auszudrücken. Knoten und Kanten können mit Text beschriftet werden, und die zweidimensionale Anordnung der Knoten eignet sich ebenfalls zum Visualisieren wichtiger Informationen.
Grundsätzlich ist es durchaus möglich, Layouts für sehr große Graphen
anzulegen. Dabei erweist sich jedoch die Aufnahmefähigkeit des menschlichen Gehirns als limitierender Faktor. Umfasst ein Graph ein paar Dutzend Knoten und einige hundert Kanten, stößt der Mensch unweiger-lich an seine Grenzen. Gute Werkzeuge lassen sich dennoch auf sehr umfangreiche Graphen skalieren, denn sie bringen Features mit, die dem Anwender helfen, die wirklich wichtigen Details auszufiltern.

Verschiedene Betrachtungsweisen sinnvoll

Programme bestehen aus einem umfangreichen, komplexen Geflecht von Abhängigkeiten zwischen vielen unterschiedlichen Arten von Komponenten. Ein Visualisierungswerkzeug, das versucht, alle diese Elemente gleichzeitig darzustellen, wäre zu umständlich für den sinnvollen Einsatz. Tatsächlich gibt es keine Visualisierung, die für alle Anwendungen ideal ist. Die sinnvollste Visualisierung für eine bestimmte Aufgabe ist vielmehr jene, die der inneren Vorstellung des zuständigen Ingenieurs gerecht wird. Auf einige besonders nützliche Programmstrukturen wird nachfolgend eingegangen.

Sinnvoll empfinden viele Entwickler eine Darstellung der verschiedenen Arten, wie die verschiedenen Datentypen miteinander in Beziehung
stehen können (Typ-Hierarchie). Das standardmäßige UML-Klassendiagramm stellt die Klassenhierarchie in leicht verständlicher Form dar, wobei Assoziations- und Containment-Beziehungen auf einer höheren Abstraktionsebene angesiedelt sind als der Code. Dies mag aus dem Blickwinkel des Designs sinnvoll sein. Programmierer empfinden es jedoch oft als hilfreicher, die konkreten Beziehungen zwischen den Typen sehen zu können.

In C und C++ geschriebene Programme machen häufig intensiven Gebrauch vom Präprozessor. Erfolgt dies mit der entsprechenden Umsicht, kann die Verständlichkeit der Programme hiervon durchaus profitieren. Oft jedoch fügt der Entwickler eine zusätzliche Ebene ein, die das Verständnis erschwert. Nutzt er den Präprozessor undiszi-pliniert, kann das zu verwickelten Abhängigkeiten führen, die Probleme beim Build zur Folge haben und die Möglichkeit der Wiederverwendbarkeit beeinträchtigen können. Den Entwicklern kann es deshalb beim Aufdecken komplexer Abhängigkeiten helfen, wenn sie sehen können, welche Dateien wo eingebunden wurden (Include-Baum).

Ein Aufrufdiagramm oder Call-Graph, in dem jeder Knoten für ein Unterprogramm steht und jede Kante eine oder mehrere Aufrufe eines anderen Unterprogramms symbolisiert, gilt häufig als die Programmstruktur, deren Visualisierung am hilfreichsten ist. Unterprogramme sind Einheiten, mit denen Entwickler gedanklich gut umgehen können, und die Aufrufbeziehungen geben den Daten- und Kontrollfluss anschaulich wieder. Der Call-Graph kann aber selbst bei einem kleinen Programm Hunderte von Knoten und Tausende von Kanten enthalten. Man ist sich deshalb schon seit langem einig, dass es nicht sinnvoll ist, das komplette Aufrufdiagramm auf einmal zu visualisieren. Stattdessen haben sich die Forscher auf Möglichkeiten konzentriert, ihn in Form von kleineren, einfacher zu verstehenden Teilstücken zu visualisieren.

In Call-Graphen wurde viel Forschungsarbeit investiert, da sie so wichtig für das Verstehen eines Programms sind und ihre Visualisierung so große Herausforderungen birgt. Insbesondere wurden neue Techniken entwickelt, um die Komplexität der Aufrufdiagramme in den Griff zu bekommen. Die folgenden Abschnitte beschreiben einige der Mechanismen, die in »CodeSonar« von GrammaTech implementiert wurden.

Top-down- und Bottom-up-Darstellungen

Durch die Top-down-Darstellung eines Call-Graphen können Anwender Fragen beantworten wie etwa: »Aus welchen abstrakten Komponenten besteht das Programm?« oder »Welches sind die Eigenschaften und Beziehungen dieser Komponenten?«.

Um dieses Problem im Zusammenhang mit dem Verstehen eines Programms zu lösen, lassen sich Tool-Designer von Programmen zur Landkartendarstellung inspirieren, beispielsweise »Google Maps«. Der Anwender hat hier die Möglichkeit, gleichsam aus großer Höhe auf ein großes Gebiet zu blicken, wobei nur die wichtigsten Merkmale dargestellt werden. Wenn der Anwender jetzt hineinzoomt, treten immer mehr Einzelheiten zutage - zunächst die Großstädte, dann auch kleinere Städte und Dörfer und schließlich einzelne Straßenzüge und sogar Gebäude. Der Detailreichtum des Bildes richtet sich also nach dem Zoomfaktor.

Programme bestehen aus Komponenten, die ihrerseits aus kleineren Komponenten zusammengesetzt sind, die wiederum aus einzelnen Bestandteilen bestehen usw. Es ergibt sich so eine hierarchische Struktur. Obwohl die direkte Aufrufbeziehung zwischen den Unter-programmen auf den unteren Abstraktionsebenen besteht, lässt sie
sich auf Komponenten weiter oben in der Hierarchie projizieren, welche die betreffenden Unterprogramme enthalten. In der Top-down-Darstellung eines Aufrufdiagramms handelt es sich bei den Elementen auf der höchsten Abstraktionsebene um Verzeichnisse (Ordner). Diese wiederum können Unterverzeichnisse und Dateien enthalten, und in den Dateien befinden sich die Unterprogramme. Eine Kante von einem Kasten zu einem anderen deutet somit einfach an, dass ein Unterprogramm im ersten Kasten ein anderes Unterprogramm im zweiten Kasten aufruft. Die Kanten auf den höheren Ebenen geben somit die Kanten der unteren Ebenen gebündelt wieder. Dieses Konzept erweist sich als sehr effektiv, um Entwicklern zu einem fundierteren Verständnis eines Programms zu verhelfen.

Bild 1 zeigt den Call-Graphen eines kleinen Programms, produziert und dargestellt von CodeSonar. Im Fenster links hat der Benutzer beispielsweise die Kante zwischen der Komponente »find« und der Komponente »gnulib« gewählt. Die in dieser gebündelten Kante enthaltenen Funktionsaufrufe sind im rechten Fenster zu sehen, das mehr Details zeigt, sobald der Anwender zum Betrachten einer einzelnen Funktion den Zoomfaktor erhöht. Dieser Zoomfaktor verdeutlicht eine wichtige Eigenschaft: Für den Entwickler ist es wichtig, einen Bezug zwischen der Ansicht und dem eigentlichen Code herstellen zu können. Auf das Selektieren einer dieser Funktionen hin erscheint deshalb der Quellcode zu dieser Funktion. In vielen Fällen wird ein Entwickler das Bottom-up-Prinzip bevorzugen, denn er erhält hier Hilfestellung bei der Beantwortung von Fragen wie: »Was tut diese Prozedur?« oder »Wie passt dies in die Struktur des Programms und wie wird es aufgerufen?«.

Nehmen wir zum Beispiel an, ein Programm sei bei der Ausführung einer bestimmten Funktion abgestürzt. Um die Ursache hierfür zu finden und eine Abhilfemaßnahme auszuarbeiten, dürfte sich der Entwickler zunächst auf diese eine Funktion konzentrieren. Anschließend wird er die unmittelbare Umgebung inspizieren, um beispielsweise zu sehen, welche anderen Funktionen sie aufruft und von welchen anderen Funktionen sie selbst aufgerufen wird. Üblicherweise erledigen Entwickler dies manuell an einem Whiteboard. Dabei zeichnet er zunächst einen Kasten, der die interessierende Prozedur symbolisiert. Anschließend weiter er diese Ansicht aus, indem er Aufrufer und Aufgerufene einzeichnet. Ein Tool kann bei dieser Arbeit helfen, indem es das aufwendige Zeichnen automatisch erledigt.

Metrics-Ebenen

Der Nutzen der Visualisierung lässt sich noch steigern, wenn man zusätzliche Ebenen einfügt, welche die Werte verschiedener Metriken zeigen.

Ein Beispiel hierfür zeigt Bild 2. Es handelt sich hier um eine Treemap, die eine besonders nützliche Visualisierung darstellt. In einer Treemap ist die Fläche eines Knotens proportional zu einer Metrik. Meist handelt es sich dabei um eine Metrik, welche die Größe des jeweiligen Elements codiert. Unterknoten werden dabei als Kacheln innerhalb des übergeordneten Knotens dargestellt, während Kanten normalerweise nicht erscheinen. Im vorliegenden Beispiel gibt die Intensität der Färbung die Zahl der vom statischen Analysewerkzeug ausgegebenen Code-Vulnerability-Warnungen wieder. Aus dieser Darstellung lassen sich die riskantesten Komponenten eines Programms einfach ablesen. 
Treemaps können sehr effektiv zur Visualisierung tief verschachtelter Strukturen genutzt werden. Sie eignen sich außerdem hervorragend für das beschriebene Zoomkonzept, bei dem mit wachsender Vergrößerung immer mehr Details dargestellt werden.

Am nützlichsten sind die angesprochenen Visualisierungen, wenn die Entwickler sich interaktiv in den Darstellungen bewegen und den Zoomfaktor variieren können oder sogar die Möglichkeit haben, Knoten und Kanten hinzuzufügen oder zu entfernen. Der Umgang mit einer solchen Benutzeroberfläche kann nämlich äußerst frustrierend sein, wenn es an der nötigen Reaktionsfähigkeit mangelt. Hunderte von Knoten und Tausende von Kanten darzustellen, kann eine echte Herausforderung sein. Günstig ist in diesem Zusammenhang, dass die Grafiktechnologie für Desktop-Workstations in den letzten Jahren enorme Fortschritte gemacht hat, hauptsächlich getrieben von den wachsenden Anforderungen der Spiele-Industrie. Ebenso sind inzwischen höchst reaktionsschnelle Benutzeroberflächen möglich, die von den Hardwarebeschleunigern auf den Grafikkarten profitieren. Daher kann die Programmvisualisierung mittlerweile auch sehr umfangreiche und komplexe Programme aufbereiten.

Über den Autor:

Paul Anderson ist Vide President of Engineering bei GrammaTech.