Was ist Undefiniertes Verhalten (Undefined Behavior)?

Verhalten, für dessen Ergebnis der Standard von C bzw. C++ keinerlei Anforderungen stellt. Dazu gehören der Überlauf vorzeichenbehafteter Ganzzahlen, Lese- und Schreibzugriffe außerhalb der Grenzen eines Arrays, die Dereferenzierung eines NULL-Zeigers, die Verwendung eines freigegebenen Bereichs und der Linksshift negativer Werte. Da der Compiler beim Optimieren annehmen darf, dass in einem korrekten Programm kein undefiniertes Verhalten auftritt, kann ein Programm zufällig wie erwartet funktionieren oder aber unerwartete Ergebnisse liefern, etwa weil je nach Optimierungseinstellung eine Prüfung verschwindet. Zu unterscheiden ist es vom implementierungsdefinierten Verhalten, das die Implementierung festlegt und dokumentiert.

Japanische Bezeichnung: 未定義動作

„Undefiniertes Verhalten (Undefined Behavior)“ kommt in 10 Übungsfragen aus 2 Themen vor, z. B. Grundlagen der Programmiersprache C, Compiler.

Fachgebiet: Grundlagen der Programmiersprache C

Typische Beispiele

Dazu gehören der Überlauf bei Operationen mit vorzeichenbehafteten Ganzzahlen (etwa INT_MAX + 1), Lese- und Schreibzugriffe auf ein Array außerhalb seiner Grenzen, die Verwendung des Werts einer nicht initialisierten automatischen Variable, Lesen oder Schreiben über einen NULL-Zeiger oder einen Dangling Pointer, die doppelte Freigabe desselben Bereichs, eine ganzzahlige Division durch 0 sowie Shifts um die Typbreite oder mehr und der Linksshift negativer Werte. Der Überlauf vorzeichenloser Ganzzahlen ist kein undefiniertes Verhalten; für ihn ist festgelegt, dass das Ergebnis auf den Rest der Division durch eine Zweierpotenz zurückfällt.

Zusammenhang mit der Compiler-Optimierung

Weil der Compiler annehmen darf, dass „kein undefiniertes Verhalten auftritt“, behandelt er x + 1 > x mitunter als stets wahr oder entfernt eine Prüfung p == NULL, die nach dem Lesen von *p steht. Ein Programm, das mit -O0 wie erwartet lief, kann daher nach dem Wechsel zu -O2 oder einer neueren Compilerversion nicht mehr funktionieren. Die Ursache ist dann kein Compilerfehler, sondern ein Programm, das die Voraussetzungen des Standards verletzt.

Finden und Vermeiden

Bei gcc und clang erkennt -fsanitize=undefined (UBSan) zur Laufzeit etwa Überläufe vorzeichenbehafteter Ganzzahlen und -fsanitize=address (ASan) Zugriffe außerhalb der Grenzen sowie Zugriffe nach der Freigabe. Auch die Warnungen von -Wall und -Wextra, Werkzeuge zur statischen Analyse und Programmierrichtlinien wie MISRA C helfen, undefiniertes Verhalten zu reduzieren.

Jetzt üben: Fragen zu Undefiniertes Verhalten (Undefined Behavior)

Welche Aussage beschreibt das undefinierte Verhalten im Sinne des C-Standards richtig?

  1. AEin Verhalten, für das der Standard nichts festlegt; der Compiler darf beim Optimieren annehmen, dass es nicht auftritt, und das Ergebnis kann unerwartet sein
  2. BEin Verhalten, dessen Ergebnis jede Implementierung festlegen und dokumentieren muss
  3. CEin Verhalten, das zur Laufzeit stets erkannt wird und das Programm mit einer Fehlermeldung anhält
Antwort und Erläuterung anzeigen

Antwort: A. Ein Verhalten, für das der Standard nichts festlegt; der Compiler darf beim Optimieren annehmen, dass es nicht auftritt, und das Ergebnis kann unerwartet sein

Undefiniertes Verhalten ist ein Verhalten, für das der Standard keinerlei Anforderungen stellt, etwa beim Überlauf vorzeichenbehafteter Ganzzahlen, bei Zugriffen auf ein Array außerhalb seiner Grenzen, bei der Dereferenzierung eines NULL-Zeigers oder bei Zugriffen nach der Freigabe. Mitunter funktioniert es zufällig wie erwartet, doch weil der Compiler beim Optimieren annehmen darf, dass es „in einem korrekten Programm nicht auftritt“, kann sich das Ergebnis schon durch andere Optimierungseinstellungen oder eine andere Compilerversion ändern. Was dagegen die Implementierung festlegen und dokumentieren muss, wie die Größe von int oder ob char ein Vorzeichen hat, heißt implementierungsdefiniertes Verhalten. Mit -fsanitize=undefined lassen sich bei gcc und clang manche Fälle von undefiniertem Verhalten zur Laufzeit erkennen.

int und unsigned int haben 32 Bit. Was davon ist nach dem C-Standard undefiniertes Verhalten?

  1. AZu einer unsigned-int-Variablen mit dem Maximalwert 4294967295 wird 1 addiert
  2. BEin negativer int-Wert wird mit >> um 1 Bit nach rechts geschoben
  3. CZu einer int-Variablen mit dem Maximalwert 2147483647 wird 1 addiert
Antwort und Erläuterung anzeigen

Antwort: C. Zu einer int-Variablen mit dem Maximalwert 2147483647 wird 1 addiert

Überschreitet das Ergebnis einer Operation mit vorzeichenbehafteten Ganzzahlen den darstellbaren Bereich, wie bei 2147483647 + 1 mit int, ist das undefiniertes Verhalten. Für Operationen mit vorzeichenlosen Ganzzahlen ist festgelegt, dass sie auf den Rest der Division durch 2³² zurückfallen; 4294967295 + 1 ergibt daher 0 und ist kein undefiniertes Verhalten. Ob ein Rechtsshift eines negativen Werts als arithmetischer Shift oder als logischer Shift erfolgt, bestimmt die Implementierung (implementierungsdefiniertes Verhalten); bei gcc ist es ein arithmetischer Shift. Ein Linksshift eines negativen Werts ist dagegen undefiniertes Verhalten.

Häufige Fragen

Was ist der Unterschied zwischen undefiniertem und implementierungsdefiniertem Verhalten?

Beim implementierungsdefinierten Verhalten wählt die Implementierung ein bestimmtes Verhalten und dokumentiert es, etwa die Größe von int, ob char ein Vorzeichen hat oder das Ergebnis eines Rechtsshifts negativer Werte. Beim undefinierten Verhalten stellt der Standard keinerlei Anforderungen; das Ergebnis darf sich selbst bei derselben Implementierung je nach Situation unterscheiden. Portable Programme kennzeichnen die Stellen, die sich auf implementierungsdefiniertes Verhalten stützen, und lösen kein undefiniertes Verhalten aus.

Ist undefiniertes Verhalten unproblematisch, solange das Programm funktioniert?

Nein. Auch wenn es in der aktuellen Umgebung zufällig wie erwartet funktioniert, kann sich das Ergebnis schon ändern, wenn sich Optimierungseinstellungen, die Compilerversion oder der umgebende Code ändern. Auch aus Sicht der Sicherheit kann undefiniertes Verhalten wie ein Schreibzugriff außerhalb der Grenzen für Angriffe missbraucht werden.

„Undefiniertes Verhalten (Undefined Behavior)“ in den Erläuterungen

Fragen mit „Undefiniertes Verhalten (Undefined Behavior)“ (10)

  1. In C ist der Überlauf von vorzeichenbehafteten Ganzzahlen undefiniertes Verhalten. Welcher Code kann ein optimierender Compiler unter dieser Voraussetzung für die obige Funktion erzeugen?
  2. Welche Aussage beschreibt das undefinierte Verhalten im Sinne des C-Standards richtig?
  3. int und unsigned int haben 32 Bit. Was davon ist nach dem C-Standard undefiniertes Verhalten?
  4. uint8_t ist ein vorzeichenloser 8-Bit-Ganzzahltyp. Welche Ausgabe erzeugt der obige Code, der x zehnmal erhöht, bei der Ausführung?
  5. int habe 32 Bit (Maximalwert 2147483647). Was ist das größte n, für das die obige Funktion fact(n) die Fakultät von n ohne Überlauf korrekt zurückgibt?
  6. Was trifft auf den Zeiger zu, den die obige Funktion make zurückgibt?
  7. Warum wird in C empfohlen, unmittelbar nach free(p); die Anweisung p = NULL; zu schreiben?
  8. Was trifft in C auf die Dereferenzierung eines NULL-Zeigers zu (wenn *p gelesen wird, während p NULL ist)?
  9. Welche Ausgabe erzeugt der obige Code, der das mit #define definierte Makro SQUARE verwendet, bei der Ausführung?
  10. Wird die obige Funktion mit gcc und aktivierter Optimierung kompiliert, kann die Prüfung if (p == NULL) entfernt werden. Was ist der Grund dafür?

Verwandte Begriffe

Zeiger (Pointer) Variable Array Arithmetischer Shift Rekursion Stack (Stapelspeicher) Dangling Pointer (hängender Zeiger) Dynamische Speicherreservierung Heap Seitenfehler (Page Fault)

Zur Glossarübersicht