Ich weiß, dass ein "undefiniertes Verhalten" in C ++ dem Compiler so ziemlich alles ermöglichen kann, was er will. Ich hatte jedoch einen Absturz, der mich überraschte, als ich davon ausging, dass der Code sicher genug war.
In diesem Fall trat das eigentliche Problem nur auf einer bestimmten Plattform mit einem bestimmten Compiler auf und nur, wenn die Optimierung aktiviert war.
Ich habe verschiedene Dinge versucht, um das Problem zu reproduzieren und maximal zu vereinfachen. Hier ist ein Auszug einer aufgerufenen Funktion Serialize
, die einen bool-Parameter verwendet und die Zeichenfolge true
oder false
in einen vorhandenen Zielpuffer kopiert .
Wäre diese Funktion in einer Codeüberprüfung enthalten, könnte nicht festgestellt werden, dass sie tatsächlich abstürzen könnte, wenn der Parameter bool ein nicht initialisierter Wert wäre?
// Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
}
Wenn dieser Code mit Clang 5.0.0 + -Optimierungen ausgeführt wird, kann / kann er abstürzen.
Der erwartete ternäre Operator boolValue ? "true" : "false"
sah für mich sicher genug aus. Ich nahm an: "Was auch immer der Müllwert ist, boolValue
spielt keine Rolle, da er sowieso als wahr oder falsch bewertet wird."
Ich habe ein Compiler Explorer-Beispiel eingerichtet , das das Problem bei der Demontage zeigt, hier das vollständige Beispiel. Hinweis: Um das Problem zu beheben, habe ich festgestellt, dass die Kombination Clang 5.0.0 mit -O2-Optimierung funktioniert.
#include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output "true" or "false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
}
Das Problem ergibt sich aus dem Optimierer: Es war klug genug zu folgern, dass sich die Zeichenfolgen "true" und "false" nur in der Länge um 1 unterscheiden. Anstatt die Länge wirklich zu berechnen, wird der Wert des Bools selbst verwendet, der sollte technisch entweder 0 oder 1 sein und geht so:
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
Während dies sozusagen "clever" ist, lautet meine Frage: Erlaubt der C ++ - Standard einem Compiler anzunehmen, dass ein Bool nur eine interne numerische Darstellung von '0' oder '1' haben kann, und verwendet sie so?
Oder handelt es sich um einen implementierungsdefinierten Fall. In diesem Fall ging die Implementierung davon aus, dass alle ihre Bools immer nur 0 oder 1 enthalten und jeder andere Wert ein undefiniertes Verhaltensgebiet ist.
true
", eine Regel für boolesche Operationen ist, einschließlich "Zuweisung zu einem Bool" (diestatic_cast<bool>()
abhängig von den Besonderheiten implizit eine aufrufen kann ). Es ist jedoch keine Anforderung an die interne Darstellung einesbool
vom Compiler ausgewählten.Antworten:
Ja, ISO C ++ ermöglicht (erfordert aber keine) Implementierungen, um diese Auswahl zu treffen.
Beachten Sie jedoch auch, dass ISO C ++ es einem Compiler ermöglicht, absichtlich abstürzenden Code (z. B. mit einer unzulässigen Anweisung) auszugeben, wenn das Programm auf UB stößt, z. B. um Ihnen bei der Suche nach Fehlern zu helfen. (Oder weil es sich um eine DeathStation 9000 handelt. Eine strikte Konformität reicht nicht aus, damit eine C ++ - Implementierung für einen echten Zweck nützlich ist.) ISO C ++ würde es einem Compiler also ermöglichen, einen Asm zu erstellen, der (aus völlig anderen Gründen) abgestürzt ist, selbst bei ähnlichem Code, der einen nicht initialisierten Code liest
uint32_t
. Auch wenn dies ein Typ mit festem Layout ohne Trap-Darstellungen sein muss.Es ist eine interessante Frage, wie echte Implementierungen funktionieren, aber denken Sie daran, dass Ihr Code auch dann unsicher wäre, wenn die Antwort anders wäre, da modernes C ++ keine portable Version der Assemblersprache ist.
Sie kompilieren für das x86-64 System V ABI , das angibt, dass ein
bool
als Funktion arg in einem Register durch die Bitmusterfalse=0
undtrue=1
in den niedrigen 8 Bits des Registers 1 dargestellt wird . Im Speicherbool
befindet sich ein 1-Byte-Typ, der wiederum einen ganzzahligen Wert von 0 oder 1 haben muss.(Ein ABI ist eine Reihe von Implementierungsoptionen, auf die sich Compiler für dieselbe Plattform einigen, damit sie Code erstellen können, der die Funktionen des anderen aufruft, einschließlich Typgrößen, Strukturlayoutregeln und Aufrufkonventionen.)
ISO C ++ spezifiziert es nicht, aber diese ABI-Entscheidung ist weit verbreitet, weil sie die Bool-> Int-Konvertierung billig macht (nur Null-Erweiterung) . Mir sind keine ABIs bekannt, bei denen der Compiler
bool
für keine Architektur (nicht nur x86) 0 oder 1 annehmen darf. Es ermöglicht Optimierungen wie!mybool
mitxor eax,1
dem Low - Bit Flip: jeden möglichen Code, der ein Bit / integer / bool zwischen 0 und 1 in einzelnem CPU - Befehl Flip kann . Odera&&b
zu einem bitweisen UND fürbool
Typen kompilieren . Einige Compiler nutzen tatsächlich Boolesche Werte als 8-Bit in Compilern. Sind Operationen an ihnen ineffizient? .Im Allgemeinen ermöglicht die Als-ob-Regel dem Compiler, die Vorteile der auf der zu kompilierenden Zielplattform zutreffenden Dinge zu nutzen , da das Endergebnis ausführbarer Code ist, der dasselbe extern sichtbare Verhalten wie die C ++ - Quelle implementiert. (Mit all den Einschränkungen, die Undefined Behavior dem auferlegt, was tatsächlich "extern sichtbar" ist: nicht mit einem Debugger, sondern von einem anderen Thread in einem wohlgeformten / legalen C ++ - Programm.)
Der Compiler ist auf jeden Fall in seinem Code-gen, um den vollen Nutzen aus einem ABI - Garantie erlaubt und Code machen , wie Sie gefunden , die optimiert
strlen(whichString)
auf5U - boolValue
. (Übrigens ist diese Optimierung etwas clever, aber vielleicht kurzsichtig im Vergleich zu Verzweigung und Inliningmemcpy
als Speicher für unmittelbare Daten 2. )Oder der Compiler hätte eine Tabelle mit Zeigern erstellen und sie mit dem ganzzahligen Wert von indizieren können
bool
, wiederum unter der Annahme, dass es sich um eine 0 oder 1 handelt. ( Diese Möglichkeit ist die Antwort von @ Barmar .)Ihr
__attribute((noinline))
Konstruktor mit aktivierter Optimierung führte dazu, dass nur ein Byte aus dem Stapel geladen wurde, um es als zu verwendenuninitializedBool
. Es hat Platz für das Objekt inmain
with geschaffenpush rax
(was kleiner und aus verschiedenen Gründen ungefähr so effizient ist wiesub rsp, 8
), sodassmain
der Wert, für den es verwendet wurde, der Müll ist, der sich bei der Eingabe in AL befanduninitializedBool
. Deshalb haben Sie tatsächlich Werte erhalten, die nicht nur waren0
.5U - random garbage
kann leicht auf einen großen vorzeichenlosen Wert umbrochen werden, was dazu führt, dass memcpy in den nicht zugeordneten Speicher gelangt. Das Ziel befindet sich im statischen Speicher, nicht im Stapel, sodass Sie keine Absenderadresse oder ähnliches überschreiben.Andere Implementierungen könnten andere Entscheidungen treffen, z . B.
false=0
undtrue=any non-zero value
. Dann würde clang wahrscheinlich keinen Code erstellen, der für diese bestimmte Instanz von UB abstürzt . (Aber es wäre immer noch erlaubt, wenn es wollte.) Ich kenne keine Implementierungen, die etwas anderes auswählen, wofür x86-64 funktioniertbool
, aber der C ++ - Standard erlaubt viele Dinge, die niemand tut oder sogar tun möchte Hardware, die mit aktuellen CPUs vergleichbar ist.ISO C ++ lässt nicht spezifiziert, was Sie finden, wenn Sie die Objektdarstellung von a untersuchen oder ändern
bool
. (z. B. indemmemcpy
Sie dasbool
In eingebenunsigned char
, was Sie tun dürfen, weilchar*
es alles aliasieren kann. Und esunsigned char
wird garantiert, dass keine Auffüllbits vorhanden sind, sodass Sie mit dem C ++ - Standard formal Objektdarstellungen ohne UB hexdumpen können. Zeiger-Casting zum Kopieren des Objekts Die Darstellung unterscheidet sichchar foo = my_bool
natürlich von der Zuweisung , sodass eine Boolesche Darstellung auf 0 oder 1 nicht stattfinden würde und Sie die Rohobjektdarstellung erhalten würden.)Sie haben teilweise „versteckt“ die UB auf diesem Ausführungspfad des Compilers mit
noinline
. Auch wenn dies nicht inline ist, können Interprocedural-Optimierungen dennoch eine Version der Funktion erstellen, die von der Definition einer anderen Funktion abhängt. (Erstens erstellt clang eine ausführbare Datei, keine gemeinsam genutzte Unix-Bibliothek, in der Symbolinterpositionen auftreten können. Zweitens muss die Definition in derclass{}
Definition enthalten sein, sodass alle Übersetzungseinheiten dieselbe Definition haben müssen. Wie beiminline
Schlüsselwort.)Ein Compiler könnte also nur eine
ret
oderud2
(unzulässige Anweisung) als Definition für ausgebenmain
, da der Ausführungspfad, der am Anfang von beginnt,main
unvermeidlich auf undefiniertes Verhalten stößt. (Was der Compiler zur Kompilierungszeit sehen kann, wenn er sich entschlossen hat, dem Pfad durch den Nicht-Inline-Konstruktor zu folgen.)Jedes Programm, das auf UB trifft, ist für seine gesamte Existenz völlig undefiniert. Aber UB innerhalb einer Funktion oder eines
if()
Zweigs, der niemals ausgeführt wird, beschädigt den Rest des Programms nicht. In der Praxis bedeutet dies, dass Compiler entscheiden können, ob eine illegale Anweisung oder eineret
oder nichts ausgegeben werden soll, und in den nächsten Block / die nächste Funktion fallen können, damit der gesamte Basisblock, der zum Zeitpunkt der Kompilierung nachgewiesen werden kann, UB enthält oder zu UB führt.GCC und Clang in der Praxis tun manchmal tatsächlich emittieren
ud2
auf UB, statt auch nur zu versuchen Code für Wege der Ausführung zu erzeugen , die keinen Sinn machen. Oder für Fälle wie das Abfallen vom Ende einer Nichtfunktionvoid
lässt gcc manchmal eineret
Anweisung weg . Wenn Sie dachten, dass "meine Funktion nur mit dem Müll in RAX zurückkehrt", irren Sie sich zutiefst. Moderne C ++ - Compiler behandeln die Sprache nicht mehr wie eine tragbare Assemblersprache. Ihr Programm muss wirklich C ++ sein, ohne Annahmen darüber zu treffen, wie eine eigenständige nicht inline-Version Ihrer Funktion in asm aussehen könnte.Ein weiteres unterhaltsames Beispiel ist, warum ein nicht ausgerichteter Zugriff auf mmap'ed-Speicher auf AMD64 manchmal fehlerhaft ist. . x86 ist nicht an nicht ausgerichteten ganzen Zahlen schuld, oder? Warum sollte eine Fehlausrichtung
uint16_t*
ein Problem sein? Dennalignof(uint16_t) == 2
und die Verletzung dieser Annahme führte zu einem Segfault bei der automatischen Vektorisierung mit SSE2.Siehe auch Was jeder C-Programmierer über undefiniertes Verhalten # 1/3 wissen sollte , ein Artikel eines Clang-Entwicklers.
Schlüsselpunkt: Wenn die Compiler die UB bei der Kompilierung bemerkt hat , es könnte „break“ (emittieren überraschend asm) den Weg durch den Code , dass Ursachen UB selbst wenn ein ABI - Targeting , wo ein Bit-Muster für eine gültige Objektdarstellung ist
bool
.Erwarten Sie völlige Feindseligkeit gegenüber vielen Fehlern des Programmierers, insbesondere vor Dingen, vor denen moderne Compiler warnen. Aus diesem Grund sollten Sie
-Wall
Warnungen verwenden und beheben. C ++ ist keine benutzerfreundliche Sprache, und etwas in C ++ kann unsicher sein, selbst wenn es in asm auf dem Ziel, für das Sie kompilieren, sicher wäre. (z. B. ist der signierte Überlauf in C ++ UB, und Compiler gehen davon aus, dass dies nicht der Fall ist, selbst wenn sie für das 2er-Komplement x86 kompilieren, es sei denn, Sie verwendenclang/gcc -fwrapv
.)UB, das zur Kompilierungszeit sichtbar ist, ist immer gefährlich, und es ist wirklich schwer (mit der Optimierung der Verbindungszeit) sicher zu sein, dass Sie UB wirklich vor dem Compiler versteckt haben und daher überlegen können, welche Art von Asm es generieren wird.
Nicht zu dramatisch sein; Oft lassen Compiler Sie mit einigen Dingen davonkommen und Code ausgeben, wie Sie es erwarten, selbst wenn etwas UB ist. Aber vielleicht wird es in Zukunft ein Problem sein, wenn Compiler-Entwickler eine Optimierung implementieren, die mehr Informationen über Wertebereiche erhält (z. B. dass eine Variable nicht negativ ist, was es ihr möglicherweise ermöglicht, die Vorzeichenerweiterung auf freie 86-Erweiterung auf x86- zu optimieren. 64). Zum Beispiel wird in gcc und clang das Tun
tmp = a+INT_MIN
nichta<0
als immer falsch optimiert , nur dastmp
ist immer negativ. (WeilINT_MIN
+a=INT_MAX
für das Komplementziel dieser 2 negativ ist unda
nicht höher sein kann.)Daher wird gcc / clang derzeit nicht zurückverfolgt, um Bereichsinformationen für die Eingaben einer Berechnung abzuleiten, sondern nur anhand der Ergebnisse, die auf der Annahme eines nicht signierten Überlaufs basieren: Beispiel für Godbolt . Ich weiß nicht, ob dies eine Optimierung ist, die absichtlich im Namen der Benutzerfreundlichkeit "verpasst" wird oder was.
Beachten Sie auch, dass Implementierungen (auch als Compiler bezeichnet) das Verhalten definieren dürfen, das ISO C ++ undefiniert lässt . Beispielsweise müssen alle Compiler, die Intels Intrinsics unterstützen (wie
_mm_add_ps(__m128, __m128)
bei der manuellen SIMD-Vektorisierung), die Bildung falsch ausgerichteter Zeiger ermöglichen, was in C ++ UB ist, auch wenn Sie sie nicht dereferenzieren.__m128i _mm_loadu_si128(const __m128i *)
führt nicht ausgerichtete Lasten aus, indem ein falsch ausgerichtetes__m128i*
Argument verwendet wird, nicht einvoid*
oderchar*
. Ist `reinterpret_cast`ing zwischen Hardwarevektorzeiger und dem entsprechenden Typ ein undefiniertes Verhalten?GNU C / C ++ definiert auch das Verhalten der Linksverschiebung einer negativ vorzeichenbehafteten Zahl (auch ohne
-fwrapv
), getrennt von den normalen UB-Regeln für vorzeichenbehaftete Überläufe. ( Dies ist UB in ISO C ++ , während Rechtsverschiebungen von vorzeichenbehafteten Zahlen implementierungsdefiniert sind (logisch vs. arithmetisch). Implementierungen von guter Qualität wählen Arithmetik für HW mit arithmetischen Rechtsverschiebungen, ISO C ++ gibt dies jedoch nicht an.) Dies wird im Abschnitt Integer des GCC-Handbuchs dokumentiert , zusammen mit der Definition des implementierungsdefinierten Verhaltens, für dessen Implementierung C-Standards Implementierungen auf die eine oder andere Weise erfordern.Es gibt definitiv Probleme mit der Implementierungsqualität, die Compiler-Entwickler interessieren. Im Allgemeinen versuchen sie nicht , absichtlich feindliche Compiler zu erstellen, aber die Nutzung aller UB-Schlaglöcher in C ++ (mit Ausnahme derjenigen, die sie definieren) zur besseren Optimierung kann manchmal kaum zu unterscheiden sein.
Fußnote 1 : Die oberen 56 Bits können Müll sein, den der Angerufene ignorieren muss, wie es für Typen üblich ist, die schmaler als ein Register sind.
( Andere ABIs tun hier verschiedene Entscheidungen treffen . Einige schmale Integer - Typen benötigen werden null- oder Vorzeichen erweiterten ein Register zu füllen , wenn übergeben oder von Funktionen zurückgegeben, wie MIPS64 und PowerPC64. Siehe den letzten Abschnitt dieser x86-64 Antwort im Vergleich zu diesen früheren ISAs .)
Beispielsweise hat ein Anrufer möglicherweise
a & 0x01010101
in RDI berechnet und es vor dem Anruf für etwas anderes verwendetbool_func(a&1)
. Der Anrufer könnte das optimieren,&1
da er dies bereits für das niedrige Byte als Teil vonand edi, 0x01010101
getan hat, und er weiß, dass der Angerufene erforderlich ist, um die hohen Bytes zu ignorieren.Oder wenn ein Bool als drittes Argument übergeben wird, lädt ihn ein für die Codegröße optimierter Aufrufer möglicherweise
mov dl, [mem]
stattdessen mitmovzx edx, [mem]
und spart 1 Byte auf Kosten einer falschen Abhängigkeit vom alten RDX-Wert (oder eines anderen Teilregister-Effekts, je nachdem auf CPU-Modell). Oder für das erste Argumentmov dil, byte [r10]
anstelle vonmovzx edi, byte [r10]
, weil beide ohnehin ein REX-Präfix benötigen.Aus diesem Grunde Klirren aussendet
movzx eax, dil
inSerialize
, stattsub eax, edi
. (Bei Ganzzahlargumenten verstößt Clang gegen diese ABI-Regel, stattdessen abhängig vom undokumentierten Verhalten von gcc und Clang auf Null- oder Vorzeichenverlängerungs-Ganzzahlen auf 32 Bit. Ist ein Vorzeichen oder eine Null-Erweiterung erforderlich, wenn einem Zeiger für ein 32-Bit- Offset hinzugefügt wird Das x86-64 ABI? Also war ich interessiert zu sehen, dass es nicht dasselbe machtbool
.)Fußnote 2: Nach der Verzweigung hätten Sie nur ein 4-Byte-
mov
Sofort oder einen 4-Byte + 1-Byte-Speicher. Die Länge ist implizit in den Speicherbreiten + Offsets enthalten.OTOH, glibc memcpy führt zwei 4-Byte-Ladevorgänge / -Speicher mit einer Überlappung aus, die von der Länge abhängt. Dadurch wird das Ganze wirklich frei von bedingten Verzweigungen auf dem Booleschen Wert. Siehe den
L(between_4_7):
Block in glibcs memcpy / memmove. Oder gehen Sie zumindest für einen der beiden Booleschen Werte in der Verzweigung von memcpy auf die gleiche Weise vor, um eine Blockgröße auszuwählen.Beim Inlining können Sie 2x
mov
-immediate +cmov
und einen bedingten Offset verwenden oder die Zeichenfolgendaten im Speicher belassen .Oder wenn Sie auf Intel Ice Lake ( mit der Funktion Fast Short REP MOV ) einstellen , ist eine tatsächliche
rep movsb
möglicherweise optimal. glibc wirdmemcpy
möglicherweiserep movsb
für kleine Größen auf CPUs mit dieser Funktion verwendet, wodurch viel Verzweigung eingespart wird.Tools zum Erkennen von UB und zur Verwendung nicht initialisierter Werte
In gcc und clang können Sie mit kompilieren
-fsanitize=undefined
, um Laufzeitinstrumente hinzuzufügen, die zur Laufzeit auf UB warnen oder Fehler verursachen. Damit werden jedoch keine einheitlichen Variablen erfasst. (Weil es die Schriftgröße nicht erhöht, um Platz für ein "nicht initialisiertes" Bit zu schaffen).Siehe https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Um die Verwendung nicht initialisierter Daten zu finden, gibt es Address Clitizer und Memory Sanitizer in clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer zeigt Beispiele für das
clang -fsanitize=memory -fPIE -pie
Erkennen nicht initialisierter Speicherlesevorgänge. Es funktioniert möglicherweise am besten, wenn Sie ohne Optimierung kompilieren , sodass alle Lesevorgänge von Variablen tatsächlich aus dem Speicher im ASM geladen werden. Sie zeigen, dass es-O2
in einem Fall verwendet wird, in dem sich die Last nicht optimieren würde. Ich habe es selbst nicht versucht. (In einigen Fällen, z. B. wenn ein Akkumulator vor dem Summieren eines Arrays nicht initialisiert wird, gibt clang -O3 Code aus, der in ein Vektorregister summiert, das nie initialisiert wurde. Bei der Optimierung kann es also vorkommen, dass dem UB kein Speicherlesevorgang zugeordnet ist . Aber-fsanitize=memory
ändert den generierten asm und kann zu einer Überprüfung führen.)Dies sollte in diesem Fall funktionieren, da der Aufruf von glibc
memcpy
mit einemlength
aus nicht initialisiertem Speicher berechneten Speicher (innerhalb der Bibliothek) zu einem Zweig führt, der auf basiertlength
. Wenn es eine vollständig verzweigungslose Version eingebunden hätte, die nurcmov
Indexierung und zwei Speicher verwendet, hätte es möglicherweise nicht funktioniert.Valgrind's
memcheck
wird auch nach dieser Art von Problem suchen und sich erneut nicht beschweren, wenn das Programm einfach nicht initialisierte Daten kopiert. Es wird jedoch angegeben, dass erkannt wird, wann ein "bedingter Sprung oder eine bedingte Bewegung von nicht initialisierten Werten abhängt", um zu versuchen, ein von außen sichtbares Verhalten zu erfassen, das von nicht initialisierten Daten abhängt.Vielleicht besteht die Idee dahinter, nicht nur eine Last zu kennzeichnen, darin, dass Strukturen aufgefüllt werden können, und das Kopieren der gesamten Struktur (einschließlich Auffüllen) mit einem breiten Vektor laden / speichern ist kein Fehler, selbst wenn die einzelnen Mitglieder jeweils nur einzeln geschrieben wurden. Auf der ASM-Ebene sind die Informationen darüber verloren gegangen, was aufgefüllt wurde und was tatsächlich Teil des Werts ist.
quelle
Der Compiler darf davon ausgehen , dass ein Boolescher Wert als Argument übergeben ist ein gültiger Booleschen Wert (dh eine , die initialisiert wurde oder umgewandelt
true
oderfalse
). Dertrue
Wert muss nicht mit der Ganzzahl 1 identisch sein - tatsächlich kann es verschiedene Darstellungen vontrue
und gebenfalse
-, aber der Parameter muss eine gültige Darstellung eines dieser beiden Werte sein, wobei "gültige Darstellung" die Implementierung ist. definiert.Wenn Sie also a nicht initialisieren
bool
können oder es Ihnen gelingt, es durch einen Zeiger eines anderen Typs zu überschreiben, sind die Annahmen des Compilers falsch und es kommt zu undefiniertem Verhalten. Sie wurden gewarnt:quelle
true
Wert muss nicht mit der Ganzzahl 1 identisch sein" ist irreführend. Sicher, das eigentliche Bitmuster könnte etwas anderes sein, aber wenn es implizit konvertiert / heraufgestuft wird (die einzige Möglichkeit, einen anderen Wert alstrue
/ zu sehenfalse
),true
ist immer1
undfalse
immer0
. Natürlich wäre ein solcher Compiler auch nicht in der Lage, den Trick zu verwenden, den dieser Compiler zu verwenden versuchte (unter Verwendung der Tatsache, dass dasbool
tatsächliche Bitmuster nur0
oder sein könnte1
), so dass es für das OP-Problem irgendwie irrelevant ist.true
auf das Bitmuster beschränkt1
, ist dies sein Vorrecht. Wenn ein anderer Satz von Darstellungen ausgewählt wird, kann die hier angegebene Optimierung tatsächlich nicht verwendet werden. Wenn es diese bestimmte Darstellung wählt, kann es. Es muss nur intern konsistent sein. Sie können die Darstellung von a untersuchen,bool
indem Sie sie in ein Byte-Array kopieren. das ist nicht UB (aber es ist implementierungsdefiniert)bool
einem Bitmuster von0
oder abhängt1
. Sie booleschen nichtbool
jedes Mal neu, wenn sie es aus dem Speicher lesen (oder ein Register, das eine Funktion arg enthält). Das sagt diese Antwort. Beispiele : gcc4.7 + optimieren können ,return a||b
umor eax, edi
in einer Funktion zurückkehrenbool
, oder MSVC optimierena&b
zutest cl, dl
. x86test
ist ein bitweisesand
, also setzt ifcl=1
unddl=2
test Flags entsprechendcl&dl = 0
.Die Funktion selbst ist korrekt, aber in Ihrem Testprogramm verursacht die Anweisung, die die Funktion aufruft, ein undefiniertes Verhalten, indem der Wert einer nicht initialisierten Variablen verwendet wird.
Der Fehler befindet sich in der aufrufenden Funktion und kann durch Codeüberprüfung oder statische Analyse der aufrufenden Funktion erkannt werden. Über Ihren Compiler-Explorer-Link erkennt der gcc 8.2-Compiler den Fehler. (Vielleicht könnten Sie einen Fehlerbericht gegen Clang einreichen, der das Problem nicht findet).
Undefiniertes Verhalten bedeutet, dass alles passieren kann, einschließlich des Absturzes des Programms einige Zeilen nach dem Ereignis, das das undefinierte Verhalten ausgelöst hat.
NB. Die Antwort auf "Kann undefiniertes Verhalten _____ verursachen?" ist immer "Ja". Das ist buchstäblich die Definition von undefiniertem Verhalten.
quelle
bool
Triggers UB ausgelöst?bool
). Das Kopieren erfordert die Auswertung der Quelletrue
,false
undnot-a-thing
Werte für booleans.Ein Bool darf nur die implementierungsabhängigen Werte enthalten, die intern für
true
und verwendet werdenfalse
, und der generierte Code kann davon ausgehen, dass er nur einen dieser beiden Werte enthält.In der Regel verwendet die Implementierung die Ganzzahl
0
fürfalse
und1
fürtrue
, um die Konvertierungen zwischenbool
und zu vereinfachenint
undif (boolvar)
den gleichen Code wie zu generierenif (intvar)
. In diesem Fall kann man sich vorstellen, dass der Code, der für das Ternär in der Zuweisung generiert wird, den Wert als Index für ein Array von Zeigern auf die beiden Zeichenfolgen verwendet, dh in Folgendes konvertiert wird:Wenn
boolValue
es nicht initialisiert ist, kann es tatsächlich einen beliebigen ganzzahligen Wert enthalten, der dann zu einem Zugriff außerhalb der Grenzen desstrings
Arrays führen würde.quelle
bool
toint
mit*(int *)&boolValue
und drucken Sie es zu Debugging-Zwecken aus. Überprüfen Sie, ob es sich um etwas anderes handelt als0
oder1
wenn es abstürzt. Wenn dies der Fall ist, bestätigt dies ziemlich genau die Theorie, dass der Compiler das Inline-If als Array optimiert, was erklärt, warum es abstürzt.std::bitset<8>
gibt mir keine schönen Namen für alle meine verschiedenen Flags. Je nachdem, was sie sind, kann das wichtig sein.Wenn Sie Ihre Frage häufig zusammenfassen, fragen Sie: Erlaubt der C ++ - Standard einem Compiler anzunehmen, dass a
bool
nur eine interne numerische Darstellung von '0' oder '1' haben kann, und verwendet sie so?Der Standard sagt nichts über die interne Darstellung von a aus
bool
. Es definiert nur, was passiert, wenn abool
in a umgewandelt wirdint
(oder umgekehrt). Aufgrund dieser integralen Konvertierungen (und der Tatsache, dass die Benutzer sich ziemlich stark auf sie verlassen) verwendet der Compiler meistens 0 und 1, muss dies jedoch nicht (obwohl er die Einschränkungen eines von ihm verwendeten ABI niedrigerer Ebene berücksichtigen muss ).Wenn der Compiler a sieht,
bool
ist er berechtigt zu berücksichtigen, dass erbool
entweder das Bitmuster 'true
' oder 'false
' enthält, und alles zu tun, wie es sich anfühlt. Also , wenn die Werte fürtrue
undfalse
sind 1 bzw. 0, wird der Compiler in der Tat zu optimieren erlaubtstrlen
zu5 - <boolean value>
. Andere lustige Verhaltensweisen sind möglich!Wie hier wiederholt festgestellt wird, hat undefiniertes Verhalten undefinierte Ergebnisse. Einschließlich, aber nicht beschränkt auf
Sehen Sie, was jeder Programmierer über undefiniertes Verhalten wissen sollte
quelle