Comment ai-je obtenu une valeur supérieure à 8 bits dans un entier de 8 bits?

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 sizeof(int) largeur de sizeof(int) . Si je comprends bien, l’incrémentation d’un caractère 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 de bit?

Code

 // Comstackd with gcc-4.7.2 #include  #include  #include  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; } 

Sortie

 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.

    Ceci est un bug de compilation.

    Bien que l’obtention de résultats impossibles pour un comportement indéfini soit une conséquence valable, votre code ne comporte en réalité aucun comportement indéfini. 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 dans int , alors c-- est supposé effectuer la soustraction c - 1 dans l’arithmétique int et convertir 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.)

    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 avec d’autres compilateurs, ainsi qu’à certaines croyances sur le comportement de la majorité de ses utilisateurs.

    Dans ce cas, il semble que ce soit un bogue de conformité. L’expression c-- devrait manipuler c d’une manière similaire à c = c - 1 . Ici, la valeur de c sur la droite est promue pour taper int , puis la soustraction a lieu. Puisque c est dans l’intervalle 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 est effectuée sur le type int8_t pour que le résultat revienne 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_t n’est pas une valeur valide définie par l’implémentation. 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 l’implémentation signifie que quelque chose dans l’intervalle de int8_t est produit et que le programme continue. Le standard C permet ainsi des comportements tels que l’arithmétique de saturation (commune aux DSP) ou le wrap-around (architectures grand public).

    Le compilateur utilise un type de machine sous-jacent plus large lors de la manipulation de valeurs de petits types entiers comme int8_t ou char . Lorsque l’arithmétique est effectuée, les résultats qui sont hors de scope du petit nombre entier peuvent être capturés de manière fiable dans ce type plus large. Pour préserver le comportement visible de l’extérieur que 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 cela car les emplacements de stockage de la machine (registres) sont plus larges que 8 bits et satisfont les valeurs les plus grandes. Ici, le compilateur a négligé de normaliser la valeur et l’a simplement transmis à printf tel quel. Le spécificateur de conversion %i dans printf n’a aucune idée que l’argument provenait à l’ int8_t calculs int8_t ; il ne fait que travailler avec un argument int .

    Je ne peux pas faire ça dans un commentaire, alors je le poste comme réponse.

    Pour une raison très étrange, l’opérateur est le coupable.

    J’ai testé le code posté sur Ideone et remplacé c-- par 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 

    Freaky ey? Je ne sais pas grand chose sur ce que fait le compilateur pour des expressions comme i++ ou i-- . Il est probable que la promotion de la valeur de retour à un int et son passage. C’est la seule conclusion logique que je puisse trouver car vous obtenez en fait des valeurs qui ne peuvent pas être intégrées à 8 bits.

    Je suppose que le matériel sous-jacent utilise toujours un registre de 32 bits pour le conserver int8_t. Comme la spécification n’impose pas de comportement en cas de dépassement de capacité, l’implémentation ne vérifie pas le dépassement de capacité 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 obtenir la valeur attendue dans la plage.

    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 associé à la post-décrémentation FF, ou seul BL doit être utilisé avec le rest de EBX clair. Curieux qu’il utilise sub au lieu de déc. Le -45 est complètement mystérieux. C’est l’inversion binary de 300 & 255 = 44. -45 = ~ 44. Il y a une connexion quelque part.

    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, 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 

    Donc, c’est un bogue dans l’optimiseur.

    Utilisez %hhd au lieu de %i ! Devrait résoudre votre problème.

    Ce que vous voyez est le résultat des optimisations du compilateur combinées à la commande par printf d’imprimer un numéro 32 bits, puis de pousser un nombre (supposé 8 bits) sur la stack, qui est vraiment de la taille d’un opcode push.

    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 à la fois pour i et c . Désactivez l'optimisation ou faites directement un cast printf("c: %i\n", (int8_t)c--);

    c est lui-même défini comme int8_t , mais lorsque vous int8_t ++ ou -- int8_t il est implicitement converti en premier et le résultat de l’opération en revanche, la valeur interne de c est imprimée avec printf qui se trouve être int .

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

     -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 la taille de la mémoire int mais imprimée en tant que int8_t lorsqu’elle est imprimée en tant que telle en utilisant seulement 8 bits . Utilise tous les 32 bits lorsqu’il est utilisé comme int

    [Bug du compilateur]

    Je pense que cela s’est produit parce que votre boucle ira jusqu’à ce que l’intérieur devienne 300 et c devienne -300. Et la dernière valeur est parce que

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