Pourquoi un débordement d'entier sur x86 avec GCC provoque-t-il une boucle infinie?

129

Le code suivant entre dans une boucle infinie sur 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;
}

Voici donc l'affaire: le dépassement d'entier signé est un comportement techniquement indéfini. Mais GCC sur x86 implémente l'arithmétique d'entiers en utilisant des instructions d'entiers x86 - qui s'enroulent en cas de débordement.

Par conséquent, je me serais attendu à ce qu'il se termine en cas de débordement - malgré le fait qu'il s'agisse d'un comportement non défini. Mais ce n'est clairement pas le cas. Et alors ... qu'est-ce que j'ai loupé?

J'ai compilé ceci en utilisant:

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

Sortie GCC:

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

... (infinite loop)

Avec les optimisations désactivées, il n'y a pas de boucle infinie et la sortie est correcte. Visual Studio compile également correctement ceci et donne le résultat suivant:

Sortie correcte:

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

Voici quelques autres variantes:

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

Voici toutes les informations de version pertinentes:

~/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$ 

La question est donc: s'agit -il d'un bogue dans GCC? Ou ai-je mal compris quelque chose sur la façon dont GCC gère l'arithmétique des entiers?

* Je marque également ce C, car je suppose que ce bogue se reproduira en C. (je ne l'ai pas encore vérifié.)

ÉDITER:

Voici l'assemblage de la boucle: (si je l'ai bien reconnue)

.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
Mysticial
la source
10
Ce serait beaucoup plus responsable si vous incluiez le code d'assemblage généré à partir de gcc -S.
Greg Hewgill
L'assemblage est étonnamment long. Dois-je toujours le modifier?
Mysticial
Juste les parties pertinentes pour votre boucle, s'il vous plaît.
Greg Hewgill
12
-1. vous dites qu'il s'agit à proprement parler d'un comportement indéfini et vous demandez s'il s'agit d'un comportement indéfini. donc ce n'est pas une vraie question pour moi.
Johannes Schaub - litb
8
@ JohannesSchaub-litb Merci pour vos commentaires. Probablement une mauvaise formulation de ma part. Je ferai de mon mieux pour clarifier de manière à gagner votre non-vote (et je modifierai la question en conséquence). En gros, je sais que c'est UB. Mais je sais aussi que GCC sur x86 utilise des instructions entières x86 - qui s'enroulent en cas de débordement. Par conséquent, je m'attendais à ce qu'il s'enroule malgré le fait qu'il soit UB. Cependant, ce n'est pas le cas et cela m'a dérouté. D'où la question.
Mysticial

Réponses:

178

Lorsque la norme dit que c'est un comportement non défini, cela le signifie . Tout peut arriver. "Tout" inclut "généralement des entiers, mais parfois des choses étranges se produisent".

Oui, sur les processeurs x86, les entiers enveloppent généralement la façon dont vous vous attendez. C'est l'une de ces exceptions. Le compilateur suppose que vous ne provoquerez pas de comportement indéfini et optimise le test de boucle. Si vous voulez vraiment un wraparound, passez -fwrapvà g++ou gcclors de la compilation; cela vous donne une sémantique de débordement bien définie (complément à deux), mais peut nuire aux performances.

bdonlan
la source
24
Oh wow. Je n'étais pas au courant -fwrapv. Merci de l'avoir signalé.
Mysticial
1
Existe-t-il une option d'avertissement qui tente de remarquer les boucles infinies accidentelles?
Jeff Burdges
5
J'ai trouvé les optimisations -Wunsafe-loop-optimisations mentionnées ici: stackoverflow.com/questions/2982507/…
Jeff Burdges
1
-1 "Oui, sur les processeurs x86, les entiers enveloppent généralement la façon dont vous vous attendez." c'est faux. mais c'est subtil. si je me souviens bien, il est possible de les piéger en cas de débordement, mais ce n'est pas ce dont nous parlons ici , et je ne l'ai jamais vu faire. à part cela, et sans tenir compte des opérations bcd x86 (représentation non autorisée en C ++), les opérations entières x86 sont toujours enveloppées, car elles sont complémentaires à deux. vous confondez l'optimisation g ++ défectueuse (ou extrêmement peu pratique et absurde) pour une propriété d'opérations entières x86.
Bravo et hth. - Alf
5
@ Cheersandhth.-Alf, par «sur les processeurs x86», je veux dire «lorsque vous développez pour des processeurs x86 en utilisant un compilateur C». Ai-je vraiment besoin de le préciser? Évidemment, tout mon discours sur les compilateurs et GCC n'est pas pertinent si vous développez en assembleur, auquel cas la sémantique du débordement d'entier est en effet très bien définie.
bdonlan le
18

C'est simple: un comportement non défini - en particulier lorsque l'optimisation ( -O2) est activée - signifie que tout peut arriver.

Votre code se comporte comme (vous) attendiez sans le -O2commutateur.

Cela fonctionne assez bien avec icl et tcc, mais vous ne pouvez pas vous fier à des trucs comme ça ...

Selon cela , l'optimisation gcc exploite en fait le débordement d'entiers signés. Cela signifierait que le "bogue" est par conception.

Dennis
la source
C'est un peu nul qu'un compilateur opte pour une boucle infinie de toutes choses pour un comportement indéfini.
Inverse
27
@Inverse: Je ne suis pas d'accord. Si vous avez codé quelque chose avec un comportement indéfini, priez pour une boucle infinie. Facilite la détection ...
Dennis
Je veux dire si le compilateur recherche activement UB, pourquoi ne pas insérer une exception au lieu d'essayer d'hyper-optimiser le code cassé?
Inverse
15
@Inverse: Le compilateur ne recherche pas activement un comportement indéfini , il suppose qu'il ne se produit pas. Cela permet au compilateur d'optimiser le code. Par exemple, au lieu de calculer for (j = i; j < i + 10; ++j) ++k;, il sera simplement défini k = 10, car cela sera toujours vrai si aucun débordement signé ne se produit.
Dennis
@Inverse Le compilateur n'a "opté" pour rien. Vous avez écrit la boucle dans votre code. Le compilateur ne l'a pas inventé.
Courses de légèreté en orbite
13

La chose importante à noter ici est que les programmes C ++ sont écrits pour la machine abstraite C ++ (qui est généralement émulée via des instructions matérielles). Le fait que vous compiliez pour x86 n'a aucun rapport avec le fait que cela a un comportement non défini.

Le compilateur est libre d'utiliser l'existence d'un comportement indéfini pour améliorer ses optimisations (en supprimant un conditionnel d'une boucle, comme dans cet exemple). Il n'y a pas de mappage garanti, ni même utile, entre les constructions de niveau C ++ et les constructions de code machine de niveau x86 en dehors de l'exigence que le code machine, une fois exécuté, produise le résultat demandé par la machine abstraite C ++.

Mankarse
la source
5
i += i;

// le débordement n'est pas défini.

Avec -fwrapv c'est correct. -fwrapv

lostyzd
la source
3

S'il vous plaît les gens, un comportement indéfini est exactement cela, indéfini . Cela signifie que tout peut arriver. En pratique (comme dans ce cas), le compilateur est libre de supposer qu'il ne le fera pasêtre appelé, et faites ce qui vous plaît si cela peut rendre le code plus rapide / plus petit. Ce qui se passe avec du code qui ne devrait pas s'exécuter est une supposition de personne. Cela dépendra du code environnant (en fonction de cela, le compilateur pourrait bien générer du code différent), des variables / constantes utilisées, des drapeaux du compilateur, ... Oh, et le compilateur pourrait être mis à jour et écrire le même code différemment, ou vous pourriez obtenir un autre compilateur avec une vue différente sur la génération de code. Ou simplement obtenir une machine différente, même un autre modèle de la même ligne d'architecture pourrait très bien avoir son propre comportement indéfini (recherchez des opcodes non définis, certains programmeurs entreprenants ont découvert que sur certaines de ces premières machines faisaient parfois des choses utiles ...) . Il y a pas"le compilateur donne un comportement défini sur un comportement non défini". Il y a des domaines qui sont définis par l'implémentation, et là, vous devriez pouvoir compter sur le comportement cohérent du compilateur.

vonbrand
la source
1
Oui, je sais très bien ce qu'est un comportement indéfini. Mais lorsque vous savez comment certains aspects du langage sont implémentés pour un environnement particulier, vous pouvez vous attendre à voir certains types d'UB et pas d'autres. Je sais que GCC implémente l'arithmétique entière en tant qu'arithmétique entière x86 - qui s'enroule en cas de débordement. J'ai donc assumé le comportement en tant que tel. Ce à quoi je ne m'attendais pas, c'était que GCC fasse autre chose comme bdonlan a répondu.
Mysticial
7
Faux. Ce qui se passe, c'est que GCC est autorisé à supposer que vous n'invoquerez pas de comportement indéfini, donc il émet simplement du code comme si cela ne pouvait pas arriver. Si cela ne se produit, les instructions pour faire ce que vous demandez avec pas un comportement non défini s'exécuté, et le résultat est tout le CPU fait. Ie, sur x86, fait des trucs x86. S'il s'agit d'un autre processeur, il pourrait faire quelque chose de totalement différent. Ou le compilateur pourrait être assez intelligent pour comprendre que vous appelez un comportement non défini et démarrer nethack (oui, certaines anciennes versions de gcc ont fait exactement cela).
vonbrand
4
Je pense que vous avez mal lu mon commentaire. J'ai dit: "Ce à quoi je ne m'attendais pas" - c'est pourquoi j'ai posé la question en premier lieu. Je ne m'attendais pas à ce que GCC fasse des tours.
Mysticial
1

Même si un compilateur devait spécifier que le débordement d'entier doit être considéré comme une forme "non critique" de comportement indéfini (tel que défini dans l'annexe L), le résultat d'un débordement d'entier devrait, en l'absence d'une plate-forme spécifique promesse d'un comportement plus spécifique, être au minimum considérée comme une "valeur partiellement indéterminée". Selon de telles règles, l'ajout de 1073741824 + 1073741824 pourrait arbitrairement être considéré comme donnant 2147483648 ou -2147483648 ou toute autre valeur qui était congruente à 2147483648 mod 4294967296, et les valeurs obtenues par des ajouts pourraient arbitrairement être considérées comme toute valeur qui était congruente à 0 mod 4294967296.

Les règles permettant aux débordements de produire des "valeurs partiellement indéterminées" seraient suffisamment bien définies pour respecter la lettre et l'esprit de l'annexe L, mais n'empêcheraient pas un compilateur de faire les mêmes inférences généralement utiles que celles qui seraient justifiées si les débordements n'étaient pas contraints Comportement indéfini. Cela empêcherait un compilateur de faire de fausses "optimisations" dont le principal effet dans de nombreux cas est d'exiger que les programmeurs ajoutent un encombrement supplémentaire au code dont le seul but est d'empêcher de telles "optimisations"; que ce soit une bonne chose ou non dépend de son point de vue.

supercat
la source