web-dev-qa-db-fra.com

Comment ai-je obtenu une valeur supérieure à 8 bits à partir d'un entier 8 bits?

J'ai retrouvé un bug extrêmement méchant caché derrière ce petit bijou. Je suis conscient que selon la spécification C++, les débordements signés sont un comportement non défini, mais uniquement lorsque le débordement se produit lorsque la valeur est étendue à la largeur de bit sizeof(int). Si je comprends bien, l'incrémentation d'un char ne devrait jamais être un comportement indéfini tant que sizeof(char) < sizeof(int). Mais cela n'explique pas comment c obtient une valeur impossible . En tant qu'entier 8 bits, comment c peut-il contenir 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   // <= ..........

Vérifiez-le sur ideone.

118
Unsigned

Ceci est un bug du compilateur.

Bien que l'obtention de 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 s'optimise en conséquence.

Si c est défini comme int8_t, et int8_t passe en int, puis c-- est censé effectuer la soustraction c - 1 dans int arithmétique et reconvertissez le résultat en int8_t. La soustraction dans int ne 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 s'agir d'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.)

111
user743382

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 également conforme à certaines croyances sur le comportement qui sont détenues par la majorité de sa base d'utilisateurs.

Dans ce cas, il semble s'agir d'un bogue de conformité. L'expression c-- devrait manipuler c d'une manière similaire à c = c - 1. Ici, la valeur de c à droite est promue au type int, puis la soustraction a lieu. Puisque c est dans la plage de int8_t, cette soustraction ne débordera pas, mais elle peut produire une valeur qui est hors de la plage de int8_t. Lorsque cette valeur est affectée, une conversion reprend le type int8_t pour que le résultat rentre dans c. Dans le cas hors plage, la conversion a une valeur définie par l'implémentation. Mais une valeur hors de la plage de int8_t n'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 à définir par implémentation signifie que quelque chose dans la plage de int8_t est produit et le programme continue. La norme C permet ainsi des comportements tels que l'arithmétique de saturation (commun sur les DSP) ou le bouclage (architectures traditionnelles).

Le compilateur utilise un type de machine sous-jacent plus large lors de la manipulation des valeurs de petits types entiers comme int8_t ou 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 que la variable est un type 8 bits, le résultat plus large doit être tronqué dans la plage 8 bits. Un code explicite est nécessaire pour cela, car les emplacements de stockage de la machine (registres) sont plus larges que 8 bits et satisfait des valeurs plus grandes. Ici, le compilateur négligé de normaliser la valeur et la transmet simplement à printf tel quel. Le spécificateur de conversion %i dans printf n'a aucune idée que l'argument provenait à l'origine de int8_t calculs; il fonctionne simplement avec un argument int.

15
Kaz

Je ne peux pas insérer ceci dans un commentaire, donc je le poste comme réponse.

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

J'ai testé le code publié sur Ideone et remplacé c-- avec c = c - 1 et 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

Ey bizarre? Je ne sais pas trop ce que le compilateur fait aux expressions comme i++ ou i--. Il s'agit probablement de promouvoir la valeur de retour en int et de la transmettre. C'est la seule conclusion logique que je peux tirer car vous obtenez en fait des valeurs qui ne peuvent pas tenir sur 8 bits.

14

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


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

12
Zoltán

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 seulement BL doit être utilisé avec le reste d'EBX clear. Curieux qu'il utilise sub au lieu de dec. Le -45 est carrément mystérieux. C'est l'inversion au niveau du bit de 300 et 255 = 44. -45 = ~ 44. Il y a une connexion quelque part.

Cela passe par 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 utilise ensuite uniquement la partie basse de RAX, il est donc 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.

11
user2513931

Utilisation %hhd au lieu de %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 en poussant un nombre (supposément 8 bits) sur la pile, qui est vraiment de la taille d'un pointeur, car c'est ainsi que fonctionne l'opcode Push en x86.

4
Zotta

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 variable int32_t i Pour i et c. Désactivez l'optimisation ou effectuez une conversion directe printf("c: %i\n", (int8_t)c--);

3
Vsevolod

c est lui-même défini comme int8_t, mais lors de l'utilisation ++ ou -- plus de int8_t il est implicitement converti d'abord en int et le résultat de l'opération à la place la valeur interne de c est imprimé avec printf qui se trouve être int.

Voir valeur réelle de c après la boucle entière, en particulier après le dernier 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

c commence à utiliser int mémoire de taille mais imprimée comme int8_t lorsqu'il est imprimé comme lui-même en utilisant uniquement 8 bits. Utilise tous les 32 bits lorsqu'il est utilisé comme int

[Bogue du compilateur]

1
Izhar Aazmi

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

printf("c: %i\n", c);
0
r.mirzojonov