Ich habe einen extrem bösen Käfer aufgespürt, der sich hinter diesem kleinen Juwel versteckt. Mir ist bekannt, dass gemäß der C ++ - Spezifikation signierte Überläufe ein undefiniertes Verhalten sind, jedoch nur dann, wenn der Überlauf auftritt, wenn der Wert auf Bitbreite erweitert wird sizeof(int)
. So wie ich es verstehe, sollte das Inkrementieren von a char
niemals undefiniertes Verhalten sein, solange sizeof(char) < sizeof(int)
. Das erklärt aber nicht, wie c
man einen unmöglichen Wert bekommt . Wie kann eine 8-Bit-Ganzzahl c
Werte enthalten, die größer als ihre Bitbreite sind?
Code
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
Ausgabe
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
Probieren Sie es auf ideone aus.
c++
gcc
undefined-behavior
Ohne Vorzeichen
quelle
quelle
printf()
funktioniert Konvertierung?Antworten:
Dies ist ein Compiler-Fehler.
Obwohl es eine gültige Konsequenz ist, unmögliche Ergebnisse für undefiniertes Verhalten zu erhalten, enthält Ihr Code tatsächlich kein undefiniertes Verhalten. Was ist passiert , dass der Compiler denkt ist das Verhalten nicht definiert und optimiert entsprechend.
Wenn
c
ist wie folgt definiertint8_t
, undint8_t
fördert aufint
, dannc--
sollte die Subtraktion auszuführenc - 1
inint
Arithmetik und das Ergebnis zurück zu konvertierenint8_t
. Die Subtraktion inint
läuft nicht über, und die Konvertierung von Integralwerten außerhalb des Bereichs in einen anderen Integraltyp ist gültig. Wenn der Zieltyp signiert ist, ist das Ergebnis implementierungsdefiniert, es muss jedoch ein gültiger Wert für den Zieltyp sein. (Und wenn der Zieltyp nicht signiert ist, ist das Ergebnis genau definiert, aber das gilt hier nicht.)quelle
c
eines breiteren Typs beizubehalten. Vermutlich passiert das hier.Ein Compiler kann Fehler aufweisen, die nicht dem Standard entsprechen, da andere Anforderungen bestehen. Ein Compiler sollte mit anderen Versionen von sich selbst kompatibel sein. Es kann auch erwartet werden, dass es in gewisser Weise mit anderen Compilern kompatibel ist und auch einigen Überzeugungen über das Verhalten entspricht, die von der Mehrheit seiner Benutzer gehalten werden.
In diesem Fall scheint es sich um einen Konformitätsfehler zu handeln. Der Ausdruck
c--
solltec
auf ähnliche Weise wie manipuliert werdenc = c - 1
. Hier wird der Wert vonc
rechts zum Typ heraufgestuftint
, und dann findet die Subtraktion statt. Da diese Subtraktionc
im Bereich von liegtint8_t
, läuft sie nicht über, kann jedoch einen Wert erzeugen, der außerhalb des Bereichs von liegtint8_t
. Wenn dieser Wert zugewiesen wird, erfolgt eine Konvertierung zurück in den Typ,int8_t
sodass das Ergebnis wieder in den Typ passtc
. Im Fall außerhalb des Bereichs hat die Konvertierung einen implementierungsdefinierten Wert. Ein Wert außerhalb des Bereichs vonint8_t
ist jedoch kein gültiger implementierungsdefinierter Wert. Eine Implementierung kann nicht "definieren", dass ein 8-Bit-Typ plötzlich 9 oder mehr Bits enthält. Wenn der Wert implementierungsdefiniert ist, bedeutet dies, dass etwas im Bereich vonint8_t
produziert wird und das Programm fortgesetzt wird. Der C-Standard ermöglicht dabei Verhaltensweisen wie Sättigungsarithmetik (bei DSPs üblich) oder Wrap-Around (Mainstream-Architekturen).Der Compiler verwendet einen breiteren zugrunde liegenden Maschinentyp, wenn Werte kleiner ganzzahliger Typen wie
int8_t
oder bearbeitet werdenchar
. Wenn eine Arithmetik durchgeführt wird, können Ergebnisse, die außerhalb des Bereichs des Typs mit kleinen ganzen Zahlen liegen, in diesem breiteren Typ zuverlässig erfasst werden. Um das von außen sichtbare Verhalten beizubehalten, dass die Variable ein 8-Bit-Typ ist, muss das breitere Ergebnis in den 8-Bit-Bereich gekürzt werden. Dazu ist expliziter Code erforderlich, da die Maschinenspeicherorte (Register) breiter als 8 Bit sind und mit den größeren Werten zufrieden sind. Hier hat der Compiler es versäumt, den Wert zu normalisieren , und ihn einfach so übergeben,printf
wie er ist. Der Konvertierungsspezifizierer%i
inprintf
hat keine Ahnung, dass das Argument ursprünglich ausint8_t
Berechnungen stammt. es funktioniert nur mit einemint
Streit.quelle
Ich kann dies nicht in einen Kommentar einfügen, daher poste ich ihn als Antwort.
Aus irgendeinem sehr seltsamen Grund ist der
--
Bediener der Schuldige.Getestet habe ich den Code auf Ideone geschrieben und ersetzt
c--
mitc = c - 1
und die Werte blieben im Bereich [-128 ... 127]:Freaky ey? Ich weiß nicht viel darüber, was der Compiler mit Ausdrücken wie
i++
oder machti--
. Es ist wahrscheinlich, dass der Rückgabewert auf a erhöhtint
und weitergegeben wird. Das ist die einzige logische Schlussfolgerung, die ich ziehen kann, weil Sie tatsächlich Werte erhalten, die nicht in 8-Bit passen.quelle
c = c - 1
bedeutetc = (int8_t) ((int)c - 1
. Das Konvertieren eines Bereichs außerhalb des Bereichsint
inint8_t
hat ein definiertes Verhalten, aber ein implementierungsdefiniertes Ergebnis. Soll das nichtc--
auch die gleichen Konvertierungen durchführen?Ich vermute, dass die zugrunde liegende Hardware immer noch ein 32-Bit-Register verwendet, um dieses int8_t zu halten. Da die Spezifikation kein Überlaufverhalten vorschreibt, prüft die Implementierung nicht auf Überlauf und ermöglicht auch das Speichern größerer Werte.
Wenn Sie die lokale Variable markieren, während
volatile
Sie die Verwendung von Speicher erzwingen, und folglich die erwarteten Werte innerhalb des Bereichs erhalten.quelle
printf
nicht berücksichtigtsizeof
werden.Der Assembler-Code zeigt das Problem:
EBX sollte mit FF nach dem Dekrement anded werden, oder nur BL sollte mit dem Rest von EBX clear verwendet werden. Neugierig, dass es sub anstelle von dec verwendet. Die -45 ist geradezu mysteriös. Es ist die bitweise Inversion von 300 & 255 = 44. -45 = ~ 44. Irgendwo besteht eine Verbindung.
Es macht viel mehr Arbeit mit c = c - 1:
Es wird dann nur der niedrige Teil von RAX verwendet, sodass es auf -128 bis 127 beschränkt ist. Compiler-Optionen "-g -O2".
Ohne Optimierung wird der richtige Code erzeugt:
Es ist also ein Fehler im Optimierer.
quelle
Verwenden Sie
%hhd
anstelle von%i
! Sollte Ihr Problem lösen.Was Sie dort sehen, ist das Ergebnis von Compiler-Optimierungen in Kombination mit der Anweisung an printf, eine 32-Bit-Nummer zu drucken und dann eine (angeblich 8-Bit-) Nummer auf den Stapel zu schieben, der wirklich zeigergroß ist, da der Push-Opcode in x86 so funktioniert.
quelle
g++ -O3
. Ändern%i
zu%hhd
ändert nichts.Ich denke, dies geschieht durch Optimierung des Codes:
Der Compilator verwendet die
int32_t i
Variable sowohl füri
als auchc
. Deaktivieren Sie die Optimierung oder führen Sie eine direkte Besetzung durchprintf("c: %i\n", (int8_t)c--);
quelle
(int8_t)(c & 0x0000ffff)--
c
ist selbst definiert alsint8_t
, aber wenn es in Betrieb++
oder--
darüber istint8_t
, wird es implizit zuerst inint
und das Ergebnis der Operation konvertiert, stattdessen wird der interne Wert von c mit printf gedruckt, was zufällig istint
.Siehe den tatsächlichen Wert des
c
nach gesamter Schleife, vor allem nach dem letzten AbnahmeEs ist der richtige Wert, der dem Verhalten ähnelt
-128 + 1 = 127
c
beginnt mit der Verwendung desint
Größenspeichers, wird jedoch soint8_t
gedruckt, als würde er nur mit sich selbst gedruckt8 bits
. Verwendet alles,32 bits
wenn es als verwendet wirdint
[Compiler Bug]
quelle
Ich denke, es ist passiert, weil Ihre Schleife so lange dauert, bis int i 300 und c -300 wird. Und der letzte Wert ist weil
quelle