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 char
ne devrait jamais être un comportement indéfini aussi longtemps que sizeof(char) < sizeof(int)
. Mais cela n'explique pas comment c
obtenir une valeur impossible . En tant qu'entier 8 bits, comment peut c
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 // <= ..........
Découvrez-le sur ideone.
c++
gcc
undefined-behavior
Non signé
la source
la source
printf()
passe la conversion?Réponses:
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
c
est défini commeint8_t
, etint8_t
promeut versint
, alorsc--
est censé effectuer la soustractionc - 1
enint
arithmétique et reconvertir le résultat enint8_t
. La soustraction dansint
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 ê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
c
un type plus large. Vraisemblablement, c'est ce qui se passe 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 à 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 manipulerc
d'une manière similaire àc = c - 1
. Ici, la valeur dec
à droite est promue en typeint
, puis la soustraction a lieu. Comme sec
situe dans la plage deint8_t
, cette soustraction ne débordera pas, mais elle peut produire une valeur hors de la plage deint8_t
. Lorsque cette valeur est affectée, une conversion a lieu de nouveau au typeint8_t
afin que le résultat rentre dansc
. Dans le cas hors limites, la conversion a une valeur définie par l'implémentation. Mais une valeur hors de la plage deint8_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 soit définie par la mise en œuvre, cela signifie que quelque chose de l'ordre deint8_t
est 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_t
ouchar
. 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éeprintf
telle quelle . Le spécificateur de conversion%i
dansprintf
n'a aucune idée que l'argument provenait à l'origine deint8_t
calculs; il s'agit simplement de travailler avec unint
argument.la source
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--
parc = c - 1
et les valeurs sont restées dans la plage [-128 ... 127]:Freaky ey? Je ne sais pas grand-chose de ce que le compilateur fait aux expressions comme
i++
oui--
. Cela promeut probablement la valeur de retour à anint
et 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.la source
c = c - 1
moyensc = (int8_t) ((int)c - 1
. La conversion d'un hors plageint
enint8_t
a un comportement défini mais un résultat défini par l'implémentation. En fait, n'est-il pasc--
censé effectuer ces mêmes conversions?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
volatile
vous obligez à utiliser la mémoire pour elle et par conséquent obtenir les valeurs attendues dans la plage.la source
printf
ne pas se souciersizeof
des valeurs de format.Le code assembleur révèle le problème:
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:
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:
C'est donc un bug dans l'optimiseur.
la source
Utilisez
%hhd
plutô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.
la source
g++ -O3
. Changer%i
pour%hhd
ne change rien.Je pense que cela se fait par optimisation du code:
Le compilateur utilise la
int32_t i
variable à la fois pouri
etc
. Désactivez l'optimisation ou effectuez une diffusion directeprintf("c: %i\n", (int8_t)c--);
la source
(int8_t)(c & 0x0000ffff)--
c
est lui-même défini commeint8_t
, mais lors de l'opération++
ou au---
dessus,int8_t
il est implicitement converti en premierint
et le résultat de l'opération à la place, la valeur interne de c est imprimée avec printf qui se trouve êtreint
.Voir la valeur réelle de
c
après boucle entière, surtout après la dernière décrémentc'est la valeur correcte qui ressemble au comportement
-128 + 1 = 127
c
commence à utiliser laint
mémoire de taille mais imprimé commeint8_t
lors de l'impression en tant que lui-même en utilisant uniquement8 bits
. Utilise tout32 bits
lorsqu'il est utilisé commeint
[Bogue du compilateur]
la source
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
la source