Ermöglicht der C ++ - Standard, dass ein nicht initialisierter Bool ein Programm zum Absturz bringt?

500

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 trueoder falsein 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, boolValuespielt 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.

Remz
quelle
200
Das ist eine gute Frage. Es ist ein gutes Beispiel dafür, dass undefiniertes Verhalten nicht nur ein theoretisches Problem ist. Wenn Leute sagen, dass durch UB etwas passieren kann, kann "alles" wirklich ziemlich überraschend sein. Man könnte annehmen, dass sich undefiniertes Verhalten immer noch auf vorhersehbare Weise manifestiert, aber heutzutage mit modernen Optimierern ist das überhaupt nicht wahr. OP nahm sich die Zeit, um ein MCVE zu erstellen, untersuchte das Problem gründlich, inspizierte die Demontage und stellte eine klare, unkomplizierte Frage. Konnte nicht mehr verlangen.
John Kugelman
7
Beachten Sie, dass die Anforderung, dass "ungleich Null ausgewertet wird true", eine Regel für boolesche Operationen ist, einschließlich "Zuweisung zu einem Bool" (die static_cast<bool>()abhängig von den Besonderheiten implizit eine aufrufen kann ). Es ist jedoch keine Anforderung an die interne Darstellung eines boolvom Compiler ausgewählten.
Euro Micelli
2
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew
3
In einem sehr verwandten Zusammenhang ist dies eine "lustige" Quelle für binäre Inkompatibilität. Wenn Sie einen ABI A haben, der vor dem Aufrufen einer Funktion Werte auf Null setzt, aber Funktionen so kompiliert, dass davon ausgegangen wird, dass die Parameter auf Null aufgefüllt sind, und einen ABI B, der das Gegenteil ist (kein Null-Pad, aber keine Null annimmt) -gefüllte Parameter), es wird meistens funktionieren, aber eine Funktion, die den B-ABI verwendet, verursacht Probleme, wenn sie eine Funktion aufruft, die den A-ABI verwendet, der einen 'kleinen' Parameter akzeptiert. IIRC Sie haben dies auf x86 mit Clang und ICC.
TLW
1
@TLW: Obwohl der Standard nicht vorschreibt, dass Implementierungen Mittel zum Aufrufen oder Aufrufen durch externen Code bereitstellen, wäre es hilfreich gewesen, solche Dinge für Implementierungen anzugeben, bei denen sie relevant sind (Implementierungen, bei denen solche Details nicht relevant sind) relevant könnte solche Attribute ignorieren).
Supercat

Antworten:

285

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 boolals Funktion arg in einem Register durch die Bitmuster false=0undtrue=1 in den niedrigen 8 Bits des Registers 1 dargestellt wird . Im Speicher boolbefindet 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 boolfür keine Architektur (nicht nur x86) 0 oder 1 annehmen darf. Es ermöglicht Optimierungen wie !myboolmit xor eax,1dem Low - Bit Flip: jeden möglichen Code, der ein Bit / integer / bool zwischen 0 und 1 in einzelnem CPU - Befehl Flip kann . Oder a&&bzu einem bitweisen UND für boolTypen 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 memcpyals 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 mainwith geschaffen push rax(was kleiner und aus verschiedenen Gründen ungefähr so ​​effizient ist wie sub rsp, 8), sodass mainder 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 garbagekann 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=0und 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 memcpySie das boolIn eingeben unsigned char, was Sie tun dürfen, weil char*es alles aliasieren kann. Und es unsigned charwird 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_boolnatü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 inlineSchlüsselwort.)

Ein Compiler könnte also nur eine retoder ud2(unzulässige Anweisung) als Definition für ausgeben main, da der Ausführungspfad, der am Anfang von beginnt, mainunvermeidlich 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 retoder 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 ud2auf 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 voidlässt gcc manchmal eine retAnweisung 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) == 2und 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 -WallWarnungen 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_MINnicht a<0als immer falsch optimiert , nur das tmpist immer negativ. (Weil INT_MIN+ a=INT_MAXfür das Komplementziel dieser 2 negativ ist und anicht 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 & 0x01010101in RDI berechnet und es vor dem Anruf für etwas anderes verwendet bool_func(a&1). Der Anrufer könnte das optimieren, &1da er dies bereits für das niedrige Byte als Teil von and edi, 0x01010101getan 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, dilin 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- movSofort 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 + cmovund 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 movsbmöglicherweise optimal. glibc wird memcpymö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 -pieErkennen 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 -O2in 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 memcpymit einem lengthaus 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 cmovIndexierung 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.

Peter Cordes
quelle
2
Ich habe einen schlimmeren Fall gesehen, in dem die Variable einen Wert angenommen hat, der nicht im Bereich einer 8-Bit-Ganzzahl liegt, sondern nur des gesamten CPU-Registers. Und Itanium hat noch eine schlechtere, die Verwendung einer nicht initialisierten Variablen kann sofort abstürzen.
Joshua
2
@Joshua: Oh, richtig, guter Punkt, Itaniums explizite Spekulation wird Registerwerte mit einem Äquivalent von "keine Zahl" kennzeichnen, so dass die Wertfehler verwendet werden.
Peter Cordes
11
Dies zeigt auch, warum der UB-Featurebug in erster Linie in das Design der Sprachen C und C ++ eingeführt wurde: weil er dem Compiler genau diese Freiheit gibt, die es den modernsten Compilern nun ermöglicht hat, diese hohe Qualität auszuführen Optimierungen, die C / C ++ zu solchen leistungsstarken Mittelsprachen machen.
The_Sympathizer
2
Und so geht der Krieg zwischen C ++ - Compiler-Autoren und C ++ - Programmierern, die versuchen, nützliche Programme zu schreiben, weiter. Diese Antwort, die bei der Beantwortung dieser Frage völlig umfassend ist, könnte auch als überzeugende Anzeigenkopie für Anbieter statischer Analysetools verwendet werden ...
Davidbak
4
@The_Sympathizer: UB wurde hinzugefügt, damit sich Implementierungen so verhalten können, wie es für ihre Kunden am nützlichsten ist . Es sollte nicht vorgeschlagen werden, dass alle Verhaltensweisen als gleich nützlich angesehen werden sollten.
Supercat
56

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 trueoder false). Der trueWert muss nicht mit der Ganzzahl 1 identisch sein - tatsächlich kann es verschiedene Darstellungen von trueund geben false-, 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 boolkö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:

50) Die Verwendung eines Bool-Werts auf eine Weise, die in dieser Internationalen Norm als „undefiniert“ beschrieben wird, z. B. durch Untersuchen des Werts eines nicht initialisierten automatischen Objekts, kann dazu führen, dass es sich so verhält, als ob es weder wahr noch falsch ist. (Fußnote zu Absatz 6 von §6.9.1, Grundtypen)

Rici
quelle
11
Der " trueWert 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 als true/ zu sehen false), trueist immer 1und falseimmer0 . 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 das booltatsächliche Bitmuster nur 0oder sein könnte 1), so dass es für das OP-Problem irgendwie irrelevant ist.
ShadowRanger
4
@ShadowRanger Sie können die Objektdarstellung jederzeit direkt überprüfen.
TC
7
@ Shadowranger: Mein Punkt ist, dass die Implementierung verantwortlich ist. Wenn es gültige Darstellungen trueauf das Bitmuster beschränkt 1, 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, boolindem Sie sie in ein Byte-Array kopieren. das ist nicht UB (aber es ist implementierungsdefiniert)
Rici
3
Ja, bei der Optimierung von Compilern (dh bei der Implementierung von C ++ in der realen Welt) wird häufig Code ausgegeben, der von booleinem Bitmuster von 0oder abhängt 1. Sie booleschen nicht booljedes 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||bum or eax, ediin einer Funktion zurückkehren bool, oder MSVC optimieren a&bzu test cl, dl. x86 testist ein bitweises and , also setzt if cl=1und dl=2test Flags entsprechend cl&dl = 0.
Peter Cordes
5
Der Punkt über undefiniertes Verhalten ist, dass der Compiler weitaus mehr Schlussfolgerungen daraus ziehen darf, z. B. anzunehmen, dass ein Codepfad, der zum Zugriff auf einen nicht initialisierten Wert führen würde, überhaupt nicht verwendet wird, da dies genau in der Verantwortung des Programmierers liegt . Es geht also nicht nur um die Möglichkeit, dass die niedrigen Werte von Null oder Eins abweichen können.
Holger
52

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.

MM
quelle
2
Ist die erste Klausel wahr? Wird durch einfaches Kopieren eines nicht initialisierten boolTriggers UB ausgelöst?
Joshua Green
10
@JoshuaGreen siehe [dcl.init] / 12 "Wenn durch eine Auswertung ein unbestimmter Wert erzeugt wird, ist das Verhalten außer in den folgenden Fällen undefiniert:" (und keiner dieser Fälle hat eine Ausnahme für bool). Das Kopieren erfordert die Auswertung der Quelle
MM
8
@JoshuaGreen Und der Grund dafür ist, dass Sie möglicherweise eine Plattform haben, die einen Hardwarefehler auslöst, wenn Sie für einige Typen auf ungültige Werte zugreifen. Diese werden manchmal als "Fallendarstellungen" bezeichnet.
David Schwartz
7
Itanium ist zwar dunkel, aber eine CPU, die noch in Produktion ist, Trap-Werte aufweist und über mindestens zwei halbmoderne C ++ - Compiler (Intel / HP) verfügt. Es hat buchstäblich true, falseund not-a-thingWerte für booleans.
MSalters
3
Auf der anderen Seite lautet die Antwort auf "Erfordert der Standard, dass alle Compiler etwas auf eine bestimmte Weise verarbeiten" im Allgemeinen "Nein", selbst / insbesondere in Fällen, in denen es offensichtlich ist, dass ein Qualitätscompiler dies tun sollte. Je offensichtlicher etwas ist, desto weniger sollte es für die Autoren des Standards notwendig sein, es tatsächlich zu sagen.
Supercat
23

Ein Bool darf nur die implementierungsabhängigen Werte enthalten, die intern für trueund verwendet werden false, und der generierte Code kann davon ausgehen, dass er nur einen dieser beiden Werte enthält.

In der Regel verwendet die Implementierung die Ganzzahl 0für falseund 1für true, um die Konvertierungen zwischen boolund zu vereinfachen intund if (boolvar)den gleichen Code wie zu generieren if (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:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Wenn boolValuees nicht initialisiert ist, kann es tatsächlich einen beliebigen ganzzahligen Wert enthalten, der dann zu einem Zugriff außerhalb der Grenzen des stringsArrays führen würde.

Barmar
quelle
1
@ SidS Danke. Theoretisch könnten die internen Darstellungen das Gegenteil davon sein, wie sie in / aus ganzen Zahlen umgewandelt werden, aber das wäre pervers.
Barmar
1
Sie haben Recht, und Ihr Beispiel wird ebenfalls abstürzen. Für eine Codeüberprüfung ist jedoch "sichtbar", dass Sie eine nicht initialisierte Variable als Index für ein Array verwenden. Außerdem würde es sogar beim Debuggen abstürzen (zum Beispiel wird ein Debugger / Compiler mit bestimmten Mustern initialisiert, um leichter erkennen zu können, wann es abstürzt). In meinem Beispiel ist der überraschende Teil, dass die Verwendung des Bools unsichtbar ist: Der Optimierer hat beschlossen, ihn in einer Berechnung zu verwenden, die im Quellcode nicht vorhanden ist.
Remz
3
@Remz Ich verwende nur das Array, um zu zeigen, was der generierte Code bedeuten könnte, und schlage nicht vor, dass jemand das tatsächlich schreiben würde.
Barmar
1
@Remz Neufassung des boolto intmit *(int *)&boolValueund drucken Sie es zu Debugging-Zwecken aus. Überprüfen Sie, ob es sich um etwas anderes handelt als 0oder 1wenn 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.
Havenard
2
@ MSalters: std::bitset<8>gibt mir keine schönen Namen für alle meine verschiedenen Flags. Je nachdem, was sie sind, kann das wichtig sein.
Martin Bonner unterstützt Monica
15

Wenn Sie Ihre Frage häufig zusammenfassen, fragen Sie: Erlaubt der C ++ - Standard einem Compiler anzunehmen, dass a boolnur 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 a boolin a umgewandelt wird int(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, boolist er berechtigt zu berücksichtigen, dass er boolentweder das Bitmuster ' true' oder ' false' enthält, und alles zu tun, wie es sich anfühlt. Also , wenn die Werte für trueund falsesind 1 bzw. 0, wird der Compiler in der Tat zu optimieren erlaubt strlenzu 5 - <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

  • Ihr Code funktioniert wie erwartet
  • Ihr Code schlägt zu zufälligen Zeiten fehl
  • Ihr Code wird überhaupt nicht ausgeführt.

Sehen Sie, was jeder Programmierer über undefiniertes Verhalten wissen sollte

Tom Tanner
quelle