Wenn ich habe:
unsigned int x;
x -= x;
Es ist klar, dass nach diesem Ausdruck Null sein x
sollte , aber überall, wo ich hinschaue, sagen sie, dass das Verhalten dieses Codes undefiniert ist, nicht nur der Wert von x
(bis vor der Subtraktion).
Zwei Fragen:
Ist das Verhalten dieses Codes tatsächlich undefiniert?
(Könnte der Code beispielsweise auf einem kompatiblen System abstürzen?)Wenn ja, warum sagt C, dass das Verhalten undefiniert ist, wenn klar ist, dass
x
hier Null sein sollte?dh Was ist der Vorteil , wenn das Verhalten hier nicht definiert wird?
Natürlich könnte der Compiler verwenden Sie einfach was auch immer Müll Wert , den es in der Variable „praktisch“ erachtet, und es würde wie beabsichtigt funktionieren ... was mit diesem Ansatz falsch?
c
undefined-behavior
initialization
user541686
quelle
quelle
x -= x
. Die Frage ist, warum der Zugriff auf nicht initialisierte Werte überhaupt UB ist.Antworten:
Ja, dieses Verhalten ist undefiniert, aber aus anderen Gründen, als die meisten Menschen wissen.
Erstens ist die Verwendung eines einheitlichen Werts an sich kein undefiniertes Verhalten, sondern der Wert ist einfach unbestimmt. Der Zugriff darauf ist dann UB, wenn der Wert zufällig eine Trap-Darstellung für den Typ ist. Vorzeichenlose Typen haben selten Trap-Darstellungen, sodass Sie auf dieser Seite relativ sicher sind.
Was das Verhalten undefiniert macht, ist eine zusätzliche Eigenschaft Ihrer Variablen, nämlich dass sie "hätte deklariert werden können
register
", dh ihre Adresse wird niemals verwendet. Solche Variablen werden speziell behandelt, weil es Architekturen gibt, die echte CPU-Register haben, die eine Art zusätzlichen Status haben, der "nicht initialisiert" ist und keinem Wert in der Typdomäne entspricht.Bearbeiten: Der relevante Satz des Standards ist 6.3.2.1p2:
Und es deutlicher zu machen, der folgende Code ist unter allen Umständen legal:
unsigned char a, b; memcpy(&a, &b, 1); a -= a;
a
undb
genommen, so dass ihr Wert nur unbestimmt ist.unsigned char
niemals Trap-Darstellungen gibt, deren unbestimmter Wert nur nicht spezifiziert ist, kann jeder Wert vonunsigned char
passieren.a
muss der Wert gehalten werden0
.Edit2:
a
undb
haben nicht spezifizierte Werte:quelle
unsigned
es sicher Fallendarstellungen geben kann. Können Sie auf den Teil des Standards verweisen, der dies sagt? Ich sehe in §6.2.6.2 / 1 Folgendes: "Für vorzeichenlose Ganzzahltypen außer vorzeichenlosen Zeichen werden die Bits der Objektdarstellung in zwei Gruppen unterteilt: Wertbits und Füllbits (es muss keines der letzteren geben). ... dies wird als Wertdarstellung bezeichnet. Die Werte aller Füllbits sind nicht angegeben. ⁴⁴⁾ "mit dem Kommentar:" ⁴⁴⁾ Einige Kombinationen von Füllbits können Trap-Darstellungen erzeugen ".unsigned char
, aber diese Antwort wird verwendetunsigned char
. Beachten Sie jedoch: Ein streng konformes Programm kannsizeof(unsigned) * CHAR_BIT
basierend darauf berechnen und bestimmenUINT_MAX
, dass bestimmte Implementierungen möglicherweise keine Trap-Darstellungen für haben könnenunsigned
. Nachdem dieses Programm diese Feststellung getroffen hat, kann es genau das tun, was diese Antwort bewirktunsigned char
.memcpy
eine Ablenkung, dh würde Ihr Beispiel nicht immer noch zutreffen, wenn es durch ersetzt würde*&a = *&b;
.unsigned char
und damitmemcpy
hilfreich ist, der für*&
weniger klar ist. Ich werde berichten, sobald sich dies beruhigt hat.Der C-Standard bietet Compilern viel Spielraum für Optimierungen. Die Konsequenzen dieser Optimierungen können überraschend sein, wenn Sie ein naives Programmmodell annehmen, bei dem der nicht initialisierte Speicher auf ein zufälliges Bitmuster eingestellt ist und alle Operationen in der Reihenfolge ausgeführt werden, in der sie geschrieben wurden.
Hinweis: Die folgenden Beispiele sind nur gültig, da
x
ihre Adresse nie vergeben wurde und sie daher "registerartig" ist. Sie wären auch gültig, wenn die Art derx
Fallen Darstellungen hätte; Dies ist selten bei nicht signierten Typen der Fall (es erfordert mindestens ein Bit Speicherplatz und muss dokumentiert werden) und ist für unmöglichunsigned char
. Wennx
ein vorzeichenbehafteter Typ vorhanden wäre, könnte die Implementierung das Bitmuster, das keine Zahl zwischen - (2 n-1 -1) und 2 n-1 -1 ist, als Trap-Darstellung definieren. Siehe Jens Gustedts Antwort .Compiler versuchen, Variablen Register zuzuweisen, da Register schneller als Speicher sind. Da das Programm möglicherweise mehr Variablen verwendet, als der Prozessor Register hat, führen Compiler eine Registerzuordnung durch, was dazu führt, dass unterschiedliche Variablen dasselbe Register zu unterschiedlichen Zeiten verwenden. Betrachten Sie das Programmfragment
unsigned x, y, z; /* 0 */ y = 0; /* 1 */ z = 4; /* 2 */ x = - x; /* 3 */ y = y + z; /* 4 */ x = y + 1; /* 5 */
Wenn Zeile 3 ausgewertet wird,
x
ist sie noch nicht initialisiert, daher muss Zeile 3 (aus Gründen des Compilers) eine Art Zufall sein, der aufgrund anderer Bedingungen, die der Compiler nicht klug genug war, um dies herauszufinden, nicht auftreten kann. Daz
es nicht nach Zeile 4 undx
nicht vor Zeile 5 verwendet wird, kann für beide Variablen dasselbe Register verwendet werden. Dieses kleine Programm ist also für die folgenden Operationen an Registern kompiliert:r1 = 0; r0 = 4; r0 = - r0; r1 += r0; r0 = r1;
Der Endwert von
x
ist der Endwert vonr0
und der Endwert vony
ist der Endwert vonr1
. Diese Werte sind x = -3 und y = -4 und nicht 5 und 4, wie dies beix
ordnungsgemäßer Initialisierung der Fall wäre .Betrachten Sie für ein ausführlicheres Beispiel das folgende Codefragment:
unsigned i, x; for (i = 0; i < 10; i++) { x = (condition() ? some_value() : -x); }
Angenommen, der Compiler erkennt, dass dies
condition
keine Nebenwirkungen hat. Dacondition
sich nichts ändertx
, weiß der Compiler, dass der erste Durchlauf durch die Schleife möglicherweise nicht zugänglich ist,x
da er noch nicht initialisiert ist. Daher ist die erste Ausführung des Schleifenkörpers äquivalent zux = some_value()
, es besteht keine Notwendigkeit, die Bedingung zu testen. Der Compiler kann diesen Code so kompilieren, als hätten Sie geschriebenunsigned i, x; i = 0; /* if some_value() uses i */ x = some_value(); for (i = 1; i < 10; i++) { x = (condition() ? some_value() : -x); }
Die Art und Weise dies innerhalb des Compilers modelliert werden kann , ist zu berücksichtigen , dass jeder Wert in Abhängigkeit von
x
hat , was Wert ist praktisch , solangex
nicht initialisiert ist. Da das Verhalten, wenn eine nicht initialisierte Variable nicht definiert ist, anstatt dass die Variable lediglich einen nicht angegebenen Wert hat, muss der Compiler keine spezielle mathematische Beziehung zwischen den für Sie geeigneten Werten verfolgen. Somit kann der Compiler den obigen Code folgendermaßen analysieren:x
nicht initialisiert, wenn die Zeit-x
ausgewertet wird.-x
hat undefiniertes Verhalten, daher ist sein Wert was auch immer-bequem ist.condition ? value : value
condition; value
Wenn derselbe Compiler mit dem Code in Ihrer Frage konfrontiert wird, analysiert er, dass bei der
x = - x
Auswertung der Wert von-x
was auch immer zweckmäßig ist. So kann die Zuordnung weg optimiert werden.Ich habe nicht nach einem Beispiel für einen Compiler gesucht, der sich wie oben beschrieben verhält, aber es ist die Art von Optimierungen, die gute Compiler versuchen. Ich wäre nicht überrascht, wenn ich einem begegnen würde. Hier ist ein weniger plausibles Beispiel für einen Compiler, mit dem Ihr Programm abstürzt. (Es ist möglicherweise nicht so unplausibel, wenn Sie Ihr Programm in einem erweiterten Debugging-Modus kompilieren.)
Dieser hypothetische Compiler ordnet jede Variable auf einer anderen Speicherseite zu und richtet Seitenattribute so ein, dass das Lesen aus einer nicht initialisierten Variablen einen Prozessor-Trap verursacht, der einen Debugger aufruft. Jede Zuordnung zu einer Variablen stellt zunächst sicher, dass ihre Speicherseite normal zugeordnet ist. Dieser Compiler versucht nicht, eine erweiterte Optimierung durchzuführen. Er befindet sich in einem Debugging-Modus, um Fehler wie nicht initialisierte Variablen leicht zu lokalisieren. Bei der
x = - x
Auswertung verursacht die rechte Seite eine Falle und der Debugger wird gestartet.quelle
x
hat er einen nicht initialisierten Wert, aber das Verhalten beim Zugriff würde dies tun definiert werden, wenn x kein registerähnliches Verhalten hatte.x
, können alle Operationen darauf weggelassen werden, unabhängig davon, ob sein Wert definiert wurde oder nicht. Wenn Code, der zB folgtif (volatile1) x=volatile2; ... x = (x+volatile3) & 255;
, gleichermaßen mit einem Wert von 0 bis 255 zufrieden wäre, derx
in dem Fall enthalten sein könnte, in demvolatile1
Null ergeben hätte, würde ich denken, dass eine Implementierung, die es dem Programmierer ermöglicht, ein unnötiges Schreiben wegzulassen,x
als eine höhere Qualität angesehen werden sollte als eine, die würde sich benehmen ...Ja, das Programm könnte abstürzen. Es kann beispielsweise Trap-Darstellungen geben (bestimmte Bitmuster, die nicht behandelt werden können), die einen CPU-Interrupt verursachen können, der das Programm zum Absturz bringen kann.
(Diese Erklärung gilt nur für Plattformen, auf denen
unsigned int
Trap-Darstellungen vorhanden sein können, was auf realen Systemen selten vorkommt. Einzelheiten und Verweise auf alternative und möglicherweise häufigere Ursachen, die zum aktuellen Wortlaut des Standards führen, finden Sie in den Kommentaren.)quelle
(Diese Antwort bezieht sich auf C 1999. Für C 2011 siehe Jens Gustedts Antwort.)
Der C-Standard besagt nicht, dass die Verwendung des Werts eines Objekts mit automatischer Speicherdauer, das nicht initialisiert wurde, ein undefiniertes Verhalten ist. In der Norm C 1999 heißt es in 6.7.8 10: „Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt.“ (In diesem Abschnitt wird weiter definiert, wie statische Objekte initialisiert werden. Die einzigen nicht initialisierten Objekte, um die wir uns kümmern, sind automatische Objekte.)
3.17.2 definiert "unbestimmten Wert" als "entweder einen nicht spezifizierten Wert oder eine Trap-Darstellung". 3.17.3 definiert "nicht spezifizierter Wert" als "gültiger Wert des relevanten Typs, wenn diese Internationale Norm keine Anforderungen stellt, für welchen Wert in irgendeinem Fall gewählt wird".
Wenn also der nicht initialisierte
unsigned int x
Wert einen nicht angegebenen Wert hat,x -= x
muss er Null erzeugen. Damit bleibt die Frage, ob es sich möglicherweise um eine Fallendarstellung handelt. Der Zugriff auf einen Trap-Wert führt gemäß 6.2.6.1 zu undefiniertem Verhalten. 5.Einige Arten von Objekten können Trap-Darstellungen aufweisen, z. B. die Signalisierungs-NaNs von Gleitkommazahlen. Ganzzahlen ohne Vorzeichen sind jedoch etwas Besonderes. Gemäß 6.2.6.2 repräsentiert jedes der N Wertbits eines vorzeichenlosen int eine Potenz von 2, und jede Kombination der Wertbits repräsentiert einen der Werte von 0 bis 2 N -1. Ganzzahlen ohne Vorzeichen können daher nur aufgrund einiger Werte in ihren Füllbits (z. B. eines Paritätsbits) Trap-Darstellungen haben.
Wenn auf Ihrer Zielplattform ein vorzeichenloses int keine Auffüllbits hat, kann ein nicht initialisiertes vorzeichenloses int keine Trap-Darstellung haben, und die Verwendung seines Werts kann kein undefiniertes Verhalten verursachen.
quelle
x
es eine Trap-Darstellung gibt, dannx -= x
könnte Trap, oder? Dennoch muss +1 für den Hinweis auf vorzeichenlose Ganzzahlen ohne zusätzliche Bits ein definiertes Verhalten haben - es ist eindeutig das Gegenteil der anderen Antworten und (laut Zitat) scheint es das zu sein, was der Standard impliziert.x
eine Trap-Darstellung hat, wirdx -= x
möglicherweise Trap ausgeführt. Selbstx
wenn es einfach als Wert verwendet wird, kann es zu einer Falle kommen. (Es ist sicherx
als l-Wert zu verwenden; das Schreiben in ein Objekt wird nicht durch eine darin enthaltene Trap-Darstellung beeinflusst.)Ja, es ist undefiniert. Der Code kann abstürzen. C sagt, das Verhalten sei undefiniert, da es keinen bestimmten Grund gibt, eine Ausnahme von der allgemeinen Regel zu machen. Der Vorteil ist der gleiche Vorteil wie in allen anderen Fällen von undefiniertem Verhalten - der Compiler muss keinen speziellen Code ausgeben, damit dies funktioniert.
Warum denkst du, passiert das nicht? Das ist genau der Ansatz. Der Compiler ist nicht erforderlich, damit es funktioniert, aber es ist nicht erforderlich, damit es fehlschlägt.
quelle
x
dies als deklariert werden könnteregister
, das heißt, dass seine Adresse niemals vergeben wird. Ich weiß nicht, ob Sie sich dessen bewusst waren (wenn Sie es effektiv versteckt haben), aber eine richtige Antwort muss es erwähnen.Für jede Variable eines beliebigen Typs, die nicht initialisiert ist oder aus anderen Gründen einen unbestimmten Wert enthält, gilt Folgendes für den Code, der diesen Wert liest:
Andernfalls nimmt die Variable einen nicht angegebenen Wert an, wenn keine Trap-Darstellungen vorhanden sind. Es gibt keine Garantie dafür, dass dieser nicht angegebene Wert bei jedem Lesen der Variablen konsistent ist. Es ist jedoch garantiert, dass es sich nicht um eine Trap-Darstellung handelt, und es wird daher garantiert, dass kein undefiniertes Verhalten hervorgerufen wird [3].
Der Wert kann dann sicher verwendet werden, ohne einen Programmabsturz zu verursachen, obwohl dieser Code nicht auf Systeme mit Trap-Darstellungen portierbar ist.
[1]: C11 6.3.2.1:
[2]: C11 6.2.6.1:
[3] C11:
quelle
stdint.h
immer anstelle der nativen Typen von C verwendet werden sollte.stdint.h
Erzwingt das 2er-Komplement und keine Auffüllbits. Mit anderen Worten, diestdint.h
Typen dürfen nicht voller Mist sein.Während sich viele Antworten auf Prozessoren konzentrieren, die den Zugriff auf nicht initialisierte Register abfangen, können selbst auf Plattformen ohne solche Traps eigenartige Verhaltensweisen auftreten, wenn Compiler verwendet werden, die keine besonderen Anstrengungen unternehmen, um UB auszunutzen. Betrachten Sie den Code:
volatile uint32_t a,b; uin16_t moo(uint32_t x, uint16_t y, uint32_t z) { uint16_t temp; if (a) temp = y; else if (b) temp = z; return temp; }
Ein Compiler für eine Plattform wie den ARM, bei der alle Befehle außer Laden und Speichern in 32-Bit-Registern ausgeführt werden, kann den Code in angemessener Weise verarbeiten, wie:
volatile uint32_t a,b; // Note: y is known to be 0..65535 // x, y, and z are received in 32-bit registers r0, r1, r2 uin32_t moo(uint32_t x, uint32_t y, uint32_t z) { // Since x is never used past this point, and since the return value // will need to be in r0, a compiler could map temp to r0 uint32_t temp; if (a) temp = y; else if (b) temp = z & 0xFFFF; return temp; }
Wenn einer der flüchtigen Lesevorgänge einen Wert ungleich Null ergibt, wird r0 mit einem Wert im Bereich von 0 bis 65535 geladen. Andernfalls wird alles ausgegeben, was beim Aufruf der Funktion enthalten war (dh der an x übergebene Wert), der möglicherweise kein Wert im Bereich 0..65535 ist. Dem Standard fehlt eine Terminologie zur Beschreibung des Verhaltens von Werten, deren Typ uint16_t ist, deren Wert jedoch außerhalb des Bereichs von 0..65535 liegt, mit der Ausnahme, dass jede Aktion, die ein solches Verhalten erzeugen könnte, UB aufruft.
quelle
uint16_t
die manchmal als 123 und manchmal als 6553623 gelesen werden kann. Wenn das Ergebnis ignoriert wird ...register
, dann kann es zusätzliche Bits hat, die das Verhalten potenziell unbestimmt machen. Genau das sagst du, oder?