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 Bitmuster false=0
undtrue=1
in den niedrigen 8 Bits des Registers 1 dargestellt wird . Im Speicher bool
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
mit xor 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 . Oder a&&b
zu einem bitweisen UND für bool
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)
auf
5U - boolValue
. (Übrigens ist diese Optimierung etwas clever, aber vielleicht kurzsichtig im Vergleich zu Verzweigung und Inlining memcpy
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 verwenden uninitializedBool
. Es hat Platz für das Objekt in main
with geschaffen push rax
(was kleiner und aus verschiedenen Gründen ungefähr so effizient ist wie sub rsp, 8
), sodass main
der Wert, für den es verwendet wurde, der Müll ist, der sich bei der Eingabe in AL befand uninitializedBool
. Deshalb haben Sie tatsächlich Werte erhalten, die nicht nur waren 0
.
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
und true=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 funktioniert bool
, 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 ändernbool
. (z. B. indem memcpy
Sie das bool
In eingeben unsigned char
, was Sie tun dürfen, weil char*
es alles aliasieren kann. Und es unsigned 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 sich char 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 mitnoinline
. 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 der class{}
Definition enthalten sein, sodass alle Übersetzungseinheiten dieselbe Definition haben müssen. Wie beim inline
Schlüsselwort.)
Ein Compiler könnte also nur eine ret
oder ud2
(unzulässige Anweisung) als Definition für ausgeben main
, 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 eine ret
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 Nichtfunktion void
lässt gcc manchmal eine ret
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? Denn alignof(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 verwenden clang/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
nicht a<0
als immer falsch optimiert , nur das tmp
ist immer negativ. (Weil INT_MIN
+ a=INT_MAX
für das Komplementziel dieser 2 negativ ist und a
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 ein void*
oder char*
. 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 verwendet bool_func(a&1)
. Der Anrufer könnte das optimieren, &1
da er dies bereits für das niedrige Byte als Teil von and 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 mit movzx 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 Argument mov dil, byte [r10]
anstelle von movzx edi, byte [r10]
, weil beide ohnehin ein REX-Präfix benötigen.
Aus diesem Grunde Klirren aussendet movzx eax, dil
in Serialize
, statt sub 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 macht bool
.)
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 wird memcpy
möglicherweise rep 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.)
Es toleriert das Kopieren von nicht initialisiertem Speicher sowie einfache logische und arithmetische Operationen damit. Im Allgemeinen verfolgt MemorySanitizer die Verbreitung nicht initialisierter Daten im Speicher stillschweigend und meldet eine Warnung, wenn ein Codezweig abhängig von einem nicht initialisierten Wert genommen (oder nicht genommen) wird.
MemorySanitizer implementiert eine Teilmenge der Funktionen von Valgrind (Memcheck-Tool).
Dies sollte in diesem Fall funktionieren, da der Aufruf von glibc memcpy
mit einem length
aus nicht initialisiertem Speicher berechneten Speicher (innerhalb der Bibliothek) zu einem Zweig führt, der auf basiert length
. Wenn es eine vollständig verzweigungslose Version eingebunden hätte, die nur cmov
Indexierung und zwei Speicher verwendet, hätte es möglicherweise nicht funktioniert.
Valgrind'smemcheck
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.