Lassen Sie mich das klar sagen: Wir rufen in unseren Programmen kein undefiniertes Verhalten auf . Es ist niemals eine gute Idee, Punkt. Es gibt seltene Ausnahmen von dieser Regel; Zum Beispiel, wenn Sie ein Bibliotheksimplementierer sind, der offsetof implementiert . Wenn Ihr Fall unter eine solche Ausnahme fällt, wissen Sie dies wahrscheinlich bereits. In diesem Fall wissen wir, dass die Verwendung nicht initialisierter automatischer Variablen ein undefiniertes Verhalten ist .
Compiler sind mit Optimierungen in Bezug auf undefiniertes Verhalten sehr aggressiv geworden, und wir können viele Fälle finden, in denen undefiniertes Verhalten zu Sicherheitslücken geführt hat. Der berüchtigtste Fall ist wahrscheinlich das Entfernen der Nullzeigerprüfung des Linux-Kernels, das ich in meiner Antwort auf den C ++ - Kompilierungsfehler erwähne . wo eine Compileroptimierung um undefiniertes Verhalten eine endliche Schleife in eine unendliche verwandelte.
Wir können die gefährlichen Optimierungen von CERT und den Verlust der Kausalität ( Video ) lesen , in denen unter anderem Folgendes steht:
Compiler-Autoren nutzen zunehmend undefinierte Verhaltensweisen in den Programmiersprachen C und C ++, um die Optimierungen zu verbessern.
Häufig beeinträchtigen diese Optimierungen die Fähigkeit von Entwicklern, eine Ursache-Wirkungs-Analyse ihres Quellcodes durchzuführen, dh die Abhängigkeit der nachgelagerten Ergebnisse von früheren Ergebnissen zu analysieren.
Folglich beseitigen diese Optimierungen die Kausalität in der Software und erhöhen die Wahrscheinlichkeit von Softwarefehlern, -fehlern und -schwachstellen.
Insbesondere in Bezug auf unbestimmte Werte bietet der C-Standardfehlerbericht 451: Instabilität nicht initialisierter automatischer Variablen einige interessante Informationen . Es wurde noch nicht gelöst, führt jedoch das Konzept der wackeligen Werte ein, was bedeutet, dass sich die Unbestimmtheit eines Wertes durch das Programm ausbreiten kann und an verschiedenen Punkten im Programm unterschiedliche unbestimmte Werte haben kann.
Ich kenne keine Beispiele, wo dies passiert, aber an diesem Punkt können wir es nicht ausschließen.
Echte Beispiele, nicht das erwartete Ergebnis
Es ist unwahrscheinlich, dass Sie zufällige Werte erhalten. Ein Compiler könnte die Abwesenheitsschleife insgesamt optimieren. Zum Beispiel mit diesem vereinfachten Fall:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r ;
}
}
clang optimiert es weg ( live sehen ):
updateEffect(int*): # @updateEffect(int*)
retq
oder vielleicht alle Nullen bekommen, wie in diesem modifizierten Fall:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r%255 ;
}
}
live sehen :
updateEffect(int*): # @updateEffect(int*)
xorps %xmm0, %xmm0
movups %xmm0, 64(%rdi)
movups %xmm0, 48(%rdi)
movups %xmm0, 32(%rdi)
movups %xmm0, 16(%rdi)
movups %xmm0, (%rdi)
retq
Beide Fälle sind durchaus akzeptable Formen undefinierten Verhaltens.
Beachten Sie, wenn wir uns auf einem Itanium befinden, könnten wir einen Trap-Wert erhalten :
[...] wenn das Register zufällig einen speziellen Nicht-Nichts-Wert enthält, lesen Sie die Registerfallen mit Ausnahme einiger Anweisungen [...]
Andere wichtige Hinweise
Es ist interessant festzustellen, dass im UB Canaries-Projekt eine Varianz zwischen gcc und clang festgestellt wurde, wie bereit sie sind, undefiniertes Verhalten in Bezug auf nicht initialisiertes Gedächtnis auszunutzen. Der Artikel stellt fest ( Hervorhebung von mir ):
Natürlich müssen wir uns völlig klar darüber sein, dass eine solche Erwartung nichts mit dem Sprachstandard zu tun hat und alles damit, was ein bestimmter Compiler tut, entweder weil die Anbieter dieses Compilers diese UB nicht ausnutzen wollen oder nur weil sie noch nicht dazu gekommen sind, es auszunutzen . Wenn es keine wirkliche Garantie des Compiler-Anbieters gibt, möchten wir sagen, dass noch nicht genutzte UBs Zeitbomben sind : Sie warten darauf, nächsten Monat oder nächstes Jahr loszulegen, wenn der Compiler etwas aggressiver wird.
Wie Matthieu M. betont, ist auch das , was jeder C-Programmierer über undefiniertes Verhalten # 2/3 wissen sollte, für diese Frage relevant. Es heißt unter anderem ( Hervorhebung von mir ):
Es ist wichtig und beängstigend zu erkennen, dass nahezu jede
Optimierung, die auf undefiniertem Verhalten basiert, jederzeit in Zukunft für fehlerhaften Code ausgelöst werden kann . Inlining, Loop-Unrolling, Speicherförderung und andere Optimierungen werden immer besser, und ein wesentlicher Teil ihres Bestehens besteht darin, sekundäre Optimierungen wie die oben genannten bereitzustellen.
Für mich ist dies zutiefst unbefriedigend, zum Teil, weil der Compiler unweigerlich beschuldigt wird, aber auch, weil riesige C-Code-Körper Landminen sind, die nur darauf warten, explodiert zu werden.
Der Vollständigkeit halber sollte ich wahrscheinlich erwähnen, dass Implementierungen undefiniertes Verhalten gut definieren können, zum Beispiel erlaubt gcc das Durchstoßen von Typen durch Gewerkschaften, während dies in C ++ wie undefiniertes Verhalten erscheint . Wenn dies der Fall ist, sollte die Implementierung dies dokumentieren und dies ist normalerweise nicht portierbar.