web-dev-qa-db-fra.com

Si un nombre est trop grand, est-ce qu'il déborde sur l'emplacement de mémoire suivant?

J'ai passé en revue la programmation C et il y a juste quelques choses qui me dérangent.

Prenons ce code par exemple:

int myArray[5] = {1, 2, 2147483648, 4, 5};
int* ptr = myArray;
int i;
for(i=0; i<5; i++, ptr++)
    printf("\n Element %d holds %d at address %p", i, myArray[i], ptr);

Je sais qu'un int peut contenir une valeur maximale positive de 2.147.483.647. Donc, en allant sur ce point, cela "déborde" à l'adresse de mémoire suivante, ce qui fait que l'élément 2 apparaît comme "-2147483648" à cette adresse? Mais cela n'a pas vraiment de sens car dans la sortie, il est toujours dit que la prochaine adresse contient la valeur 4, puis 5. Si le nombre avait débordé à l'adresse suivante, cela ne changerait-il pas la valeur stockée à cette adresse ?

Je me souviens vaguement de la programmation dans MIPS Assembly et de regarder les adresses changer les valeurs pendant le programme étape par étape que les valeurs affectées à ces adresses changeraient.

Sauf si je me souviens mal, voici une autre question: si le numéro attribué à une adresse spécifique est plus grand que le type (comme dans mon tableau [2]), cela n'affecte-t-il pas les valeurs stockées à l'adresse suivante?

Exemple: Nous avons int myNum = 4 milliards à l'adresse 0x10010000. Bien sûr, myNum ne peut pas stocker 4 milliards, il apparaît donc comme un nombre négatif à cette adresse. Bien qu'il ne soit pas en mesure de stocker ce grand nombre, cela n'a aucun effet sur la valeur stockée à l'adresse suivante de 0x10010004. Correct?

Les adresses mémoire ont juste assez d'espace pour contenir certaines tailles de nombres/caractères, et si la taille dépasse la limite alors elle sera représentée différemment (comme essayer de stocker 4 milliards dans l'int, mais elle apparaîtra comme un nombre négatif) et cela n'a donc aucun effet sur les nombres/caractères stockés à l'adresse suivante.

Désolé si je suis allé trop loin. J'ai eu un gros problème cérébral toute la journée.

29
stumpy

Non. En C, les variables ont un ensemble fixe d'adresses mémoire avec lesquelles travailler. Si vous travaillez sur un système avec 4 octets ints et que vous définissez une variable int sur 2,147,483,647 puis ajoutez 1, la variable contiendra généralement -2147483648. (Sur la plupart des systèmes. Le comportement n'est en fait pas défini.) Aucun autre emplacement de mémoire ne sera modifié.

En substance, le compilateur ne vous permettra pas d'attribuer une valeur trop grande pour le type. Cela générera une erreur de compilation. Si vous le forcez avec un cas, la valeur sera tronquée.

Vu sous forme de bits, si le type ne peut stocker que 8 bits et que vous essayez de forcer la valeur 1010101010101 avec un boîtier, vous vous retrouverez avec les 8 derniers bits, ou 01010101.

Dans votre exemple, indépendamment de ce que vous faites pour myArray[2], myArray[3] contiendra '4'. Il n'y a pas de "débordement". Vous essayez de mettre quelque chose de plus de 4 octets, il se contentera de tout couper en haut de gamme, laissant les 4 derniers octets. Sur la plupart des systèmes, cela entraînera -2147483648.

D'un point de vue pratique, vous voulez simplement vous assurer que cela n'arrive jamais. Ces sortes de débordements entraînent souvent des défauts difficiles à résoudre. En d'autres termes, si vous pensez qu'il y a une chance que vos valeurs se chiffrent en milliards, n'utilisez pas int.

48
Gort the Robot

Le dépassement d'entier signé est un comportement non défini. Si cela se produit, votre programme n'est pas valide. Le compilateur n'est pas tenu de vérifier cela pour vous, il peut donc générer un exécutable qui semble faire quelque chose de raisonnable, mais rien ne garantit qu'il le fera.

Cependant, le dépassement d'entier non signé est bien défini. Il encapsulera le modulo UINT_MAX + 1. La mémoire non occupée par votre variable ne sera pas affectée.

Voir aussi https://stackoverflow.com/q/18195715/95189

24
Vaughn Cato

Donc, il y a deux choses ici:

  • le niveau de langage: quelle est la sémantique de C
  • le niveau machine: quelle est la sémantique du Assembly/CPU que vous utilisez

Au niveau de la langue:

En C:

  • le débordement et le sous-dépassement sont définis comme une arithmétique modulo pour les entiers non signés , ainsi leur valeur "boucles"
  • le débordement et le sous-dépassement sont Comportement indéfini pour les entiers signés , donc tout peut arriver

Pour ceux qui voudraient un exemple "quoi que ce soit", j'ai vu:

for (int i = 0; i >= 0; i++) {
    ...
}

changer en:

for (int i = 0; true; i++) {
    ...
}

et oui, c'est une transformation légitime.

Cela signifie qu'il existe en effet des risques potentiels d'écrasement de la mémoire en cas de débordement en raison d'une transformation étrange du compilateur.

Remarque: sur Clang ou gcc, utilisez -fsanitize=undefined dans Debug pour activer le ndefined Behavior Sanitizer qui abandonnera en cas de dépassement/dépassement des entiers signés.

Ou cela signifie que vous pouvez remplacer la mémoire en utilisant le résultat de l'opération d'indexation (non cochée) dans un tableau. Ceci est malheureusement beaucoup plus probable en l'absence de détection de débordement/débordement.

Remarque: sur Clang ou gcc, utilisez -fsanitize=address dans Debug pour activer Address Sanitizer qui abandonnera l'accès hors limites.


Au niveau de la machine :

Cela dépend vraiment des instructions d'assemblage et du processeur que vous utilisez:

  • sur x86, [[# # ~] ajouter [~ # ~] utilisera 2-complément en cas de débordement/débordement et définira le OF (drapeau de débordement)
  • sur le futur processeur Mill, il y aura 4 modes de débordement différents pour Add:
    • Modulo: modulo à 2 compléments
    • Piège: un piège est généré, interrompant le calcul
    • Saturer: la valeur reste bloquée sur min en cas de sous-débit ou max en cas de débordement
    • Double largeur: le résultat est généré dans un registre double largeur

Notez que si des choses se produisent dans les registres ou la mémoire, dans aucun cas le CPU n'écrase la mémoire en cas de débordement.

14
Matthieu M.

Pour approfondir la réponse de @ StevenBurnap, la raison pour laquelle cela se produit est à cause du fonctionnement des ordinateurs au niveau de la machine.

Votre baie est stockée en mémoire (par exemple en RAM). Lorsqu'une opération arithmétique est effectuée, la valeur en mémoire est copiée dans les registres d'entrée du circuit qui effectue l'arithmétique (l'ALU: Arithmetic Logic Unit ), l'opération est alors effectuée sur les données de les registres d'entrée, produisant un résultat dans le registre de sortie. Ce résultat est ensuite recopié en mémoire à la bonne adresse en mémoire, laissant les autres zones de mémoire intactes.

4
Pharap

Tout d'abord (en supposant la norme C99), vous pouvez inclure <stdint.h> en-tête standard et utilisez certains des types définis ici, notamment int32_t qui est exactement un entier signé de 32 bits, ou uint64_t qui est exactement un entier non signé de 64 bits, et ainsi de suite. Vous souhaiterez peut-être utiliser des types tels que int_fast16_t pour des raisons de performances.

Lisez les réponses des autres expliquant que l'arithmétique non signée ne se déverse pas (ou ne déborde pas) vers des emplacements de mémoire adjacents. Attention au comportement indéfini sur débordement signé .

Ensuite, si vous devez calculer exactement d'énormes nombres entiers (par exemple, vous voulez calculer la factorielle de 1000 avec tous ses 2568 chiffres en décimal), vous voulez des bigints aka nombres de précision arbitraires (ou bignums). Les algorithmes pour une arithmétique bigint efficace sont très intelligents et nécessitent généralement l'utilisation d'instructions machine spécialisées (par exemple, certains ajoutent Word avec carry, si votre processeur en dispose). Par conséquent, je recommande fortement dans ce cas d'utiliser une bibliothèque bigint existante comme GMPlib

4