Wie habe ich aus einer 8-Bit-Ganzzahl einen Wert erhalten, der größer als 8 Bit ist?

118

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 charniemals undefiniertes Verhalten sein, solange sizeof(char) < sizeof(int). Das erklärt aber nicht, wie cman einen unmöglichen Wert bekommt . Wie kann eine 8-Bit-Ganzzahl cWerte 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.

Ohne Vorzeichen
quelle
61
"Mir ist bekannt, dass gemäß der C ++ - Spezifikation signierte Überläufe undefiniert sind." -- Richtig. Um genau zu sein, ist nicht nur der Wert undefiniert, sondern auch das Verhalten . Das Erscheinen physikalisch unmöglicher Ergebnisse ist eine gültige Konsequenz.
@hvd Ich bin sicher, jemand hat eine Erklärung dafür, wie häufig C ++ - Implementierungen dieses Verhalten verursachen. Vielleicht hat es mit Ausrichtung zu tun oder wie printf()funktioniert Konvertierung?
Rliu
Andere haben das Hauptproblem angesprochen. Mein Kommentar ist allgemeiner und bezieht sich auf diagnostische Ansätze. Ich glaube, ein Teil der Gründe, warum Sie dieses Rätsel gelöst haben, ist der unerklärliche Glaube, dass es unmöglich war. Offensichtlich ist es nicht unmöglich, also akzeptieren Sie das und schauen Sie noch einmal
Tim X
@ TimX - Ich habe das Verhalten beobachtet und bin offensichtlich zu dem Schluss gekommen, dass es in diesem Sinne nicht unmöglich ist. Meine Verwendung des Wortes bezog sich auf eine 8-Bit-Ganzzahl mit einem 9-Bit-Wert, was per Definition unmöglich ist. Die Tatsache, dass dies passiert ist, deutet darauf hin, dass es nicht als 8-Bit-Wert behandelt wird. Wie andere bereits angesprochen haben, liegt dies an einem Compiler-Fehler. Die einzige scheinbare Unmöglichkeit ist hier ein 9-Bit-Wert in einem 8-Bit-Raum, und diese offensichtliche Unmöglichkeit wird dadurch erklärt, dass der Raum tatsächlich "größer" ist als angegeben.
Unsigned
Ich habe es gerade an meiner Maschine getestet und das Ergebnis ist genau das, was es sein sollte. c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 Und meine Umgebung ist: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Antworten:

111

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 cist wie folgt definiert int8_t, und int8_tfördert auf int, dann c--sollte die Subtraktion auszuführen c - 1in intArithmetik und das Ergebnis zurück zu konvertieren int8_t. Die Subtraktion in intlä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
Ich würde es nicht als "Bug" beschreiben. Da ein signierter Überlauf ein undefiniertes Verhalten verursacht, kann der Compiler durchaus davon ausgehen, dass dies nicht der Fall ist, und die Schleife optimieren, um Zwischenwerte ceines breiteren Typs beizubehalten. Vermutlich passiert das hier.
Mike Seymour
4
@ MikeSeymour: Der einzige Überlauf hier ist die (implizite) Konvertierung. Der Überlauf bei der signierten Konvertierung weist kein undefiniertes Verhalten auf. es liefert lediglich ein implementierungsdefiniertes Ergebnis (oder löst ein implementierungsdefiniertes Signal aus, aber das scheint hier nicht zu passieren). Der Unterschied in der Definiertheit zwischen arithmetischen Operationen und Konvertierungen ist merkwürdig, aber so definiert ihn der Sprachstandard.
Keith Thompson
2
@KeithThompson Das ist etwas, das sich zwischen C und C ++ unterscheidet: C ermöglicht ein implementierungsdefiniertes Signal, C ++ nicht. C ++ sagt nur "Wenn der Zieltyp signiert ist, bleibt der Wert unverändert, wenn er im Zieltyp (und in der Bitfeldbreite) dargestellt werden kann; andernfalls ist der Wert implementierungsdefiniert."
Zufällig kann ich das seltsame Verhalten unter g ++ 4.8.0 nicht reproduzieren.
Daniel Landau
2
@DanielLandau Siehe Kommentar 38 in diesem Fehler: "Behoben für 4.8.0." :)
15

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--sollte cauf ähnliche Weise wie manipuliert werden c = c - 1. Hier wird der Wert von crechts zum Typ heraufgestuft int, und dann findet die Subtraktion statt. Da diese Subtraktion cim Bereich von liegt int8_t, läuft sie nicht über, kann jedoch einen Wert erzeugen, der außerhalb des Bereichs von liegt int8_t. Wenn dieser Wert zugewiesen wird, erfolgt eine Konvertierung zurück in den Typ, int8_tsodass das Ergebnis wieder in den Typ passt c. Im Fall außerhalb des Bereichs hat die Konvertierung einen implementierungsdefinierten Wert. Ein Wert außerhalb des Bereichs von int8_tist 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 von int8_tproduziert 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_toder bearbeitet werden char. 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, printfwie er ist. Der Konvertierungsspezifizierer %iin printfhat keine Ahnung, dass das Argument ursprünglich aus int8_tBerechnungen stammt. es funktioniert nur mit einemint Streit.

Kaz
quelle
Dies ist eine klare Erklärung.
David Healy
Der Compiler erzeugt guten Code bei deaktiviertem Optimierer. Erklärungen mit "Regeln" und "Definitionen" sind daher nicht anwendbar. Es ist ein Fehler im Optimierer.
14

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--mit c = c - 1und die Werte blieben im Bereich [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

Freaky ey? Ich weiß nicht viel darüber, was der Compiler mit Ausdrücken wie i++oder macht i--. Es ist wahrscheinlich, dass der Rückgabewert auf a erhöht intund weitergegeben wird. Das ist die einzige logische Schlussfolgerung, die ich ziehen kann, weil Sie tatsächlich Werte erhalten, die nicht in 8-Bit passen.

user123
quelle
4
Wegen der integralen Werbeaktionen c = c - 1bedeutet c = (int8_t) ((int)c - 1. Das Konvertieren eines Bereichs außerhalb des Bereichs intin int8_that ein definiertes Verhalten, aber ein implementierungsdefiniertes Ergebnis. Soll das nicht c--auch die gleichen Konvertierungen durchführen?
12

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 volatileSie die Verwendung von Speicher erzwingen, und folglich die erwarteten Werte innerhalb des Bereichs erhalten.

Zoltán
quelle
1
Oh wow. Ich habe vergessen, dass die kompilierte Assembly lokale Variablen in Registern speichert, wenn dies möglich ist. Dies scheint die wahrscheinlichste Antwort zu sein, da die Formatwerte printfnicht berücksichtigt sizeofwerden.
Rliu
3
@roliu Führen Sie g ++ -O2 -S code.cpp aus, und Sie sehen die Assembly. Darüber hinaus ist printf () eine variable Argumentfunktion, sodass Argumente, deren Rang kleiner als ein int ist, zu einem int heraufgestuft werden.
Nr.
@nos würde ich gerne. Ich konnte keinen UEFI-Bootloader (insbesondere rEFInd) installieren, um Archlinux auf meinem Computer zum Laufen zu bringen, daher habe ich lange Zeit nicht mehr mit GNU-Tools codiert. Ich werde es schaffen ... irgendwann. Im
Moment ist
@rollu Führen Sie es in einer virtuellen Maschine aus, z. B. VirtualBox
Nr.
@nos Ich möchte das Thema nicht entgleisen, aber ja, ich könnte. Ich könnte Linux auch einfach mit einem BIOS-Bootloader installieren. Ich bin nur stur und wenn ich es mit einem UEFI-Bootloader nicht zum Laufen bringen kann, werde ich es wahrscheinlich überhaupt nicht zum Laufen bringen: P.
Rliu
11

Der Assembler-Code zeigt das Problem:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

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:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

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:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

Es ist also ein Fehler im Optimierer.


quelle
4

Verwenden Sie %hhdanstelle 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.

Zotta
quelle
1
Ich kann das ursprüngliche Verhalten auf meinem System mit reproduzieren g++ -O3. Ändern %izu %hhdändert nichts.
Keith Thompson
3

Ich denke, dies geschieht durch Optimierung des Codes:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

Der Compilator verwendet die int32_t iVariable sowohl für ials auch c. Deaktivieren Sie die Optimierung oder führen Sie eine direkte Besetzung durch printf("c: %i\n", (int8_t)c--);

Wsewolod
quelle
Schalten Sie dann die Optimierung aus. oder machen Sie so etwas:(int8_t)(c & 0x0000ffff)--
Vsevolod
1

cist selbst definiert als int8_t, aber wenn es in Betrieb ++oder --darüber ist int8_t, wird es implizit zuerst in intund das Ergebnis der Operation konvertiert, stattdessen wird der interne Wert von c mit printf gedruckt, was zufällig ist int.

Siehe den tatsächlichen Wert des cnach gesamter Schleife, vor allem nach dem letzten Abnahme

-301 + 256 = -45 (since it revolved entire 8 bit range once)

Es ist der richtige Wert, der dem Verhalten ähnelt -128 + 1 = 127

cbeginnt mit der Verwendung des intGrößenspeichers, wird jedoch so int8_tgedruckt, als würde er nur mit sich selbst gedruckt 8 bits. Verwendet alles, 32 bitswenn es als verwendet wirdint

[Compiler Bug]

Izhar Aazmi
quelle
0

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

printf("c: %i\n", c);
r.mirzojonov
quelle
'c' ist ein 8-Bit-Wert, daher ist es unmöglich, jemals eine Zahl von bis zu -300 zu halten.