Comment obtenir une valeur supérieure à 8 bits à partir d'un entier de 8 bits?

118

J'ai retrouvé un insecte extrêmement méchant qui se cachait derrière ce petit bijou. Je suis conscient que selon la spécification C ++, les débordements signés sont un comportement indéfini, mais uniquement lorsque le débordement se produit lorsque la valeur est étendue à la largeur en bits sizeof(int). Si je comprends bien, incrémenter un charne devrait jamais être un comportement indéfini aussi longtemps que sizeof(char) < sizeof(int). Mais cela n'explique pas comment cobtenir une valeur impossible . En tant qu'entier 8 bits, comment peut ccontenir des valeurs supérieures à sa largeur en bits?

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

Production

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

Découvrez-le sur ideone.

Non signé
la source
61
"Je suis conscient que selon la spécification C ++, les débordements signés ne sont pas définis." -- Droite. Pour être précis, non seulement la valeur n'est pas définie, mais le comportement l' est. Apparaître pour obtenir des résultats physiquement impossibles est une conséquence valable.
@hvd Je suis sûr que quelqu'un a une explication sur la façon dont les implémentations courantes de C ++ provoquent ce comportement. Peut-être que cela a à voir avec l'alignement ou comment se printf()passe la conversion?
rliu
D'autres ont abordé le problème principal. Mon commentaire est plus général et concerne les approches diagnostiques. Je crois qu'une partie de la raison pour laquelle vous avez trouvé ce casse-tête est la croyance sous-jacente qu'il était impossible. De toute évidence, ce n'est pas impossible, alors acceptez cela et regardez à nouveau
Tim X
@TimX - J'ai observé le comportement et j'ai évidemment tiré la conclusion que ce n'était pas impossible dans ce sens. Mon utilisation du mot faisait référence à un entier de 8 bits contenant une valeur de 9 bits, ce qui est une impossibilité par définition. Le fait que cela se soit produit suggère qu'il n'est pas traité comme une valeur 8 bits. Comme d'autres l'ont abordé, cela est dû à un bogue du compilateur. La seule impossibilité apparente ici est une valeur de 9 bits dans un espace de 8 bits, et cette impossibilité apparente s'explique par le fait que l'espace est en fait "plus grand" que ce qui a été rapporté.
Non signé du
Je viens de le tester sur ma mechine, et le résultat est exactement ce qu'il devrait être. 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 Et mon environnement est: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Réponses:

111

Ceci est un bogue du compilateur.

Bien qu'obtenir des résultats impossibles pour un comportement non défini soit une conséquence valide, il n'y a en fait aucun comportement non défini dans votre code. Ce qui se passe, c'est que le compilateur pense que le comportement n'est pas défini et optimise en conséquence.

Si cest défini comme int8_t, et int8_tpromeut vers int, alors c--est censé effectuer la soustraction c - 1en intarithmétique et reconvertir le résultat en int8_t. La soustraction dans intne déborde pas et la conversion des valeurs intégrales hors plage en un autre type intégral est valide. Si le type de destination est signé, le résultat est défini par l'implémentation, mais il doit être une valeur valide pour le type de destination. (Et si le type de destination n'est pas signé, le résultat est bien défini, mais cela ne s'applique pas ici.)


la source
Je ne le décrirais pas comme un "bug". Puisque le débordement signé provoque un comportement indéfini, le compilateur est parfaitement en droit de supposer que cela ne se produira pas et d'optimiser la boucle pour conserver les valeurs intermédiaires d' cun type plus large. Vraisemblablement, c'est ce qui se passe ici.
Mike Seymour
4
@MikeSeymour: Le seul débordement ici concerne la conversion (implicite). Le débordement sur la conversion signée n'a pas de comportement indéfini; il donne simplement un résultat défini par l'implémentation (ou lève un signal défini par l'implémentation, mais cela ne semble pas se produire ici). La différence de définition entre les opérations arithmétiques et les conversions est étrange, mais c'est ainsi que la norme de langage la définit.
Keith Thompson
2
@KeithThompson C'est quelque chose qui diffère entre C et C ++: C permet un signal défini par l'implémentation, C ++ ne le fait pas. C ++ dit simplement "Si le type de destination est signé, la valeur est inchangée si elle peut être représentée dans le type de destination (et la largeur du champ de bits); sinon, la valeur est définie par l'implémentation."
En l'occurrence, je ne peux pas reproduire le comportement étrange sur g ++ 4.8.0.
Daniel Landau
2
@DanielLandau Voir le commentaire 38 dans ce bogue: "Corrigé pour 4.8.0." :)
15

Un compilateur peut avoir des bogues autres que des non-conformités à la norme, car il existe d'autres exigences. Un compilateur doit être compatible avec d'autres versions de lui-même. On peut également s'attendre à ce qu'il soit compatible à certains égards avec d'autres compilateurs, et aussi à se conformer à certaines croyances sur le comportement qui sont détenues par la majorité de sa base d'utilisateurs.

Dans ce cas, cela semble être un bogue de conformité. L'expression c--doit manipuler cd'une manière similaire à c = c - 1. Ici, la valeur de cà droite est promue en type int, puis la soustraction a lieu. Comme se csitue dans la plage de int8_t, cette soustraction ne débordera pas, mais elle peut produire une valeur hors de la plage de int8_t. Lorsque cette valeur est affectée, une conversion a lieu de nouveau au type int8_tafin que le résultat rentre dans c. Dans le cas hors limites, la conversion a une valeur définie par l'implémentation. Mais une valeur hors de la plage de int8_tn'est pas une valeur définie par l'implémentation valide. Une implémentation ne peut pas "définir" qu'un type 8 bits contient soudainement 9 bits ou plus. Pour que la valeur soit définie par la mise en œuvre, cela signifie que quelque chose de l'ordre de int8_test produit et le programme se poursuit. La norme C permet ainsi des comportements tels que l'arithmétique de saturation (commune sur les DSP) ou le bouclage (architectures grand public).

Le compilateur utilise un type de machine sous-jacent plus large lors de la manipulation de valeurs de petits types entiers tels que int8_tou char. Lorsque l'arithmétique est effectuée, les résultats qui sont hors de portée du type petit entier peuvent être capturés de manière fiable dans ce type plus large. Pour conserver le comportement visible de l'extérieur selon lequel la variable est de type 8 bits, le résultat plus large doit être tronqué dans la plage de 8 bits. Un code explicite est nécessaire pour ce faire, car les emplacements de stockage de la machine (registres) sont plus larges que 8 bits et satisfaits des valeurs plus élevées. Ici, le compilateur a négligé de normaliser la valeur et l'a simplement passée printftelle quelle . Le spécificateur de conversion %idans printfn'a aucune idée que l'argument provenait à l'origine de int8_tcalculs; il s'agit simplement de travailler avec unint argument.

Kaz
la source
C'est une explication lucide.
David Healy
Le compilateur produit du bon code avec l'optimiseur désactivé. Par conséquent, les explications utilisant des «règles» et des «définitions» ne sont pas applicables. C'est un bug dans l'optimiseur.
14

Je ne peux pas intégrer cela dans un commentaire, donc je le poste comme réponse.

Pour une raison très étrange, l' --opérateur se trouve être le coupable.

J'ai testé le code affiché sur Ideone et remplacé c--par c = c - 1et les valeurs sont restées dans la plage [-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? Je ne sais pas grand-chose de ce que le compilateur fait aux expressions comme i++ou i--. Cela promeut probablement la valeur de retour à an intet la transmet. C'est la seule conclusion logique que je puisse trouver car vous obtenez en fait des valeurs qui ne peuvent pas tenir dans 8 bits.

user123
la source
4
En raison des promotions intégrales, des c = c - 1moyens c = (int8_t) ((int)c - 1. La conversion d'un hors plage inten int8_ta un comportement défini mais un résultat défini par l'implémentation. En fait, n'est-il pas c--censé effectuer ces mêmes conversions?
12

Je suppose que le matériel sous-jacent utilise toujours un registre 32 bits pour contenir ce int8_t. Étant donné que la spécification n'impose pas de comportement pour le débordement, l'implémentation ne vérifie pas le débordement et permet également de stocker des valeurs plus importantes.


Si vous marquez la variable locale comme volatilevous obligez à utiliser la mémoire pour elle et par conséquent obtenir les valeurs attendues dans la plage.

Zoltán
la source
1
Oh wow. J'ai oublié que l'assemblage compilé stockera les variables locales dans des registres s'il le peut. Cela semble être la réponse la plus probable et printfne pas se soucier sizeofdes valeurs de format.
rliu
3
@roliu Exécutez g ++ -O2 -S code.cpp, et vous verrez l'assembly. De plus, printf () est une fonction d'argument variable, donc les arguments dont le rang est inférieur à un int seront promus en int.
nos
@nos je voudrais. Je n'ai pas pu installer un chargeur de démarrage UEFI (rEFInd en particulier) pour faire fonctionner archlinux sur ma machine, donc je n'ai pas réellement codé avec les outils GNU depuis longtemps. J'y arriverai ... éventuellement. Pour l'instant, c'est juste C # dans VS et essayer de se souvenir de C / apprendre du C ++ :)
rliu
@rollu Exécutez-le dans une machine virtuelle, par exemple VirtualBox
nos
@nos Je ne veux pas faire dérailler le sujet, mais oui, je pourrais. Je pourrais aussi simplement installer Linux avec un chargeur de démarrage BIOS. Je suis juste têtu et si je ne peux pas le faire fonctionner avec un chargeur de démarrage UEFI, je ne le ferai probablement pas fonctionner du tout: P.
rliu
11

Le code assembleur révèle le problème:

: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 doit être anded avec FF post décrément, ou seul BL doit être utilisé avec le reste d'EBX clear. Curieux qu'il utilise sous au lieu de déc. Le -45 est complètement mystérieux. C'est l'inversion bit à bit de 300 & 255 = 44. -45 = ~ 44. Il y a une connexion quelque part.

Il nécessite beaucoup plus de travail en utilisant 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

Il n'utilise alors que la partie basse de RAX, donc il est limité à -128 à 127. Options du compilateur "-g -O2".

Sans optimisation, il produit un code correct:

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

C'est donc un bug dans l'optimiseur.


la source
4

Utilisez %hhdplutôt que %i! Devrait résoudre votre problème.

Ce que vous voyez là est le résultat des optimisations du compilateur combinées avec vous disant à printf d'imprimer un nombre 32 bits, puis de pousser un nombre (supposé 8 bits) sur la pile, qui est vraiment de la taille d'un pointeur, car c'est ainsi que fonctionne l'opcode push dans x86.

Zotta
la source
1
Je suis capable de reproduire le comportement d'origine sur mon système en utilisant g++ -O3. Changer %ipour %hhdne change rien.
Keith Thompson
3

Je pense que cela se fait par optimisation du code:

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

Le compilateur utilise la int32_t ivariable à la fois pour iet c. Désactivez l'optimisation ou effectuez une diffusion directe printf("c: %i\n", (int8_t)c--);

Vsevolod
la source
Désactivez ensuite l'optimisation. ou faites quelque chose comme ça:(int8_t)(c & 0x0000ffff)--
Vsevolod
1

cest lui-même défini comme int8_t, mais lors de l'opération ++ou au- --dessus, int8_til est implicitement converti en premier intet le résultat de l'opération à la place, la valeur interne de c est imprimée avec printf qui se trouve être int.

Voir la valeur réelle de caprès boucle entière, surtout après la dernière décrément

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

c'est la valeur correcte qui ressemble au comportement -128 + 1 = 127

ccommence à utiliser la intmémoire de taille mais imprimé comme int8_tlors de l'impression en tant que lui-même en utilisant uniquement 8 bits. Utilise tout 32 bitslorsqu'il est utilisé commeint

[Bogue du compilateur]

Izhar Aazmi
la source
0

Je pense que c'est arrivé parce que votre boucle ira jusqu'à ce que l'int i devienne 300 et c devienne -300. Et la dernière valeur est que

printf("c: %i\n", c);
r.mirzojonov
la source
«c» est une valeur de 8 bits, il est donc impossible qu'il contienne un nombre aussi grand que -300.