![]() |
|
|
Viele C und C++ Programmierer sind der Ansicht, daß ein Tool wie Lint heutzutage überflüssig ist: Der Compiler liefert schließlich mehr als genug Warnungen von denen die meisten dazu noch ignoriert werden. Wozu also ein Tool einsetzen, welches noch mehr Meldungen produziert?
Nun mag die schiere Anzahl möglicher Compiler-Warnungen zu der Annahme verleiten, der Compiler führe tatsächlich eine umfassende inhaltliche (und nicht nur syntaktische) Analyse Ihres Programms durch. Diese Annahme ist leider falsch.
Das wird sehr schnell deutlich, wenn man sich die vom Compiler erzeugten Meldungen einmal genauer anschaut: Man wird dann feststellen, daß sie ausschließlich für Situationen erzeugt werden, die für den Compiler mit minimalem Zusatzaufwand erkennbar sind. Einige Beispiele sollen das zeigen:
Loss of precision / Conversion may lose significant digits
Dies ist die Compiler-Warnung, die ich wohl am häufigsten erhalte. Sie wird ausgegeben, wenn Sie eine Zuweisung von einem 'größeren' in einen 'kleineren' Typ vornehmen:
char c;
int i;
float f = 3.14;
i = f;
c = i;
Die beiden letzten Zeilen würden jeweils diese Meldung verursachen.
Ausgehend vom Vorgang des Compilierens ist sie nun nicht mehr als ein
Abfallprodukt" (obgleich ein durchaus nützliches), denn der eigentliche
Sachverhalt ist dem Compiler zwangsläufig bekannt: Er muß speziellen
Code einfügen, um etwa die Umwandlung eines float in einen int vorzunehmen.
Dann fällt es nicht schwer, diesen Sachverhalt auch in Form einer Warnung auszugeben.
Variable not initialized / Possible use of xxx before definition
Ein extrem nützlicher Hinweis: Eine lokale Variable einer Funktion wurde benutzt, bevor sie initialisiert wurde. Kein Syntax-, sondern mit Sicherheit ein inhaltlicher Fehler.
Wie erkennt der Compiler diesen Sachverhalt? Er wird bei der Definition einer lokalen Variablen zwangsläufig eine interne Datenstruktur anlegen, die den Namen der Variablen, ihren Typ sowie sonstige, intern benutzte Merkmale festhält (etwa ihre Position auf dem Stack, ein hierfür benutztes Register, etc.). Er muß es tun er braucht die Informationen, wenn im Inneren der Funktion auf diese Variable zugegriffen wird.
Dann ist es kaum ein größerer Aufwand, zwei zusätzliche Flags einzuführen: 'Variable wird gelesen' sowie 'Variable wird geschrieben'. Sie werden jeweils gesetzt, wenn die entsprechende Operation ausgeführt wird. Tritt nun eine Situation auf, in der die betreffende Variable gelesen werden soll, aber das Flag 'wird geschrieben' ist noch nicht gesetzt, so liegt offensichtlich der Fall der fehlenden Initialisierung vor, und der Compiler kann für die betreffende Zeile die entsprechende Warnung ausgeben.
Am Ende der Funktion werden die Datenstrukturen aller lokalen Variablen wieder entfernt. Warum also nicht vorher noch einmal hinein schauen? Ist keines der beiden Flags gesetzt, wurde die Variable nie benutzt, ist nur das 'wird geschrieben' Flag gesetzt, wurde sie zwar beschrieben, aber der Wert nie abgefragt, etc. Das bedeutet, daß dem Programmierer weitere Informationen gegeben werden können (z.B. Variable not referenced).
So kann man sich sämtliche, vom Compiler erzeugten Warnungen ansehen: man wird zum Ergebnis kommen, daß die entsprechenden Informationen entweder im Rahmen seiner Arbeit ohnehin anfallen, oder mit minimalem Zusatzaufwand bereitgestellt werden können.
Um nicht mißverstanden zu werden: Dies stellt keine Kritik an der Funktionsweise von Compilern dar. Im Gegenteil, ich begrüße es, daß bereits der Compiler z.B. die fehlende Initialisierung erkennt; schließlich gibt es keinen schnelleren Weg, mich auf diesen Fehler hinzuweisen. Nur eines sollte man nicht tun: Aus der großen Anzahl möglicher Warnungen den Schluß ziehen, es habe eine weitgehende Analyse des Source-Codes stattgefunden.
Die Hersteller von Compilern haben andere Prioritäten (ohne Anspruch auf Vollständigkeit):
Vollständige Implementierung des erst vor kurzem verabschiedeten C++ Standards,
Optimierung des erzeugten Codes (Geschwindigkeit / Größe) und
Speed, Speed, Speed.
Weitere Warnungen werden sie nur insoweit hinzufügen, als es mit minimalem Aufwand realisierbar ist zumindest ist das das Bild, das sich momentan bietet. Der Compiler wird auch weiterhin nicht mehr tun, als linear, Zeile für Zeile ein Source-File zur Zeit zu verarbeiten.
Er untersucht nicht den Kontrollfluß innerhalb einer Funktion und seine möglichen Auswirkungen,
er interessiert sich nicht für bestimmte Eigenschaften von Funktionen, und
es ist ihm völlig unmöglich, Abhängigkeiten zwischen verschiedenen Source-Files zu erkennen und auf mögliche Diskrepanzen hinzuweisen,
um nur einige Aspekte einer Analyse zu nennen, die diesen Namen auch verdienen würde.
Für eine inhaltliche Analyse unseres Source-Codes werden wir deshalb weiterhin auf andere Tools angewiesen sein Tools wie Lint, für den PC etwa in Form von PC-Lint.
Ein einfaches Beispiel wird Ihnen zeigen, wie schnell die Code-Analysen des Compilers versagen. Hier ist es nämlich notwendig, den Kontrollfluß zu untersuchen, um den dort enthaltenen, potentiell tückischen Fehler zu entdecken:
int f( int n)
{
int result;
if (n == 1) result = 0;
else if (n > 0) result = 1;
else if (n < 0) result = 2;
return result;
}
PC-Lint meldet
return result;
ex644.c 8 Warning 644: Variable 'result' (line 3) may not have
been initialized
Tatsächlich, wenn man aufgrund dieser Warnung genauer hinschaut, sieht man, daß es einen Weg durch das if-Statement gibt, bei dem result undefiniert bleibt.
Aber kein Compiler bemerkt hier etwas: Für ihn ist result vor der Rückgabe initialisiert worden leider bedeutet vor für den Compiler aber lediglich, daß es in einer früheren Zeile geschah, mit der Programmlogik hat das nichts zu tun. Das ist auch kein Geheimnis. Im Hilfetext zur Meldung Possible use of xxx before definition des Borland Compilers wird extra hierauf hingewiesen.
Dies kann übrigens schnell ein ziemlich katastrophaler Fehler werden (leider weiß ich das aus eigener Erfahrung): Ist das if-Statement verschachtelt und genügend komplex, verbirgt sich leicht eine Konstellation von Bedingungen, unter der eine bestimmte Variable nicht initialisiert wird. Das macht sich natürlich an einer völlig anderen Stelle bemerkbar, vielleicht als Katastrophe Murphy läßt grüßen.
Der Hersteller von PC-Lint bezeichnet die Technik, die hier angewandt wird, als value tracking: Die möglichen Werte von Variablen werden aus allen verfügbaren Quellen bestimmt und weiter verfolgt. Damit wird z.B. ein Problem in dem folgenden Beispiel aufgedeckt:
#include <string.h>
char foo( char *buff, char code)
{
char *p, option;
p = strchr( buff, code); /* Position von code */
option = *(p+1); /* option folgt code */
return option;
}
PC-Lint meldet an dieser Stelle:
option = *(p+1); /* option folgt code */
ex613.c 8 Warning 613: Possible use of null pointer 'p' in left
argument to operator 'ptr+int'
strchr kann 0 liefern wenn nämlich code nicht in buff enthalten ist. In diesem Fall führt der Zugriff auf *(p+1) zu einem illegalen Zugriff, was alle möglichen negativen Konsequenzen nach sich ziehen kann.
PC-Lint findet diesen möglichen Fehler, weil es über zusätzliche Informationen über strchr verfügt hier die Tatsache, daß strchr eine 0 liefern kann. Diese Funktion ist Teil der C Standard Library, und daher ist ihr Verhalten bekannt.
Das Schöne ist, Sie können PC-Lint derartige Informationen auch für Ihre eigenen Funktionen mitteilen und damit die Diagnosemöglichkeit des value tracking auch für diese nutzen.
Daneben kann Lint etwas, was wohl kein Compiler jemals können wird (wegen der Definition von C und C++): Ein Projekt insgesamt analysieren, d.h. die Interaktionen und Abhängigkeiten über die Grenzen eines Source-Files hinweg betrachten. Damit erhalten Sie u.U. extrem wichtige Hinweise:
Unterschiedliche Typen-Deklarationen: ein Modul geht davon aus, daß
eine externe Variable val ein double ist (File B.C), in Wirklichkeit ist
es jedoch ein float (File A.C) Wird val jetzt innerhalb einer Funktion
in B.C geschrieben, so geschieht das als double. Damit wird aber die im Speicher
folgende Variable überschrieben. Das kann noch schwerer zu finden sein als ein nicht oder
falsch initialisierter Zeiger.
Eigentlich sind Header-Files dafür vorgesehen, daß so etwas nicht passiert, aber Fehler
kommen vor. (Sie können ein Beispiel herunterladen.)
Ihre Initialisierung globaler Variablen könnte davon abhängen, daß
sie in einer bestimmten Reihenfolge erfolgt dies wird vom Compiler aber nicht
garantiert. Das ist ein problematischer Programmierstil, aber vermutlich waren Sie sich
dessen gar nicht bewußt. Wenn diese Initialisierungen in unterschiedlichen Modulen
erfolgen, kann Lint sie auch nur bei der projekt-weiten Analyse
erkennen.
Dies ist übrigens auch eine Zeitbombe: Das Programm hat immer funktioniert, aber auf
einmal 'spinnt' es weil sich die Reihenfolge beim Linken geändert hat. Darauf
kommt so schnell niemand.
Vielleicht werden Compiler in einigen Jahren Programme ebenso gründlich auf mögliche Schwachstellen untersuchen, wie PC-Lint es tut. Vielleicht. Bis dahin ist es ein unverzichtbares Tool für alle Software-Entwickler, die an einem Maximum an Fehlerfreiheit und damit Qualität Ihrer Programme interessiert sind.
Copyright © Helmut Giese, email: hgiese@ratiosoft.com
Parkstr. 41, 34119 Kassel, Tel.: 0561 - 766 59 50, Fax: 0561 - 766 59 51
Königstor 59, 34119 Kassel, Tel.: 0561 - 739 35 30, Fax: 0561 - 739 35 31
Web Design von Bianca Engler: email
internet