Ist es legal, dass Quellcode mit undefiniertem Verhalten den Compiler zum Absturz bringt?


85

Nehmen wir an, ich kompiliere schlecht geschriebenen C ++ - Quellcode, der undefiniertes Verhalten hervorruft, und daher (wie sie sagen) "kann alles passieren".

Aus der Perspektive dessen, was die C ++ - Sprachspezifikation in einem "konformen" Compiler als akzeptabel erachtet, bedeutet "alles" in diesem Szenario, dass der Compiler abstürzt (oder meine Passwörter stiehlt oder sich auf andere Weise beim Kompilieren schlecht verhält oder fehlerhaft ist) oder ist Der Umfang des undefinierten Verhaltens beschränkt sich speziell darauf, was passieren kann, wenn die resultierende ausführbare Datei ausgeführt wird.


22
"UB ist UB. Lebe damit" ... Nein, warte. "Bitte poste eine MCVE." ... Nein, warte. Ich liebe die Frage für all die Reflexe, die sie unangemessen auslöst. :-)
Yunnosch

14
Es gibt wirklich keine Einschränkung, weshalb UB Nasendämonen beschwören kann .
Einige Programmierer Typ

15
UB kann den Autor dazu bringen, eine Frage zu SO zu stellen. : P
Tanveer Badar

45
Unabhängig davon, was der C ++ - Standard sagt, würde ich als Compiler-Autor dies sicherlich als Fehler in meinem Compiler betrachten. Wenn Sie dies sehen, reichen Sie einen Fehlerbericht ein.
John

9
@LeifWillerts Das war in den 80ern. Ich erinnere mich nicht an das genaue Konstrukt, denke aber, dass es von der Verwendung eines gewundenen Variablentyps abhängt. Nachdem ich einen Ersatz eingesetzt hatte, hatte ich einen Moment "Was habe ich gedacht - die Dinge funktionieren nicht so". Ich habe dem Compiler nicht die Schuld gegeben, dass er das Konstrukt abgelehnt hat, sondern nur, dass er den Computer neu gestartet hat. Ich bezweifle, dass heute jemand auf diesen Compiler stoßen würde. Es war der HP C Cross Compiler für den HP 64000, der auf den 68000-Mikroprozessor abzielte.
Avi Berger

Antworten:


71

Die normative Definition von undefiniertem Verhalten lautet wie folgt:

[defns.undefined]

Verhalten, für das diese Internationale Norm keine Anforderungen stellt

[Hinweis: Undefiniertes Verhalten kann erwartet werden, wenn diese Internationale Norm keine explizite Definition des Verhaltens enthält oder wenn ein Programm ein fehlerhaftes Konstrukt oder fehlerhafte Daten verwendet. Das zulässige undefinierte Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung). Viele fehlerhafte Programmkonstrukte erzeugen kein undefiniertes Verhalten. Sie müssen diagnostiziert werden. Die Auswertung eines konstanten Ausdrucks zeigt niemals ein Verhalten, das explizit als undefiniert angegeben ist. - Endnote]

Obwohl die Notiz selbst nicht normativ ist, beschreibt sie eine Reihe von Verhaltensweisen, von denen bekannt ist, dass sie Implementierungen aufweisen. Ein Absturz des Compilers (bei dem die Übersetzung abrupt beendet wird) ist laut diesem Hinweis legitim. Aber wirklich, wie der normative Text sagt, setzt der Standard weder der Ausführung noch der Übersetzung Grenzen. Wenn eine Implementierung Ihre Passwörter stiehlt, verstößt dies nicht gegen einen im Standard festgelegten Vertrag.


42
Das heißt, wenn Sie tatsächlich einen Compiler dazu bringen können, beliebigen Code zur Kompilierungszeit ohne Sandboxing auszuführen, wären verschiedene Sicherheitsleute sehr daran interessiert, davon zu erfahren. Gleiches gilt für das Segfaulting des Compilers.
Kevin

66
Das Gleiche gilt für das, was Kevin gesagt hat. Als C / C ++ / etc-Compiler-Ingenieur in einer früheren Karriere war unsere Position, dass undefiniertes Verhalten Ihr Programm zum Absturz bringen , Ihre Ausgabedaten vermasseln und Ihr Haus in Brand setzen könnte, was auch immer. Der Compiler sollte jedoch niemals abstürzen, unabhängig von der Eingabe. (Es gibt möglicherweise keine hilfreichen Fehlermeldungen, aber es sollte eine Art Diagnose und Exit erzeugen, anstatt nur zu schreien. CTHULHU TAKE THE WHEEL und Segfaulting.)
Ti Strga

8
@ TiStrga Ich wette, Cthulhu wäre ein großartiger F1-Fahrer.
Zeta-Band

35
"Wenn eine Implementierung Ihre Passwörter stiehlt, verstößt dies nicht gegen einen im Standard festgelegten Vertrag." Das stimmt, unabhängig davon, ob der Code UB hat, nicht wahr? Der Standard schreibt nur vor, was das kompilierte Programm tun soll - ein Compiler, der den Code korrekt kompiliert, dabei aber Ihre Passwörter stiehlt, würde dem Standard nicht widersprechen.
Carmeister

8
@Carmeister, oooh, das ist ein guter Punkt, ich werde die Leute daran erinnern, wenn die Argumente "UB gibt dem Compiler die Erlaubnis, einen Atomkrieg zu beginnen" auftauchen. Nochmal.
Ilkkachu

8

Die meisten Arten von UB, über die wir uns normalerweise Sorgen machen, wie NULL-Deref oder Division durch Null, sind Laufzeit- UB. Das Kompilieren einer Funktion, die bei Ausführung zur Laufzeit UB führen würde, darf nicht zum Absturz des Compilers führen. Es sei denn, es kann vielleicht beweisen, dass die Funktion (und dieser Pfad durch die Funktion) definitiv wird durch das Programm ausgeführt werden.

(2. Gedanke: Vielleicht habe ich beim Kompilieren nicht berücksichtigt, dass Template / Constexpr eine Evaluierung erforderlich macht. Möglicherweise darf UB während der Übersetzung willkürliche Verrücktheiten verursachen, selbst wenn die resultierende Funktion niemals aufgerufen wird.)

Das Verhalten während der Übersetzung des ISO C ++ - Zitats in der Antwort von @ StoryTeller ähnelt der im ISO C-Standard verwendeten Sprache. C enthält keine Vorlagen oder constexprobligatorische Auswertungen zur Kompilierungszeit.

Aber lustige Tatsache : ISO C sagt in einem Hinweis, dass die Beendigung einer Übersetzung mit einer Diagnosemeldung erfolgen muss. Oder "Verhalten während der Übersetzung ... auf dokumentierte Weise". Ich denke nicht, dass "die Situation vollständig ignorieren" so verstanden werden könnte, dass die Übersetzung gestoppt wird.


Alte Antwort, geschrieben, bevor ich etwas über die Übersetzungszeit von UB erfuhr. Dies gilt jedoch für Runtime-UB und ist daher möglicherweise immer noch nützlich.


Es gibt keine UB, die zur Kompilierungszeit passiert . Es kann für den Compiler entlang eines bestimmten Ausführungspfads sichtbar sein , aber in C ++ ist dies nicht geschehen erst wenn die Ausführung diesen Ausführungspfad über eine Funktion erreicht hat.

Fehler in einem Programm, die das Kompilieren unmöglich machen, sind nicht UB, sondern Syntaxfehler. Ein solches Programm ist in der C ++ - Terminologie "nicht wohlgeformt" (wenn ich meine Standardsprache korrekt habe). Ein Programm kann wohlgeformt sein, aber UB enthalten. Unterschied zwischen undefiniertem Verhalten und schlecht geformt, keine Diagnosemeldung erforderlich

Wenn ich nichts falsch verstehe, muss dieses Programm in ISO C ++ korrekt kompiliert und ausgeführt werden, da die Ausführung niemals die Division durch Null erreicht. (In der Praxis ( Godbolt ) machen gute Compiler nur funktionierende ausführbare Dateien. Gcc / clang warnt davor, x / 0aber nicht davor , selbst wenn sie optimiert werden. Trotzdem versuchen wir zu sagen, wie niedrig ISO C ++ die Qualität der Implementierung sein lässt. Überprüfen Sie also gcc / clang ist kaum ein nützlicher Test, außer um zu bestätigen, dass ich das Programm richtig geschrieben habe.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Ein Anwendungsfall hierfür könnte der C-Präprozessor oder constexprVariablen und die Verzweigung dieser Variablen sein, was zu Unsinn in einigen Pfaden führt, die für diese Auswahl von Konstanten nie erreicht werden.

Es kann davon ausgegangen werden, dass Ausführungspfade, die eine zur Kompilierungszeit sichtbare UB verursachen, niemals verwendet werden, z. B. könnte ein Compiler für x86 eine ud2(Ursache für eine Anweisung für unzulässige Anweisungen) als Definition für ausgeben cause_UB(). Oder innerhalb einer Funktion kann der Zweig entfernt werden , wenn eine Seite von if()zu nachweisbarem UB führt .

Aber der Compiler muss immer noch alles andere auf vernünftige und korrekte Weise kompilieren . Alle Pfade, die UB nicht begegnen (oder deren Nachweis nicht nachgewiesen werden kann), müssen weiterhin zu asm kompiliert werden, das so ausgeführt wird, als ob die abstrakte C ++ - Maschine es ausführen würde.


Sie könnten argumentieren, dass die bedingungslose UB in der Kompilierungszeit maineine Ausnahme von dieser Regel darstellt. Oder auf andere Weise zur Kompilierungszeit nachweisbar, dass die Ausführung ab beginntmain tatsächlich die garantierte UB erreicht.

Ich würde immer noch argumentieren, dass das Verhalten von legalen Compilern das Produzieren einer Granate beinhaltet, die explodiert, wenn sie ausgeführt wird. Oder plausibler, eine Definition maindavon besteht aus einer einzigen illegalen Anweisung. Ich würde behaupten, wenn Sie das Programm nie ausführen, gibt es noch keine UB. Der Compiler selbst darf nicht explodieren, IMO.


Funktionen, die mögliche oder nachweisbare UB innerhalb von Zweigen enthalten

UB auf einem bestimmten Ausführungspfad reicht rechtzeitig zurück, um den gesamten vorherigen Code zu "kontaminieren". In der Praxis können Compiler diese Regel jedoch nur dann nutzen, wenn sie tatsächlich nachweisen können , dass Ausführungspfade zu UB führen, die zur Kompilierungszeit sichtbar sind. z.B

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

Der Compiler muss asm erstellen, das für alle xanderen als 3 funktioniert , bis zu den Punkten, an denen x * 5bei INT_MIN und INT_MAX ein UB mit Vorzeichenüberlauf verursacht wird. Wenn diese Funktion nie mit aufgerufen wird x==3, enthält das Programm natürlich keine UB und muss wie geschrieben funktionieren.

Wir hätten genauso gut if(x == 3) __builtin_unreachable();in GNU C schreiben können , um dem Compiler mitzuteilen, dass dies xdefinitiv nicht 3 ist.

In der Praxis gibt es in normalen Programmen überall "Minenfeld" -Code. Beispielsweise verspricht jede Division durch eine Ganzzahl dem Compiler, dass sie nicht Null ist. Jeder Zeiger-Deref verspricht dem Compiler, dass er nicht NULL ist.


3

Was bedeutet "legal" hier? Alles, was nicht dem C-Standard oder dem C ++ - Standard widerspricht, ist gemäß diesen Standards legal. Wenn Sie eine Anweisung ausführen i = i++;und infolgedessen Dinosaurier die Welt übernehmen, widerspricht dies nicht den Standards. Es widerspricht jedoch den Gesetzen der Physik, also wird es nicht passieren :-)

Wenn undefiniertes Verhalten Ihren Compiler zum Absturz bringt, verstößt dies nicht gegen den C- oder C ++ - Standard. Dies bedeutet jedoch, dass die Qualität des Compilers verbessert werden könnte (und wahrscheinlich sollte).

In früheren Versionen des C-Standards gab es Anweisungen, die fehlerhaft waren oder nicht von undefiniertem Verhalten abhingen:

char* p = 1 / 0;

Das Zuweisen einer Konstanten 0 zu einem Zeichen * ist zulässig. Das Zulassen einer Nicht-Null-Konstante ist nicht. Da der Wert 1/0 ein undefiniertes Verhalten ist, ist es ein undefiniertes Verhalten, ob der Compiler diese Anweisung akzeptieren soll oder nicht. (Heutzutage entspricht 1/0 nicht mehr der Definition des "ganzzahligen konstanten Ausdrucks").


3
Um genau zu sein: Dinosaurier, die die Welt erobern, widersprechen keinen Gesetzen der Physik (z. B. Variation des Jurassic Park). Es ist nur sehr unwahrscheinlich. :)
freakish
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.