Come ho ottenuto un valore maggiore di 8 bit da un numero intero a 8 bit?


118

Ho rintracciato un bug estremamente brutto che si nasconde dietro questa piccola gemma. Sono consapevole del fatto che secondo le specifiche C ++, gli overflow con segno sono un comportamento non definito, ma solo quando si verifica l'overflow quando il valore viene esteso alla larghezza di bit sizeof(int). A quanto ho capito, l'incremento di a charnon dovrebbe mai essere un comportamento indefinito fintanto che sizeof(char) < sizeof(int). Ma questo non spiega come csi ottenga un valore impossibile . Come numero intero a 8 bit, come può ccontenere valori maggiori della sua larghezza di bit?

Codice

// 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;
}

Produzione

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   // <= ..........

Dai un'occhiata su ideone.


61
"Sono consapevole del fatto che secondo le specifiche C ++, gli overflow con segno non sono definiti." -- Giusto. Per essere precisi, non solo il valore è indefinito, ma anche il comportamento . Apparire per ottenere risultati fisicamente impossibili è una valida conseguenza.

@hvd Sono sicuro che qualcuno abbia una spiegazione di come le comuni implementazioni C ++ causano questo comportamento. Forse ha a che fare con l'allineamento o come printf()funziona la conversione?
rliu

Altri hanno affrontato il problema principale. Il mio commento è più generale e si riferisce agli approcci diagnostici. Credo che una parte del motivo per cui hai trovato questo enigma del genere sia la convinzione che fosse impossibile. Ovviamente, non è impossibile, quindi accettalo e guarda di nuovo
Tim X

@TimX - Ho osservato il comportamento e ovviamente ho tratto la conclusione che non era impossibile in quel senso. Il mio uso della parola si riferiva a un numero intero a 8 bit contenente un valore a 9 bit, il che è impossibile per definizione. Il fatto che ciò sia accaduto suggerisce che non viene trattato come un valore a 8 bit. Come altri hanno affrontato, ciò è dovuto a un bug del compilatore. L'unica apparente impossibilità qui è un valore di 9 bit in uno spazio di 8 bit, e questa apparente impossibilità è spiegata dal fatto che lo spazio è effettivamente "più grande" di quanto riportato.
Non firmato

L'ho appena testato sul mio mechine e il risultato è proprio quello che dovrebbe essere. 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 E il mio ambiente è: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Risposte:


111

Questo è un bug del compilatore.

Sebbene ottenere risultati impossibili per un comportamento indefinito sia una conseguenza valida, in realtà non esiste un comportamento indefinito nel codice. Quello che sta succedendo è che il compilatore pensa che il comportamento non sia definito e si ottimizza di conseguenza.

Se cè definito come int8_te int8_tpromuove a int, c--si suppone che esegua la sottrazione c - 1in intaritmetica e converta il risultato in int8_t. La sottrazione in intnon eccede e la conversione di valori integrali fuori intervallo in un altro tipo integrale è valida. Se il tipo di destinazione è firmato, il risultato è definito dall'implementazione, ma deve essere un valore valido per il tipo di destinazione. (E se il tipo di destinazione non è firmato, il risultato è ben definito, ma qui non si applica.)


Non lo descriverei come un "bug". Poiché l'overflow firmato causa un comportamento indefinito, il compilatore ha il diritto di presumere che non accadrà e ottimizza il ciclo per mantenere i valori intermedi cin un tipo più ampio. Presumibilmente, è quello che sta succedendo qui.
Mike Seymour

4
@MikeSeymour: L'unico overflow qui è sulla conversione (implicita). L'overflow sulla conversione con segno non ha un comportamento indefinito; produce semplicemente un risultato definito dall'implementazione (o solleva un segnale definito dall'implementazione, ma ciò non sembra accadere qui). La differenza di definizione tra operazioni aritmetiche e conversioni è strana, ma è così che lo definisce lo standard del linguaggio.
Keith Thompson

2
@KeithThompson Questo è qualcosa che differisce tra C e C ++: C consente un segnale definito dall'implementazione, C ++ no. C ++ dice semplicemente "Se il tipo di destinazione è firmato, il valore è invariato se può essere rappresentato nel tipo di destinazione (e nella larghezza del campo di bit); in caso contrario, il valore è definito dall'implementazione."

Come succede, non riesco a riprodurre lo strano comportamento su g ++ 4.8.0.
Daniel Landau

2
@DanielLandau Vedi il commento 38 in quel bug: "Risolto per 4.8.0." :)

15

Un compilatore può avere bug diversi dalle non conformità allo standard, perché ci sono altri requisiti. Un compilatore dovrebbe essere compatibile con altre versioni di se stesso. Ci si può anche aspettare che sia compatibile in qualche modo con altri compilatori e che si conformi anche ad alcune convinzioni sul comportamento che sono sostenute dalla maggior parte della sua base di utenti.

In questo caso, sembra essere un bug di conformità. L'espressione c--dovrebbe essere manipolata cin un modo simile a c = c - 1. Qui, il valore di ca destra viene promosso al tipo int, quindi ha luogo la sottrazione. Poiché cè nell'intervallo di int8_t, questa sottrazione non supererà, ma potrebbe produrre un valore che è fuori dall'intervallo di int8_t. Quando viene assegnato questo valore, viene eseguita una conversione al tipo in int8_tmodo che il risultato rientri c. Nel caso fuori intervallo, la conversione ha un valore definito dall'implementazione. Ma un valore fuori dall'intervallo di int8_tnon è un valore definito dall'implementazione valido. Un'implementazione non può "definire" che un tipo a 8 bit contenga improvvisamente 9 o più bit. Affinché il valore sia definito dall'implementazione significa che int8_tviene prodotto qualcosa nell'intervallo di e il programma continua. Lo standard C consente quindi comportamenti come l'aritmetica della saturazione (comune sui DSP) o il wrap-around (architetture tradizionali).

Il compilatore utilizza un tipo di macchina sottostante più ampio quando manipola valori di piccoli tipi interi come int8_to char. Quando si esegue l'aritmetica, i risultati che sono fuori gamma del tipo piccolo intero possono essere catturati in modo affidabile in questo tipo più ampio. Per preservare il comportamento visibile esternamente che la variabile è di tipo a 8 bit, il risultato più ampio deve essere troncato nell'intervallo di 8 bit. A tal fine è necessario un codice esplicito poiché le posizioni di memoria della macchina (registri) sono più larghe di 8 bit e soddisfano i valori più grandi. Qui, il compilatore ha trascurato di normalizzare il valore e lo ha semplicemente passato così printfcom'è. Lo specificatore di conversione %iin printfnon ha idea che l'argomento provenga originariamente da int8_tcalcoli; sta solo lavorando con un fileint discussione.


Questa è una chiara spiegazione.
David Healy,

Il compilatore produce un buon codice con l'ottimizzatore disattivato. Pertanto, le spiegazioni che utilizzano "regole" e "definizioni" non sono applicabili. È un bug nell'ottimizzatore.

14

Non riesco a inserire questo in un commento, quindi lo pubblico come risposta.

Per qualche strana ragione, l' --operatore sembra essere il colpevole.

Ho testato il codice pubblicato su Ideone e sostituito c--con c = c - 1ei valori sono rimasti all'interno del range [-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? Non so molto di cosa fa il compilatore a espressioni come i++o i--. È probabile che promuova il valore di ritorno a an inte lo trasmetta. Questa è l'unica conclusione logica che posso ottenere perché in realtà STAI ottenendo valori che non possono essere contenuti in 8 bit.


4
A causa delle promozioni integrali, c = c - 1significa c = (int8_t) ((int)c - 1. La conversione di un fuori intervallo intin int8_tha un comportamento definito ma un risultato definito dall'implementazione. In realtà, non c--dovrebbe eseguire anche le stesse conversioni?

12

Immagino che l'hardware sottostante utilizzi ancora un registro a 32 bit per contenere int8_t. Poiché la specifica non impone un comportamento per l'overflow, l'implementazione non controlla l'overflow e consente di memorizzare anche valori maggiori.


Se contrassegni la variabile locale come volatilecostringi a usare la memoria per essa e di conseguenza ottieni i valori attesi all'interno dell'intervallo.


1
Oh wow. Ho dimenticato che l'assembly compilato memorizzerà le variabili locali nei registri, se possibile. Questa sembra la risposta più probabile insieme al printfnon preoccuparsi sizeofdei valori di formato.
rliu

3
@roliu Esegui g ++ -O2 -S code.cpp e vedrai l'assembly. Inoltre, printf () è una funzione di argomento variabile, quindi gli argomenti il ​​cui rango è minore di un int verranno promossi a un int.
n.

@nos mi piacerebbe. Non sono stato in grado di installare un boot loader UEFI (rEFInd in particolare) per far funzionare archlinux sulla mia macchina, quindi non ho effettivamente codificato con gli strumenti GNU da molto tempo. Ci arriverò ... alla fine. Per ora è solo C # in VS e cerca di ricordare C / impara un po 'di C ++ :)
rliu

@rollu Eseguilo in una macchina virtuale, ad esempio VirtualBox
nos

@nos Non voglio far deragliare l'argomento, ma sì, potrei. Potrei anche installare Linux con un bootloader BIOS. Sono solo testardo e se non riesco a farlo funzionare con un bootloader UEFI, probabilmente non lo farò funzionare affatto: P.
rliu

11

Il codice assembler rivela il problema:

: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 dovrebbe essere sottoposto ad andamento con FF post decremento, o solo BL dovrebbe essere usato con il resto di EBX clear. Curioso che utilizzi sub invece di dec. Il -45 è decisamente misterioso. È l'inversione bit per bit di 300 e 255 = 44. -45 = ~ 44. C'è una connessione da qualche parte.

Esegue molto più lavoro usando 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

Quindi utilizza solo la parte bassa di RAX, quindi è limitato a -128 fino a 127. Opzioni del compilatore "-g -O2".

Senza ottimizzazione, produce il codice corretto:

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

Quindi è un bug nell'ottimizzatore.


4

Usa %hhdinvece di %i! Dovrebbe risolvere il tuo problema.

Quello che vedi è il risultato delle ottimizzazioni del compilatore combinate con il fatto che tu dica a printf di stampare un numero a 32 bit e quindi spingere un numero (presumibilmente a 8 bit) sullo stack, che è in realtà delle dimensioni di un puntatore, perché è così che funziona il push opcode in x86.


1
Sono in grado di riprodurre il comportamento originale sul mio sistema utilizzando g++ -O3. Il passaggio %ia %hhdnon cambia nulla.
Keith Thompson

3

Penso che ciò avvenga tramite l'ottimizzazione del codice:

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

Il compilatore usa la int32_t ivariabile sia per iche c. Disattiva l'ottimizzazione o esegui il cast diretto printf("c: %i\n", (int8_t)c--);


Quindi disattiva l'ottimizzazione. o fare qualcosa del genere:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

cè esso stesso definito come int8_t, ma quando si opera ++o --su int8_tè implicitamente convertito prima in inte il risultato dell'operazione invece il valore interno di c viene stampato con printf che sembra essere int.

Vedere il valore effettivo di cdopo l'intero ciclo, specialmente dopo l'ultimo decremento

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

è il valore corretto che assomiglia al comportamento -128 + 1 = 127

cinizia a utilizzare la intmemoria di formato ma viene stampato come int8_tquando viene stampato come se stesso utilizzando solo 8 bits. Utilizza tutto 32 bitsse usato comeint

[Bug del compilatore]


0

Penso che sia successo perché il tuo ciclo andrà avanti fino a quando l'int i diventerà 300 ec diventerà -300. E l'ultimo valore è perché

printf("c: %i\n", c);

"c" è un valore a 8 bit, quindi è impossibile che contenga un numero grande come -300.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.