Betrachten Sie die folgende Aussage:
*((char*)NULL) = 0; //undefined behavior
Es ruft eindeutig undefiniertes Verhalten hervor. Bedeutet das Vorhandensein einer solchen Anweisung in einem bestimmten Programm, dass das gesamte Programm undefiniert ist oder dass das Verhalten erst dann undefiniert wird, wenn der Kontrollfluss diese Anweisung trifft?
Wäre das folgende Programm genau definiert, falls der Benutzer die Nummer nie eingibt 3
?
while (true) {
int num = ReadNumberFromConsole();
if (num == 3)
*((char*)NULL) = 0; //undefined behavior
}
Oder ist es völlig undefiniertes Verhalten, egal was der Benutzer eingibt?
Kann der Compiler auch davon ausgehen, dass undefiniertes Verhalten zur Laufzeit niemals ausgeführt wird? Das würde es ermöglichen, rechtzeitig rückwärts zu argumentieren:
int num = ReadNumberFromConsole();
if (num == 3) {
PrintToConsole(num);
*((char*)NULL) = 0; //undefined behavior
}
Hier könnte der Compiler argumentieren, dass für den Fall, dass num == 3
wir immer undefiniertes Verhalten aufrufen. Daher muss dieser Fall unmöglich sein und die Nummer muss nicht gedruckt werden. Die gesamte if
Aussage könnte optimiert werden. Ist diese Art des Rückwärtsdenkens nach dem Standard zulässig?
const int i = 0; if (i) 5/i;
.PrintToConsole
nicht aufruft,std::exit
daher muss er den Aufruf ausführen.Antworten:
Weder. Die erste Bedingung ist zu stark und die zweite zu schwach.
Der Objektzugriff wird manchmal sequenziert, aber der Standard beschreibt das Verhalten des Programms außerhalb der Zeit. Danvil zitierte bereits:
Dies kann interpretiert werden:
Eine nicht erreichbare Anweisung mit UB gibt dem Programm also nicht UB. Eine erreichbare Aussage, die (aufgrund der Werte von Eingaben) niemals erreicht wird, gibt dem Programm keine UB. Deshalb ist Ihre erste Bedingung zu stark.
Jetzt kann der Compiler im Allgemeinen nicht sagen, was UB hat. Damit der Optimierer Anweisungen mit potenziellem UB neu anordnen kann, die bei Definition ihres Verhaltens nachbestellbar wären, muss UB vor dem vorhergehenden Sequenzpunkt (oder in C) in die Zeit zurückgreifen und einen Fehler machen ++ 11 Terminologie, damit die UB Dinge beeinflusst, die vor der UB-Sache sequenziert werden). Daher ist Ihre zweite Bedingung zu schwach.
Ein wichtiges Beispiel hierfür ist, wenn der Optimierer auf striktem Aliasing beruht. Der Sinn der strengen Aliasing-Regeln besteht darin, dem Compiler zu ermöglichen, Vorgänge neu zu ordnen, die nicht gültig neu angeordnet werden könnten, wenn es möglich wäre, dass die fraglichen Zeiger denselben Speicher aliasen. Wenn Sie also illegal Aliasing-Zeiger verwenden und UB auftritt, kann dies leicht eine Anweisung "vor" der UB-Anweisung beeinflussen. Für die abstrakte Maschine wurde die UB-Anweisung noch nicht ausgeführt. Der eigentliche Objektcode wurde teilweise oder vollständig ausgeführt. Der Standard versucht jedoch nicht, detailliert darzulegen, was es für den Optimierer bedeutet, Anweisungen neu zu ordnen oder welche Auswirkungen dies auf UB hat. Es gibt nur die Implementierungslizenz, um schief zu gehen, sobald es gefällt.
Sie können sich das als "UB hat eine Zeitmaschine" vorstellen.
Speziell um Ihre Beispiele zu beantworten:
PrintToConsole(3)
sei denn, es ist bekannt, dass es sicher zurückkehren wird. Es könnte eine Ausnahme auslösen oder was auch immer.Ein ähnliches Beispiel wie bei Ihrem zweiten ist die Option gcc
-fdelete-null-pointer-checks
, die Code wie diesen annehmen kann (ich habe dieses spezielle Beispiel nicht überprüft, betrachte es als Beispiel für die allgemeine Idee):und ändern Sie es zu:
Warum? Wenn if
p
null ist, hat der Code ohnehin UB, sodass der Compiler davon ausgehen kann, dass er nicht null ist, und entsprechend optimieren kann. Der Linux - Kernel über diese angesprochen ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) im Wesentlichen , weil es in einem Modus arbeitet , wo dereferencing ein Null - Zeiger nicht sollte Bei UB wird erwartet, dass dies zu einer definierten Hardware-Ausnahme führt, die der Kernel verarbeiten kann. Wenn die Optimierung aktiviert ist, muss gcc verwendet-fno-delete-null-pointer-checks
werden, um diese über den Standard hinausgehende Garantie zu gewährleisten.PS Die praktische Antwort auf die Frage "Wann schlägt undefiniertes Verhalten zu?" ist "10 Minuten bevor Sie für den Tag abreisen wollten".
quelle
void can_add(int x) { if (x + 100 < x) complain(); }
kann es vollständigx+100
wegoptimiert werden , denn wenn es nicht zu einem Überlauf kommt, passiert nichts, und wennx+100
es zu einem Überlauf kommt, ist das UB gemäß dem Standard, sodass möglicherweise nichts passiert.3
wenn es wollte, und für den Tag nach Hause packen, sobald sie eines sah eingehend.Der Standard besagt 1,9 / 4
Der interessante Punkt ist wahrscheinlich, was "enthalten" bedeutet. Wenig später bei 1,9 / 5 heißt es:
Hier wird ausdrücklich "Ausführung ... mit dieser Eingabe" erwähnt. Ich würde das so interpretieren, dass undefiniertes Verhalten in einem möglichen Zweig, der gerade nicht ausgeführt wird, keinen Einfluss auf den aktuellen Ausführungszweig hat.
Ein anderes Problem sind jedoch Annahmen, die auf undefiniertem Verhalten während der Codegenerierung basieren. Weitere Informationen hierzu finden Sie in der Antwort von Steve Jessop.
quelle
Ein lehrreiches Beispiel ist
Sowohl der aktuelle GCC als auch der aktuelle Clang optimieren dies (auf x86) auf
weil sie daraus ableiten, dass
x
der UB im Steuerpfad immer Null istif (x)
. GCC gibt Ihnen nicht einmal eine Warnung zur Verwendung eines nicht initialisierten Werts! (weil der Durchlauf, der die obige Logik anwendet, vor dem Durchlauf ausgeführt wird, der Warnungen mit nicht initialisierten Werten generiert)quelle
a
selbst wenn unter allen Umständen ein uninitializeda
würde an die Funktion übergeben, mit der die Funktion niemals etwas anfangen würde)?Der aktuelle C ++ - Arbeitsentwurf besagt dies in 1.9.4
Auf dieser Grundlage würde ich sagen, dass ein Programm, das undefiniertes Verhalten auf einem beliebigen Ausführungspfad enthält, zu jedem Zeitpunkt seiner Ausführung alles tun kann.
Es gibt zwei wirklich gute Artikel über undefiniertes Verhalten und was Compiler normalerweise tun:
quelle
int f(int x) { if (x > 0) return 100/x; else return 100; }
ruft sicherlich niemals undefiniertes Verhalten auf, obwohl sie100/0
natürlich undefiniert ist.printf("Hello, World"); *((char*)NULL) = 0
wird nicht garantiert, dass etwas gedruckt wird. Dies unterstützt die Optimierung, da der Compiler Operationen, von denen er weiß, dass sie irgendwann auftreten werden, frei neu anordnen kann (natürlich abhängig von Abhängigkeitsbeschränkungen), ohne undefiniertes Verhalten berücksichtigen zu müssen.int x,y; std::cin >> x >> y; std::cout << (x+y);
dass "1 + 1 = 17" gesagt werden darf, nur weil es einige Eingaben gibt, bei denenx+y
Überläufe auftreten (was UB ist, daint
es sich um einen vorzeichenbehafteten Typ handelt).Das Wort "Verhalten" bedeutet, dass etwas getan wird . Ein Status, der niemals ausgeführt wird, ist kein "Verhalten".
Eine Illustration:
Ist das undefiniertes Verhalten? Angenommen, wir sind uns
ptr == nullptr
mindestens einmal während der Programmausführung 100% sicher . Die Antwort sollte ja sein.Was ist damit?
Ist das undefiniert? (Erinnerst
ptr == nullptr
du dich mindestens einmal?) Ich hoffe nicht, sonst kannst du überhaupt kein nützliches Programm schreiben.Bei der Beantwortung dieser Antwort wurde kein Srandardese verletzt.
quelle
Das undefinierte Verhalten tritt auf, wenn das Programm undefiniertes Verhalten verursacht, unabhängig davon, was als nächstes passiert. Sie haben jedoch das folgende Beispiel angegeben.
Wenn der Compiler die Definition von nicht kennt
PrintToConsole
, kann er dieif (num == 3)
Bedingung nicht entfernen . Nehmen wir an, Sie haben einenLongAndCamelCaseStdio.h
Systemheader mit der folgenden Deklaration vonPrintToConsole
.Nichts zu hilfreich, alles klar. Lassen Sie uns nun sehen, wie böse (oder vielleicht nicht so böse, undefiniertes Verhalten hätte schlimmer sein können) der Anbieter ist, indem wir die tatsächliche Definition dieser Funktion überprüfen.
Der Compiler muss tatsächlich davon ausgehen, dass eine beliebige Funktion, von der der Compiler nicht weiß, was er tut, eine Ausnahme beenden oder auslösen kann (im Fall von C ++). Sie können feststellen, dass dies
*((char*)NULL) = 0;
nicht ausgeführt wird, da die Ausführung nach demPrintToConsole
Aufruf nicht fortgesetzt wird .Das undefinierte Verhalten tritt auf, wenn es
PrintToConsole
tatsächlich zurückkehrt. Der Compiler erwartet, dass dies nicht geschieht (da dies dazu führen würde, dass das Programm undefiniertes Verhalten ausführt, egal was passiert), daher kann alles passieren.Betrachten wir jedoch etwas anderes. Angenommen, wir führen eine Nullprüfung durch und verwenden die Variable nach der Nullprüfung.
In diesem Fall ist leicht zu erkennen, dass
lol_null_check
ein Nicht-NULL-Zeiger erforderlich ist. Das Zuweisen zur globalen nichtflüchtigenwarning
Variablen kann das Programm nicht beenden oder Ausnahmen auslösen. Daspointer
ist auch nicht flüchtig, so dass es seinen Wert in der Mitte der Funktion nicht magisch ändern kann (wenn ja, ist es undefiniertes Verhalten). Das Aufrufenlol_null_check(NULL)
führt zu einem undefinierten Verhalten, das dazu führen kann, dass die Variable nicht zugewiesen wird (da zu diesem Zeitpunkt bekannt ist, dass das Programm das undefinierte Verhalten ausführt).Das undefinierte Verhalten bedeutet jedoch, dass das Programm alles tun kann. Daher hindert nichts das undefinierte Verhalten daran, in die Zeit zurückzukehren und Ihr Programm vor der ersten
int main()
Ausführungszeile zum Absturz zu bringen . Es ist undefiniertes Verhalten, es muss keinen Sinn ergeben. Es kann auch nach der Eingabe von 3 abstürzen, aber das undefinierte Verhalten wird in der Zeit zurückgehen und abstürzen, bevor Sie überhaupt 3 eingeben. Und wer weiß, vielleicht überschreibt undefiniertes Verhalten Ihren System-RAM und führt dazu, dass Ihr System 2 Wochen später abstürzt. während Ihr undefiniertes Programm nicht läuft.quelle
PrintToConsole
ist mein Versuch, einen programmexternalen Nebeneffekt einzufügen, der auch nach Abstürzen sichtbar und stark sequenziert ist. Ich wollte eine Situation schaffen, in der wir mit Sicherheit feststellen können, ob diese Aussage optimiert wurde. Aber Sie haben Recht damit, dass es niemals zurückkehren könnte.; Ihr Beispiel für das Schreiben in ein globales Format kann anderen Optimierungen unterliegen, die nicht mit UB zusammenhängen. Beispielsweise kann eine nicht verwendete globale Datei gelöscht werden. Haben Sie eine Idee, einen externen Nebeneffekt so zu erzeugen, dass die Kontrolle garantiert wieder hergestellt wird?volatile
Variable liest, eine E / A-Operation auslösen, die wiederum den aktuellen Thread sofort unterbrechen könnte. Der Interrupt-Handler könnte dann den Thread beenden, bevor er die Möglichkeit hat, etwas anderes auszuführen. Ich sehe keine Rechtfertigung dafür, dass der Compiler vor diesem Punkt undefiniertes Verhalten pushen könnte.Wenn das Programm eine Anweisung erreicht, die undefiniertes Verhalten aufruft, werden keinerlei Anforderungen an die Ausgabe / das Verhalten des Programms gestellt. Es spielt keine Rolle, ob sie "vor" oder "nach" stattfinden, wenn undefiniertes Verhalten aufgerufen wird.
Ihre Argumentation zu allen drei Codefragmenten ist richtig. Insbesondere kann ein Compiler jede Anweisung, die bedingungslos undefiniertes Verhalten aufruft, so behandeln,
__builtin_unreachable()
wie GCC es behandelt : als Optimierungshinweis, dass die Anweisung nicht erreichbar ist (und damit alle Codepfade, die bedingungslos zu ihr führen, auch nicht erreichbar sind). Andere ähnliche Optimierungen sind natürlich möglich.quelle
__builtin_unreachable()
es aus Neugier zu Effekten, die zeitlich sowohl vorwärts als auch rückwärts gingen? Angesichts so etwas wieextern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }
ich das sehen konnte ,builtin_unreachable()
als gutes die Compiler wissen lassen es die weglassenreturn
Anweisung, aber das wäre etwas anders zu sagen , dass die vorhergehende Code weggelassen werden könnte.__builtin_unreachable
erreicht wird. Dieses Programm ist definiert.restrict
Zeiger weder zugegriffen wurde noch wird, mit einem geschrieben werdenunsigned char*
.Viele Standards für viele Arten von Dingen erfordern viel Aufwand bei der Beschreibung von Dingen, die Implementierungen SOLLTEN oder NICHT tun SOLLTEN, wobei eine Nomenklatur verwendet wird, die der in IETF RFC 2119 definierten ähnlich ist (obwohl nicht unbedingt die Definitionen in diesem Dokument zitiert werden). In vielen Fällen sind Beschreibungen von Dingen, die Implementierungen ausführen sollten, außer in Fällen, in denen sie nutzlos oder unpraktisch wären, wichtiger als die Anforderungen, denen alle konformen Implementierungen entsprechen müssen.
Leider neigen C- und C ++ - Standards dazu, Beschreibungen von Dingen zu vermeiden, die zwar nicht zu 100% erforderlich sind, aber dennoch von Qualitätsimplementierungen erwartet werden sollten, die kein gegenteiliges Verhalten dokumentieren. Ein Vorschlag, dass Implementierungen etwas tun sollten, könnte bedeuten, dass diejenigen, die nicht minderwertig sind, und in Fällen, in denen es im Allgemeinen offensichtlich ist, welche Verhaltensweisen bei einer bestimmten Implementierung nützlich oder praktisch oder unpraktisch und nutzlos sind, vorhanden sind wenig wahrgenommene Notwendigkeit für den Standard, solche Urteile zu stören.
Ein cleverer Compiler könnte dem Standard entsprechen und gleichzeitig jeden Code eliminieren, der keine Auswirkungen hätte, außer wenn Code Eingaben empfängt, die unweigerlich undefiniertes Verhalten verursachen würden, aber "clever" und "dumm" sind keine Antonyme. Die Tatsache, dass die Autoren des Standards entschieden haben, dass es einige Arten von Implementierungen geben könnte, bei denen ein nützliches Verhalten in einer bestimmten Situation nutzlos und unpraktisch wäre, impliziert keine Beurteilung, ob solche Verhaltensweisen für andere als praktisch und nützlich angesehen werden sollten. Wenn eine Implementierung eine Verhaltensgarantie ohne Kosten aufrechterhalten könnte, die über den Verlust einer Beschneidungsmöglichkeit für "tote Zweige" hinausgeht, würde fast jeder Wert, den Benutzercode aus dieser Garantie erhalten könnte, die Kosten für deren Bereitstellung übersteigen. Die Beseitigung von toten Ästen kann in Fällen in Ordnung sein, in denen dies nicht der Fall ist.Wenn der Benutzercode jedoch in einer bestimmten Situation fast jedes andere mögliche Verhalten als die Beseitigung von Totzweigen hätte handhaben können , müsste der Benutzercode zur Vermeidung von UB wahrscheinlich den von DBE erzielten Wert überschreiten.
quelle
x*y < z
wennx*y
kein Überlauf auftritt, und wenn ein Überlauf auf willkürliche Weise 0 oder 1 ergibt, jedoch ohne Nebenwirkungen, gibt es auf den meisten Plattformen keinen Grund, warum das Erfüllen der zweiten und dritten Anforderung teurer sein sollte als Das Erfüllen des ersten, aber jede Art, den Ausdruck zu schreiben, um in allen Fällen ein standarddefiniertes Verhalten zu gewährleisten, würde in einigen Fällen erhebliche Kosten verursachen. Das Schreiben des Ausdrucks(int64_t)x*y < z
könnte die Berechnungskosten mehr als vervierfachen ...(int)((unsigned)x*y) < z
würde ein Compiler verhindern, dass ansonsten nützliche algebraische Substitutionen verwendet werden (z. B. wenn er das weißx
undz
gleich und positiv ist, könnte er den ursprünglichen Ausdruck vereinfacheny<0
, aber die Version ohne Vorzeichen würde den Compiler zwingen, die Multiplikation durchzuführen). Wenn der Compiler garantieren kann, obwohl der Standard dies nicht vorschreibt, wird er die Anforderung "Ausbeute 0 oder 1 ohne Nebenwirkungen" einhalten. Benutzercode könnte dem Compiler Optimierungsmöglichkeiten bieten, die er sonst nicht erhalten könnte.x*y
, der im Falle eines Überlaufs einen normalen Wert ausgibt, aber überhaupt einen Wert. Konfigurierbares UB in C / C ++ scheint mir wichtig zu sein.