web-dev-qa-db-fra.com

Pourquoi cette boucle for se termine-t-elle sur certaines plates-formes et pas sur d'autres?

J'ai récemment commencé à apprendre le C et je prends un cours avec le C comme sujet. Je suis en train de jouer avec des boucles et je rencontre un comportement étrange que je ne sais pas comment expliquer.

#include <stdio.h>

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%d \n", sizeof(array)/sizeof(int));
  return 0;
}

Sur mon ordinateur portable sous Ubuntu 14.04, ce code ne casse pas. Il va jusqu'au bout. Sur l'ordinateur de mon école qui exécute CentOS 6.6, tout fonctionne bien. Sous Windows 8.1, la boucle ne se termine jamais.

Ce qui est encore plus étrange, c'est que lorsque je modifie la condition de la boucle for en: i <= 11, le code ne se termine que sur mon ordinateur portable sous Ubuntu. Il ne se termine jamais dans CentOS et Windows.

Quelqu'un peut-il expliquer ce qui se passe dans la mémoire et pourquoi les différents systèmes d'exploitation exécutant le même code donnent des résultats différents?

EDIT: Je sais que la boucle est hors limites. Je le fais intentionnellement. Je n'arrive tout simplement pas à comprendre en quoi le comportement peut être différent selon les systèmes d'exploitation et les ordinateurs.

240
JonCav

Sur mon ordinateur portable sous Ubuntu 14.04, ce code ne casse pas, il est terminé. Sur l'ordinateur de mon école qui exécute CentOS 6.6, tout fonctionne bien. Sous Windows 8.1, la boucle ne se termine jamais.

Ce qui est plus étrange, c’est quand je modifie la condition de la boucle for en: i <= 11, le code ne se termine que sur mon ordinateur portable sous Ubuntu. CentOS et Windows ne se termine jamais.

Vous venez de découvrir la mémoire vive. Vous pouvez en lire plus à ce sujet ici: Qu'est-ce qu'un "stomp de mémoire"?

Lorsque vous allouez int array[10],i;, ces variables sont enregistrées dans la mémoire (en particulier, elles sont affectées à la pile, qui est un bloc de mémoire associé à la fonction). array[] et i sont probablement adjacents en mémoire. Il semble que sous Windows 8.1, i se trouve dans array[10]. Sur CentOS, i se trouve dans array[11]. Et sur Ubuntu, il n’est ni l’un ni l’autre (c’est peut-être à array[-1]?).

Essayez d’ajouter ces instructions de débogage à votre code. Vous devriez remarquer qu'à l'itération 10 ou 11, array[i] pointe sur i.

#include <stdio.h>

int main() 
{ 
  int array[10],i; 

  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 
356
QuestionC

Le bug se situe entre ces morceaux de code:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

Puisque array ne comporte que 10 éléments, dans la dernière itération array[10] = 0; correspond à un dépassement de mémoire tampon. Les débordements de mémoire tampon sont NDEFINED BEHAVIOR, ce qui signifie qu'ils pourraient formater votre disque dur ou faire fuir les démons de votre nez.

Il est assez courant que toutes les variables de pile soient disposées les unes à côté des autres. Si i est situé où array[10] écrit dans, l'UB réinitialisera i en 0, conduisant ainsi à la boucle non terminée.

Pour résoudre ce problème, changez la condition de la boucle en i < 10.

98
o11c

Dans ce qui devrait être la dernière exécution de la boucle, vous écrivez dans array[10], mais le tableau ne contient que 10 éléments, numérotés de 0 à 9. La spécification du langage C indique qu'il s'agit d'un "comportement indéfini". En pratique, cela signifie que votre programme essaiera d'écrire dans la mémoire de la taille de int qui se trouve immédiatement après la mémoire de array. Ce qui se passe ensuite dépend de ce qui se trouve réellement là-bas, et cela dépend non seulement du système d'exploitation, mais plus encore du compilateur, des options du compilateur (telles que les paramètres d'optimisation), de l'architecture du processeur, du code environnant. , etc. Cela peut même varier d’une exécution à l’autre, par exemple en raison de randomisation de l'espace d'adressage (probablement pas sur cet exemple de jouet, mais cela se produit dans la vie réelle). Certaines possibilités incluent:

  • L'emplacement n'a pas été utilisé. La boucle se termine normalement.
  • L'emplacement a été utilisé pour quelque chose qui a eu la valeur 0. La boucle se termine normalement.
  • L'emplacement contenait l'adresse de retour de la fonction. La boucle se termine normalement, mais le programme se bloque car il tente de passer à l'adresse 0.
  • L'emplacement contient la variable i. La boucle ne se termine jamais car i redémarre à 0.
  • L'emplacement contient une autre variable. La boucle se termine normalement, mais des choses "intéressantes" se produisent.
  • L'emplacement est une adresse mémoire invalide, par ex. parce que array est juste à la fin d'une page de mémoire virtuelle et que la page suivante n'est pas mappée.
  • Les démons sortent de votre nez . Heureusement, la plupart des ordinateurs ne disposent pas du matériel requis.

Ce que vous avez observé sous Windows est que le compilateur a décidé de placer la variable i immédiatement après le tableau en mémoire, de sorte que array[10] = 0 a finalement été affecté à i. Sur Ubuntu et CentOS, le compilateur n'a pas placé i là. Presque toutes les implémentations en C regroupent les variables locales en mémoire, sur une pile pile mémoire , à une exception près: certaines variables locales peuvent être entièrement placées dans registres . Même si la variable est sur la pile, l’ordre des variables est déterminé par le compilateur et peut dépendre non seulement de l’ordre dans le fichier source, mais également de leurs types (pour éviter de gaspiller de la mémoire en raison de contraintes d’alignement qui laisseraient des trous). , sur leurs noms, sur certaines valeurs de hachage utilisées dans la structure de données interne du compilateur, etc.

Si vous voulez savoir ce que votre compilateur a décidé de faire, vous pouvez lui dire de vous montrer le code de l'assembleur. Oh, et apprendre à déchiffrer l'assembleur (c'est plus facile que de l'écrire). Avec GCC (et d’autres compilateurs, en particulier dans le monde Unix), passez l’option -S pour produire du code assembleur au lieu d’un fichier binaire. Par exemple, voici l'extrait d'assembleur pour la boucle de compilation avec GCC sur AMD64 avec l'option d'optimisation -O0 (aucune optimisation), avec des commentaires ajoutés manuellement:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -52(%rbp)            ; compare i to 10
    jle     .L3

Ici, la variable i est 52 octets sous le haut de la pile, tandis que le tableau commence 48 octets sous le haut de la pile. Donc, ce compilateur a placé i juste avant le tableau; vous écraseriez i s'il vous arrivait d'écrire sur array[-1]. Si vous changez array[i]=0 en array[9-i]=0, vous obtiendrez une boucle infinie sur cette plate-forme particulière avec ces options de compilateur particulières.

Maintenant, compilons votre programme avec gcc -O1.

    movl    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %ebx
    jne     .L3

C'est plus court! Le compilateur a non seulement refusé d'allouer un emplacement de pile pour i - il est uniquement stocké dans le registre ebx - mais il n'a pas pris la peine d'allouer de mémoire pour array, ni de générer du code pour définir ses éléments, car il a remarqué qu'aucun de ces éléments n'est jamais utilisé.

Pour rendre cet exemple plus éloquent, assurons-nous que les assignations de tableaux sont effectuées en fournissant au compilateur quelque chose qu'il ne peut pas optimiser. Une méthode simple consiste à utiliser le tableau d’un autre fichier. En raison de la compilation séparée, le compilateur ne sait pas ce qui se passe dans un autre fichier (à moins d’optimiser au moment de la liaison, qui gcc -O0 ou gcc -O1 pas). Créer un fichier source use_array.c contenant

void use_array(int *array) {}

et changez votre code source en

#include <stdio.h>
void use_array(int *array);

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%zd \n", sizeof(array)/sizeof(int));
  use_array(array);
  return 0;
}

Compiler avec

gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o

Cette fois, le code assembleur ressemble à ceci:

    movq    %rsp, %rbx
    leaq    44(%rsp), %rbp
.L3:
    movl    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

Maintenant, le tableau est sur la pile, à 44 octets du haut. Qu'en est-il de i? Cela n'apparaît nulle part! Mais le compteur de boucle est conservé dans le registre rbx. Ce n'est pas exactement i, mais l'adresse du array[i]. Le compilateur a décidé que, puisque la valeur de i n'avait jamais été utilisée directement, il était inutile de recourir à l'arithmétique pour calculer où stocker 0 lors de chaque exécution de la boucle. Au lieu de cela, cette adresse est la variable de boucle et l'arithmétique pour déterminer les limites a été effectuée en partie au moment de la compilation (multiplier 11 itérations par 4 octets par élément de tableau pour obtenir 44) et en partie au moment de l'exécution mais une fois pour toutes avant le début de la boucle ( effectuer une soustraction pour obtenir la valeur initiale).

Même sur cet exemple très simple, nous avons vu comment changer les options du compilateur (activer l'optimisation) ou changer quelque chose de mineur (array[i] en array[9-i])) ou même changer quelque chose apparemment sans rapport (ajouter l'appel à use_array) peut faire une différence significative par rapport au programme exécutable généré par le compilateur. Les optimisations du compilateur peuvent faire beaucoup de choses qui peuvent paraître peu intuitives sur des programmes qui invoquent un comportement non défini . C'est pourquoi un comportement indéfini reste complètement indéfini. Lorsque vous vous écartez un peu des morceaux, dans des programmes réels, il peut être très difficile de comprendre la relation entre ce que fait le code et ce qu'il aurait dû faire, même pour les programmeurs expérimentés.

38
Gilles

Contrairement à Java, le C ne vérifie pas les limites d'un tableau, c'est-à-dire qu'il n'y a pas de ArrayIndexOutOfBoundsException, le travail consistant à vérifier que l'index du tableau est valide est laissé au programmeur. Faire exprès cela conduit à un comportement indéfini, tout peut arriver.


Pour un tableau:

int array[10]

les index ne sont valides que dans la plage 0 à 9. Cependant, vous essayez de:

for (i = 0; i <=10 ; i++)

accès array[10] ici, changez la condition en i < 10

25
Yu Hao

Vous avez une violation de bornes, et sur les plateformes qui ne terminent pas, je pense que vous mettez par inadvertance i à zéro à la fin de la boucle, de sorte que tout recommence.

array[10] n'est pas valide; il contient 10 éléments, array[0] à array[9], et array[10] est le 11e. Votre boucle doit être écrite pour arrêter avant10, comme suit:

for (i = 0; i < 10; i++)

array[10]lands est défini par la mise en œuvre et, de façon amusante, sur deux de vos plates-formes, il atterrit sur i, que ces plates-formes disposent apparemment directement après array. i est mis à zéro et la boucle continue indéfiniment. Pour vos autres plates-formes, i peut être situé avant array, ou array peut être suivi d'un remplissage.

19
Derek T. Jones

Vous déclarez int array[10] signifie que array a un index 0 à 9 (total 10 éléments entiers qu’il peut contenir). Mais la boucle suivante,

for (i = 0; i <=10 ; i++)

boucle 0 à 10 signifie 11 temps. Par conséquent, lorsque i = 10, il débordera de la mémoire tampon et provoquera comportement non défini .

Alors essayez ceci:

for (i = 0; i < 10 ; i++)

ou,

for (i = 0; i <= 9 ; i++)
12
rakeb.mazharul

Il est indéfini dans array[10] et donne comportement non défini comme décrit précédemment. Pensez-y comme ça:

J'ai 10 articles dans mon panier d'épicerie. Elles sont:

0: une boîte de céréales
1: Pain
2: Lait
3: Tarte
4 œufs
5: Gâteau
6: 2 litres de soda
7: Salade
8: Burgers
9: Crème glacée

cart[10] n'est pas défini et peut donner une exception hors limites dans certains compilateurs. Mais beaucoup ne le font apparemment pas. Le 11ème élément apparent est un élément non actuellement dans le panier. Le 11ème élément est ce que je vais appeler, un "élément poltergeist". Cela n'a jamais existé, mais c'était là.

Pourquoi certains compilateurs donnent i un index de array[10] ou array[11] ou même array[-1] est à cause de votre déclaration d'initialisation/déclaration. Certains compilateurs interprètent ceci comme:

  • "Alloue 10 blocs de ints pour array[10] et un autre bloc int. pour que ce soit plus facile les place les uns à côté des autres."
  • Comme auparavant, mais déplacez-le d'un espace ou deux pour que array[10] ne pointe pas vers i.
  • Faites la même chose qu'avant, mais allouez i à array[-1] (car un index d'un tableau ne peut pas, ou ne devrait pas être négatif), ou allouez-le à un endroit complètement différent, car le système d'exploitation peut le gérer, et c'est plus sûr.

Certains compilateurs veulent que les choses aillent plus vite et certains préfèrent la sécurité. Tout est une question de contexte. Si je développais une application pour l'ancien système d'exploitation BREW (le système d'exploitation d'un téléphone de base), par exemple, la sécurité ne lui importait pas. Si je développais pour un iPhone 6, celui-ci pourrait fonctionner rapidement quoi qu'il en soit, il faudrait donc mettre l'accent sur la sécurité. (Sérieusement, avez-vous lu les directives de l'App Store d'Apple ou avez-vous lu le développement de Swift et de Swift 2.0?)

7
DDPWNAGE

Puisque vous avez créé un tableau de taille 10, la condition de la boucle for devrait être la suivante:

int array[10],i;

for (i = 0; i <10 ; i++)
{

Actuellement, vous essayez d'accéder à l'emplacement non attribué à partir de la mémoire en utilisant array[10] et cela provoque le comportement non défini. Un comportement indéfini signifie que votre programme se comportera de manière indéterminée, de sorte qu'il peut donner différents résultats à chaque exécution.

6
Steephen

Eh bien, le compilateur C ne vérifie généralement pas les limites. Vous pouvez obtenir une erreur de segmentation si vous vous référez à un emplacement qui "n'appartient" pas à votre processus. Cependant, les variables locales sont allouées sur la pile et, selon la manière dont la mémoire est allouée, la zone située juste au-delà du tableau (array[10]) peut appartenir au segment de mémoire du processus. Ainsi, aucun piège de segmentation n’est jeté et c’est ce que vous semblez éprouver. Comme d'autres l'ont souligné, il s'agit d'un comportement non défini en C et votre code peut être considéré comme erratique. Comme vous apprenez le C, il est préférable de prendre l’habitude de vérifier les limites de votre code.

5
unxnut

Au-delà de la possibilité que la mémoire soit agencée de sorte qu'une tentative d'écriture sur a[10] écrase en réalité i, il est également possible qu'un compilateur optimiseur détermine que le test de boucle ne peut pas être atteint avec une valeur de i supérieur à dix sans que le code ait d'abord accédé à l'élément de tableau inexistant a[10].

Puisqu'une tentative d'accéder à cet élément constituerait un comportement indéfini, le compilateur n'aurait aucune obligation quant à ce que le programme pourrait faire après ce moment. Plus précisément, étant donné que le compilateur n’aurait aucune obligation de générer du code pour vérifier l’indice de boucle dans les cas où il serait supérieur à dix, il n’aurait aucune obligation de générer du code pour le vérifier du tout; il pourrait plutôt supposer que le test <=10 donnera toujours la valeur true. Notez que cela serait vrai même si le code lisait a[10] plutôt que de l'écrire.

4
supercat

Lorsque vous parcourez passé i==9, vous affectez zéro aux "éléments de tableau" qui sont réellement situés après le tableau , de sorte que vous écrasez certains autre informations. Très probablement, vous écrasez la variable i, située après a[]. Ainsi, vous réinitialisez simplement la variable i _ et redémarrez ainsi la boucle.

Vous pourriez le découvrir vous-même si vous imprimiez i dans la boucle:

      printf("test i=%d\n", i);

au lieu de juste

      printf("test \n");

Bien sûr, ce résultat dépend fortement de l’allocation de mémoire pour vos variables, qui à son tour dépend du compilateur et de ses paramètres. Il est donc généralement Comportement non défini - C'est pourquoi les résultats sur des machines différentes, des systèmes d'exploitation différents ou des compilateurs différents peuvent différer.

3
CiaPan

Je vais suggérer quelque chose que je ne trouve pas ci-dessus:

Essayez d’assigner un tableau [i] = 20;

Je suppose que cela devrait terminer le code partout .. (étant donné que vous gardez i <= 10 ou ll)

Si cela fonctionne, vous pouvez fermement décider que les réponses spécifiées ici sont déjà correctes [la réponse liée à la mémoire en écrasant une par ex.]

0
Raining fire

l'erreur est dans le tableau de portions [10] w/c est également l'adresse de i (int tableau [10], i;). quand le tableau [10] est mis à 0, alors i serait 0 w/c réinitialise la boucle entière et provoque la boucle infinie. il y aura boucle infinie si le tableau [10] est compris entre 0 et 10. La boucle correcte doit être pour (i = 0; i <10; i ++) {...} int tableau [10], i; pour (i = 0; i <= 10; i ++) tableau [i] = 0;

0