Compilerfehler

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Nichts ist perfekt, und auch ein Compiler ist es nicht. Er kann also Fehler haben, sogenannte Compilerfehler oder Compiler-Bugs. Erfahrungsgemäß entpuppen sich mindestens 99% der berichteten Compilerfehler als Fehler in der Anwendung, die ihre Ursache oft in einem nicht ausreichenden Verständnis der verwendeten Programmiersprache haben.

Bevor also ein angeblicher Compilerfehler berichtet wird, sollte man sicher sein, dass nicht einer der folgenden, häufigen Programmierfehler vorliegt.

Die häufigsten Nicht-Fehler

Häufig führen Missverständnisse über oder Wissenslücken in der Programmiersprache zu Fehlinterpretationen des Compilerverhaltens als Compilerfehler. Die häufigsten Irrtümer sind:

Compiler-Optimierungen

Ein optimierender Compiler kann

  • die Reihenfolge von Instruktionen ändern
  • nicht verwendeten Code entfernen
  • nicht verwendete Variablen eliminieren
  • Schleifen, die keinen Effekt haben, entfernen
  • komplexen Code vereinfachen, rekursive Funktionsaufrufe in Schleifen umwandeln, etc.

Häufig sorgt das Wegoptimiern von Warteschleifen für Verwirrung. Code wie

for (int i=0; i <= 100; i++);

hat keine Wirkung weil i nicht weiterverwendet wird und kann daher entfernt werden! Aufgabe eines Optimizers ist es ja gerade, Programme schneller zu machen und daher sind solche nutzlosen Zeitfresser für ihn ein gefundenes Fressen.

Alles in allem darf ein C/C++-Compiler den Code so optimieren, daß lediglich die Nebeneffekte der Funktionen unverändert bleiben. Nebeneffekte sind z.B. das Verändern globaler Variablen. Beispiel:

int z;

int foo (int x)
{
    int a = 1;
    int b;

    // b (und damit a) werden im weiteren Verlauf zwar noch verwendet, aber
    // der Wert in b ist dem Compiler bekannt. Er braucht also keine Variable
    // "b" anzulegen. b ist also in einem Debugger u.U. nicht mehr vorhanden.
    b = a+3;

    z = x;
    // z ist nicht volatile und wird mit 0 beschrieben. Die vorangegangene
    // Zuweisung darf also wegoptimiert werden.
    z = 0;

    // Welchen Wert hat b? "return b" ist gleichbedeutend mit "return 4".
    return b;
}

In Summa hat die obige Funktion also den gleichen effekt wie

int z;

int foo (int x)
{
    z = 0;

    return 4;
}

Interrupt-Programmierung

Beim Datenaustausch zwischen Interrupt-Routinen und Programm ist zu beachten:

  • C-Programme sind immer sequenziell, und davon geht der Compiler auch aus. Werden in einer ISR unter der Hand Daten des Hauptprogramms verändert, dann muss das in der Variablendeklaration durch das Schlüsselwort volatile dem Compiler mitgeteilt werden.
  • Der Zugriff auf diese Daten muss atomar, also ununterbrechbar und damit interruptfest erfolgen. Wenn z. B. ein 16-Bit-Wert aus dem Speicher gelesen wird, und dazu zwei 8-Bit-Instruktionen erforderlich sind, dann darf zwischen diesen Instruktionen keine ISR ausgeführt werden, die ebenfalls diese Daten verwendet oder ändert.

Position von const und volatile bei Pointern

Ein

volatile char * pPtr;

hat eine andere Bedeutung und damit auch ein anderes Optimierungsverhalten als ein

char * volatile pPtr;

Im ersten Fall ist das, worauf der Pointer zeigt, volatile. Im zweiten Fall ist der Pointer selbst volatile. Ist sowohl der Pointer als auch das Ziel des Pointers volatile, so lautet die Definition

volatile char * volatile pPtr;

In Gedanken trennt man die Definition beim "*" und ordnet das volatile dem jeweiligen Teil der Definition zu.

(Implizite) Typ-Umwandlungen und Bereichsüberläufe

Wenn Zwischenergebnisse nicht in den Wertebereich passen, dann werden die übergelaufenen Bits entfernt. Das kann auch dann geschehen, wenn das Endergebnis nicht überläuft.

Bei Rechnung mit Vorzeichen ist das Ergebnis bei Überlauf auch in den unteren Bits undefiniert, nur bei Rechnung ohne Vorzeichen ist das Verhalten bei Überlauf klar definiert.

Stacküberlauf

Der Stack wächst in den Bereich normaler Daten und überschreibt diese — oder umgekehrt. Das Programm zeigt unvorhersagbares Verhalten oder stürzt komplett ab.

Aussagekräftige Fehlerbeschreibung

Wenn es sich wirklich um einen Compilerfehler handelt, bzw. die Hinweise darauf sich verdichten, ist es sinnvoll, z. B. in einem Forum nachzufragen, um Hilfe und Ratschläge zu bekommen. Manchmal ist ein Fehler schon bekannt und es kann gesagt werden, wie der Fehler umschifft (eng. Workaround) werden kann.

Dazu ist es notwendig, einen Testfall zu erstellen, damit andere den Fehler nachvollziehen und beurteilen können.

Compiler sind hochkomplexe Programme, und ohne die Eingabe — also ohne die Quelldatei, die den Fehler hervorruft — sind praktisch keine Aussagen möglich, ob es sich bei einem Phänomen um einen Compilerfehler handelt oder nicht.

Compiler-Version und System-Info
Um welche Compilerversion handelt es sich? Läuft er unter Linux oder Microsoft Windows? Wie wurde der Compiler erzeugt? All diese Informationen zeigt gcc an, wenn man ihn mit der Option –v aufruft.
Kommandozeile
Sämtliche Kommandozeilen-Optionen, auch wenn es viele sind.
Quelldatei
Die Eingabe von gcc ist nicht eine C-Datei, sondern die precompilierte C-Datei. Diese kannst du erzeugen mit dem Schalter –save–temps auf der Kommandozeile. Dies erstellt eine i-Datei, die von jedermann ohne Probleme verwendet werden kann.
Eine C-Datei ist i.d.R. nicht zu gebrauchen, weil dort andere Dateien includet werden. Wenn du also eine Quelldatei hast, in der ein Fehler auftritt, und diese Zeilen enthält wie #include "lcd.h", dann kann niemand was damit anfangen.
Es ist zudem vollkommen unnötig und überflüssig, ein komplettes Projekt zu posten! Niemand will sich gerne durch deine Makefiles hangeln. Ein i-File ist vollkommen ausreichend.

Bekannte Fehler

Weblinks