Undefiniertes Verhalten
Der C-Sprachstandard legt das beobachtbare Verhalten von C-Programmen präzise fest, mit Ausnahme der folgenden Kategorien:
- undefiniertes Verhalten – es gibt keine Einschränkungen für das Verhalten des Programms. Beispiele für undefiniertes Verhalten sind Speicherzugriffe außerhalb von Array-Grenzen, Überlauf vorzeichenbehafteter Ganzzahlen, Dereferenzierung eines Nullzeigers, Änderung desselben Skalars mehr als einmal in einem Ausdruck ohne Sequenzpunkte, Zugriff auf ein Objekt über einen Zeiger eines anderen Typs usw. Compiler sind nicht verpflichtet, undefiniertes Verhalten zu diagnostizieren (obwohl viele einfache Situationen diagnostiziert werden), und das kompilierte Programm muss nichts Sinnvolles tun.
- nicht spezifiziertes Verhalten – zwei oder mehr Verhaltensweisen sind zulässig und die Implementierung ist nicht verpflichtet, die Auswirkungen jedes Verhaltens zu dokumentieren. Beispiele hierfür sind die Auswertungsreihenfolge, ob identische String-Literale unterschiedlich sind usw. Jedes nicht spezifizierte Verhalten führt zu einem von mehreren gültigen Ergebnissen und kann bei Wiederholung im selben Programm ein anderes Ergebnis liefern.
- implementierungsdefiniertes Verhalten – nicht spezifiziertes Verhalten, bei dem jede Implementierung dokumentiert, wie die Auswahl getroffen wird. Beispiele: Anzahl der Bits in einem Byte oder ob eine vorzeichenbehaftete Ganzzahl-Rechtsverschiebung arithmetisch oder logisch ist.
- lokalespezifisches Verhalten – implementierungsdefiniertes Verhalten, das von der aktuell gewählten Locale abhängt. Beispiel: ob islower für andere Zeichen als die 26 Kleinbuchstaben der lateinischen Schrift true zurückgibt.
(Hinweis: Streng konforme Programme hängen nicht von nicht spezifiziertem, undefiniertem oder implementierungsdefiniertem Verhalten ab)
Compiler sind verpflichtet, Diagnosemeldungen (Fehler oder Warnungen) für Programme auszugeben, die gegen C-Syntaxregeln oder semantische Einschränkungen verstoßen, auch wenn ihr Verhalten als undefiniert oder implementierungsdefiniert spezifiziert ist oder wenn der Compiler eine Spracherweiterung anbietet, die ihm erlaubt, ein solches Programm zu akzeptieren. Diagnosen für undefiniertes Verhalten sind andernfalls nicht erforderlich.
Inhalt |
[bearbeiten] UB und Optimierung
Da korrekte C-Programme frei von undefiniertem Verhalten sind, können Compiler unerwartete Ergebnisse liefern, wenn ein Programm, das tatsächlich UB aufweist, mit aktivierter Optimierung kompiliert wird.
Zum Beispiel,
[bearbeiten] Vorzeichenüberlauf
int foo(int x) { return x + 1 > x; // either true or UB due to signed overflow }
kann kompiliert werden als (Demo)
foo: mov eax, 1 ret
[bearbeiten] Zugriff außerhalb der Grenzen
int table[4] = {0}; int exists_in_table(int v) { // return 1 in one of the first 4 iterations or UB due to out-of-bounds access for (int i = 0; i <= 4; i++) if (table[i] == v) return 1; return 0; }
kann kompiliert werden als (Demo)
exists_in_table: mov eax, 1 ret
[bearbeiten] Nicht initialisierte Skalare
kann die folgende Ausgabe produzieren (beobachtet mit einer älteren Version von gcc)
p is true p is false
kann kompiliert werden als (Demo)
f: mov eax, 42 ret
[bearbeiten] Ungültige Skalare
int f(void) { _Bool b = 0; unsigned char* p = (unsigned char*)&b; *p = 10; // reading from b is now UB return b == 0; }
kann kompiliert werden als (Demo)
f: mov eax, 11 ret
[bearbeiten] Nullzeiger-Dereferenzierung
int foo(int* p) { int x = *p; if (!p) return x; // Either UB above or this branch is never taken else return 0; } int bar() { int* p = NULL; return *p; // Unconditional UB }
kann kompiliert werden als (Demo)
foo: xor eax, eax ret bar: ret
[bearbeiten] Zugriff auf einen an realloc übergebenen Zeiger
Wählen Sie clang, um die angezeigte Ausgabe zu beobachten.
Mögliche Ausgabe
12
[bearbeiten] Endlosschleife ohne Nebeneffekte
Wählen Sie clang, um die angezeigte Ausgabe zu beobachten.
#include <stdio.h> int fermat() { const int MAX = 1000; // Endless loop with no side effects is UB for (int a = 1, b = 1, c = 1; 1;) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return 1; ++a; if (a > MAX) { a = 1; ++b; } if (b > MAX) { b = 1; ++c; } if (c > MAX) c = 1; } return 0; } int main(void) { if (fermat()) puts("Fermat's Last Theorem has been disproved."); else puts("Fermat's Last Theorem has not been disproved."); }
Mögliche Ausgabe
Fermat's Last Theorem has been disproved.
[bearbeiten] Referenzen
- C23-Standard (ISO/IEC 9899:2024)
- 3.4 Verhalten (S. TBD)
- 4 Konformität (S. TBD)
- C17-Standard (ISO/IEC 9899:2018)
- 3.4 Verhalten (S. 3-4)
- 4 Konformität (S. 8)
- C11-Standard (ISO/IEC 9899:2011)
- 3.4 Verhalten (S. 3-4)
- 4/2 Undefiniertes Verhalten (S. 8)
- C99-Standard (ISO/IEC 9899:1999)
- 3.4 Verhalten (S. 3-4)
- 4/2 Undefiniertes Verhalten (S. 7)
- C89/C90-Standard (ISO/IEC 9899:1990)
- 1.6 DEFINITIONEN VON BEGRIFFEN
[bearbeiten] Siehe auch
| C++-Dokumentation für Undefiniertes Verhalten
|
[bearbeiten] Externe Links
| 1. | What Every C Programmer Should Know About Undefined Behavior #1/3 |
| 2. | What Every C Programmer Should Know About Undefined Behavior #2/3 |
| 3. | What Every C Programmer Should Know About Undefined Behavior #3/3 |
| 4. | Undefiniertes Verhalten kann zu Zeitreisen führen (unter anderem, aber Zeitreisen ist das verrückteste). |
| 5. | Understanding Integer Overflow in C/C++ |
| 6. | Undefined Behavior and Fermat’s Last Theorem |
| 7. | Fun with NULL pointers, part 1 (lokaler Exploit in Linux 2.6.30 verursacht durch UB wegen Nullzeiger-Dereferenzierung) |