Perché l'overflow degli interi su x86 con GCC provoca un loop infinito?


129

Il seguente codice entra in un ciclo infinito su GCC:

#include <iostream>
using namespace std;

int main(){
    int i = 0x10000000;

    int c = 0;
    do{
        c++;
        i += i;
        cout << i << endl;
    }while (i > 0);

    cout << c << endl;
    return 0;
}

Quindi ecco l'affare: l' overflow di interi con segno è un comportamento tecnicamente indefinito. Ma GCC su x86 implementa l'aritmetica dei numeri interi usando le istruzioni sui numeri interi x86 - che si concludono con l'overflow.

Pertanto, mi sarei aspettato che si avvolgesse in un trabocco, nonostante si tratti di un comportamento indefinito. Ma chiaramente non è così. Quindi cosa mi sono perso?

Ho compilato questo usando:

~/Desktop$ g++ main.cpp -O2

Uscita GCC:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0

... (infinite loop)

Con le ottimizzazioni disabilitate, non esiste un loop infinito e l'output è corretto. Visual Studio inoltre compila correttamente questo e dà il seguente risultato:

Uscita corretta:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

Ecco alcune altre varianti:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

Ecco tutte le informazioni sulla versione rilevanti:

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..

...

Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4) 
~/Desktop$ 

Quindi la domanda è: è un bug in GCC? O ho frainteso qualcosa su come GCC gestisce l'aritmetica dei numeri interi?

* Sto anche taggando questa C, perché presumo che questo bug si riproduca in C. (Non l'ho ancora verificato.)

MODIFICARE:

Ecco l'assemblaggio del loop: (se l'ho riconosciuto correttamente)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5

10
Ciò sarebbe molto più rispondente se si includesse il codice assembly generato da gcc -S.
Greg Hewgill,

L'assemblea è sorprendentemente lunga. Devo ancora modificarlo in?
Mistico il

Solo le parti rilevanti per il tuo loop, per favore.
Greg Hewgill,

12
-1. dici che si tratta di un comportamento indefinito e chiedi se si tratta di un comportamento indefinito. quindi questa non è una vera domanda per me.
Johannes Schaub - litb

8
@ JohannesSchaub-litb Grazie per aver commentato. Probabilmente una cattiva espressione da parte mia. Farò del mio meglio per chiarire in modo da guadagnare il tuo voto negativo (e modificherò la domanda di conseguenza). Fondamentalmente, so che è UB. Ma so anche che GCC su x86 usa istruzioni per numeri interi x86 - che si concludono con un overflow. Pertanto, mi aspettavo che avvolgesse nonostante fosse UB. Tuttavia, non lo ha fatto e questo mi ha confuso. Da qui la domanda.
Mistico

Risposte:


178

Quando lo standard dice che è un comportamento indefinito, significa che . Tutto può succedere. "Qualsiasi cosa" include "di solito gli interi vanno in giro, ma a volte accadono cose strane".

Sì, sulle CPU x86, gli interi di solito si avvolgono come previsto. Questa è una di quelle eccezioni. Il compilatore presuppone che non causerai un comportamento indefinito e ottimizza il test del ciclo. Se vuoi davvero avvolgente, passa -fwrapva g++o gccdurante la compilazione; questo ti dà una semantica di overflow ben definita (complemento a due), ma può compromettere le prestazioni.


24
Oh wow Non ne ero a conoscenza -fwrapv. Grazie per averlo segnalato.
Mistico il

1
Esiste un'opzione di avviso che tenta di notare loop infiniti accidentali?
Jeff Burdges,

5
Ho trovato -Wunsafe-loop-ottimizzazioni già detto qui: stackoverflow.com/questions/2982507/...
Jeff Burdges

1
-1 "Sì, sulle CPU x86, gli interi di solito si avvolgono come previsto." è sbagliato. ma è sottile. come ricordo è possibile farli intrappolare in overflow, ma non è di questo che stiamo parlando qui , e non l'ho mai visto fatto. a parte questo, e trascurando le operazioni x86 bcd (rappresentazione non consentita in C ++) le operazioni su interi x86 vanno sempre a capo, perché sono il complemento a due. stai confondendo g ++ ottimizzazione difettosa (o estremamente poco pratica e senza senso) per una proprietà di op interi x86.
Saluti e hth. - Alf,

5
@ Cheersandhth.-Alf, di 'su CPU x86' Intendo 'quando stai sviluppando per CPU x86 usando un compilatore C.' Devo davvero precisarlo? Ovviamente tutti i miei discorsi sui compilatori e GCC sono irrilevanti se si sta sviluppando in assemblatore, nel qual caso la semantica per l'overflow di numeri interi è davvero molto ben definita.
bdonlan,

18

È semplice: un comportamento indefinito, specialmente con l'ottimizzazione ( -O2) attivata, significa che può succedere di tutto.

Il tuo codice si comporta come (tu) previsto senza lo -O2switch.

A proposito, funziona abbastanza bene con icl e tcc, ma non puoi fare affidamento su cose del genere ...

In base a ciò , l'ottimizzazione di gcc effettivamente sfrutta l'overflow di numeri interi con segno. Ciò significherebbe che il "bug" è di progettazione.


Fa schifo che un compilatore opti per un ciclo infinito di tutte le cose per un comportamento indefinito.
Inverso

27
@Inverso: non sono d'accordo. Se hai codificato qualcosa con un comportamento indefinito, prega per un ciclo infinito. Rende più facile il rilevamento ...
Dennis

Voglio dire se il compilatore sta cercando attivamente UB, perché non inserire un'eccezione invece di provare a ottimizzare i codici rotti?
Inverso,

15
@Inverso: il compilatore non è attivamente alla ricerca di comportamenti indefiniti , presuppone che non si verifichi. Ciò consente al compilatore di ottimizzare il codice. Ad esempio, anziché il calcolo for (j = i; j < i + 10; ++j) ++k;, verrà impostato k = 10, poiché ciò sarà sempre vero se non si verifica un overflow firmato.
Dennis,

@Inverso Il compilatore non ha "optato" per nulla. Hai scritto il ciclo nel tuo codice. Il compilatore non l'ha inventato.
Razze di leggerezza in orbita

13

La cosa importante da notare qui è che i programmi C ++ sono scritti per la macchina astratta C ++ (che di solito viene emulata attraverso le istruzioni hardware). Il fatto che si stia compilando per x86 è del tutto irrilevante per il fatto che questo ha un comportamento indefinito.

Il compilatore è libero di utilizzare l'esistenza di un comportamento indefinito per migliorare le sue ottimizzazioni (rimuovendo un condizionale da un ciclo, come in questo esempio). Non esiste alcuna mappatura garantita o utile tra costrutti di livello C ++ e costrutti di codice macchina di livello x86 a parte il requisito che il codice macchina, una volta eseguito, produrrà il risultato richiesto dalla macchina astratta C ++.



3

Per favore, gente, il comportamento indefinito è esattamente questo, indefinito . Significa che potrebbe succedere di tutto. In pratica (come in questo caso), il compilatore è libero di presumere che non lo faràessere invocato e fare tutto ciò che gli piace se ciò potrebbe rendere il codice più veloce / più piccolo. Quello che succede con il codice che non dovrebbe essere eseguito è l'ipotesi di nessuno. Dipenderà dal codice circostante (a seconda di ciò, il compilatore potrebbe anche generare codice diverso), variabili / costanti utilizzate, flag del compilatore, ... Oh, e il compilatore potrebbe essere aggiornato e scrivere lo stesso codice in modo diverso, oppure potresti ottenere un altro compilatore con una visione diversa sulla generazione del codice. O semplicemente ottenere una macchina diversa, anche un altro modello nella stessa linea di architettura potrebbe benissimo avere un proprio comportamento indefinito (cercare codici operativi indefiniti, alcuni programmatori intraprendenti hanno scoperto che su alcune di quelle prime macchine a volte facevano cose utili ...) . Non c'è"il compilatore fornisce un comportamento definito su un comportamento indefinito". Esistono aree definite dall'implementazione e dovresti poter contare sul comportamento costante del compilatore.


1
Sì, so benissimo cos'è un comportamento indefinito. Ma quando sai come vengono implementati alcuni aspetti del linguaggio per un determinato ambiente, puoi aspettarti di vedere alcuni tipi di UB e non altri. So che GCC implementa l'aritmetica dei numeri interi come l'aritmetica dei numeri interi x86, che si avvolge in un overflow. Quindi ho assunto il comportamento in quanto tale. Quello che non mi aspettavo era che GCC facesse qualcos'altro come ha risposto bdonlan.
Mistico

7
Sbagliato. Quello che succede è che GCC è autorizzato ad assumere che non invocherai comportamenti indefiniti, quindi emette semplicemente codice come se non potesse accadere. Se non accade, le istruzioni per fare quello che chiedi con nessun comportamento indefinito vengono eseguiti, e il risultato è ciò che fa la CPU. Cioè, su x86 è roba x86. Se si tratta di un altro processore, potrebbe fare qualcosa di completamente diverso. Oppure il compilatore potrebbe essere abbastanza intelligente da capire che stai invocando un comportamento indefinito e avviare nethack (sì, alcune versioni antiche di gcc lo hanno fatto esattamente).
vonbrand,

4
Credo che tu abbia letto male il mio commento. Ho detto: "Quello che non mi aspettavo" - ecco perché ho posto la domanda in primo luogo. Io non mi aspettavo GCC per tirare qualsiasi trucchi.
Mistico

1

Anche se un compilatore dovesse specificare che l'overflow di numeri interi deve essere considerato una forma "non critica" di comportamento indefinito (come definito nell'allegato L), il risultato di un overflow di numeri interi dovrebbe, in assenza di una specifica piattaforma promessa di un comportamento più specifico, essere almeno considerato un "valore parzialmente indeterminato". In base a tali regole, l'aggiunta di 1073741824 + 1073741824 potrebbe essere arbitrariamente considerata come un rendimento di 2147483648 o -2147483648 o qualsiasi altro valore congruente con 2147483648 mod 4294967296, e i valori ottenuti mediante aggiunte potrebbero essere arbitrariamente considerati qualsiasi valore congruente con 0 mod 4294967296.

Le regole che consentono all'overflow di produrre "valori parzialmente indeterminati" sarebbero sufficientemente ben definite per rispettare la lettera e lo spirito dell'allegato L, ma non impedirebbero a un compilatore di fare le stesse deduzioni generalmente utili che sarebbero giustificate se gli overflow non fossero vincolati Comportamento indefinito. Impedirebbe a un compilatore di effettuare alcune "ottimizzazioni" false il cui effetto principale è in molti casi richiedere ai programmatori di aggiungere ulteriore ingombro al codice il cui unico scopo è impedire tali "ottimizzazioni"; se ciò sarebbe positivo o meno dipende dal proprio punto di vista.

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.