Pourquoi cela pour la boucle de sortie sur certaines plates-formes et pas sur d’autres?

J’ai récemment commencé à apprendre le C et je prends un cours avec C comme sujet. Je joue actuellement avec des boucles et je rencontre des comportements bizarres que je ne sais pas comment expliquer.

#include  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 fonctionne jusqu’à la fin. Sur l’ordinateur de mon école qui exécute CentOS 6.6, cela fonctionne également très bien. Sur 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 pour: i <= 11 , le code se termine uniquement 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 va hors limites. Je le fais intentionnellement. Je n’arrive pas à comprendre comment le comportement peut être différent selon les systèmes d’exploitation et les ordinateurs.

    Sur mon ordinateur portable fonctionnant sous Ubuntu 14.04, ce code ne l’interrompt pas jusqu’à son terme. Sur l’ordinateur de mon école qui exécute CentOS 6.6, cela fonctionne également très bien. Sur Windows 8.1, la boucle ne se termine jamais.

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

    Vous venez de découvrir le piétinement de la mémoire. Vous pouvez en savoir plus à ce sujet ici: Qu'est-ce qu'un «stomp de mémoire»?

    Lorsque vous allouez int array[10],i; , ces variables vont en mémoire (en particulier, elles sont allouées sur la stack, 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 me trouve au array[10] . Sur CentOS, i me trouve au array[11] . Et sur Ubuntu, ce n'est dans aucun des deux endroits (peut-être que c'est au array[-1] ?).

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

     #include  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; } 

    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 compte que 10 éléments, dans le dernier array[10] = 0; itération array[10] = 0; est un buffer overflow. Les débordements de tampons sont des COMPORTEMENTS NON DÉFINIS , ce qui signifie qu'ils peuvent formater votre disque dur ou provoquer le vol de démons.

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

    Pour corriger, changez la condition de la boucle en i < 10 .

    Dans ce qui devrait être la dernière exécution de la boucle, vous écrivez dans le array[10] , mais il n’y a que 10 éléments dans le tableau, numérotés de 0 à 9. La spécification du langage C indique qu’il s’agit d’un «comportement indéfini». Ce que cela signifie dans la pratique, c’est que votre programme essaiera d’écrire sur le morceau de mémoire intégré qui se trouve immédiatement après le array en mémoire. Qu’est-ce qui se passe alors dépend de ce qui, en fait, se trouve là, et cela dépend non seulement du système d’exploitation, mais plus encore sur le compilateur, sur les options du compilateur (comme les parameters d’optimisation), sur l’architecture du processeur, sur le code environnant , etc. Cela pourrait même varier d’une exécution à l’autre, par exemple en raison de la randomisation de l’espace d’adressage (probablement pas sur cet exemple de jouet, mais cela se produit dans la vraie vie). 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 s’est avéré avoir 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 essaie de sauter à l’adresse 0.
    • L’emplacement contient la variable i . La boucle ne se termine jamais parce que i redémarre à 0.
    • L’emplacement contient une autre variable. La boucle se termine normalement, mais alors des choses «intéressantes» se produisent.
    • L’emplacement est une adresse mémoire invalide, par exemple parce que le 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 requirejs.

    Ce que vous avez observé sur Windows, c’est que le compilateur a décidé de placer la variable i immédiatement après le tableau en mémoire, de sorte que le array[10] = 0 été atsortingbué à i . Sur Ubuntu et CentOS, le compilateur ne m’a pas placé. Presque toutes les implémentations C regroupent les variables locales en mémoire, sur une stack de mémoire , à une exception près: certaines variables locales peuvent être placées entièrement dans des registres . Même si la variable est sur la stack, l’ordre des variables est déterminé par le compilateur et peut dépendre non seulement de la commande dans le fichier source mais aussi de leurs types (pour éviter de gaspiller de la mémoire dans les contraintes d’alignement qui laisseraient des trous) , sur leurs noms, sur une valeur de hachage utilisée 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 demander 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 certains autres compilateurs, en particulier dans le monde Unix), passez l’option -S pour produire du code assembleur au lieu d’un binary. Par exemple, voici l’extrait d’assemblage de la boucle de compilation avec GCC sur amd64 avec l’option d’optimisation -O0 (sans 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 ssortingng 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 de 52 octets en dessous du sumt de la stack, tandis que le tableau commence à 48 octets en dessous du sumt de la stack. Donc, ce compilateur se trouve avoir placé juste avant le tableau; vous écraseriez i si vous écrivez dans le array[-1] . Si vous changez le array[i]=0 en array[9-i]=0 , vous obtiendrez une boucle infinie sur cette plate-forme particulière avec ces options de compilation particulières.

    Maintenant, gcc -O1 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 n’a pas seulement refusé d’allouer un emplacement de stack pour i – il n’est jamais enregistré que dans le registre ebx – mais il n’a pas pris la peine d’allouer de la mémoire pour le array ou de générer du code pour définir ses éléments. des éléments sont jamais utilisés.

    Pour rendre cet exemple plus éloquent, assurons-nous que les affectations de tableau sont effectuées en fournissant au compilateur quelque chose qu’il n’est pas capable d’optimiser. Un moyen simple de le faire est d’utiliser le tableau à partir d’un autre fichier – à cause de la compilation séparée, le compilateur ne sait pas ce qui se passe dans un autre fichier (à moins qu’il optimise au moment du lien que gcc -O0 ou gcc -O1 ne ). Créez un fichier source use_array.c contenant

     void use_array(int *array) {} 

    et changez votre code source en

     #include  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; } 

    Comstackr 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 stack, à 44 octets du haut. Qu'en est-il de i ? Il 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'a jamais été utilisée directement, il était inutile de faire de l'arithmétique pour calculer où stocker 0 à chaque exécution de la boucle. Au lieu de cela, l'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 partiellement 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 (append l'appel à use_array ) peut faire une différence significative avec le programme exécutable généré par le compilateur. Les optimisations du compilateur peuvent faire beaucoup de choses qui peuvent sembler peu intuitives sur les programmes qui invoquent un comportement indéfini . C'est pourquoi le comportement indéfini rest complètement indéfini. Lorsque vous vous écartez légèrement des pistes, dans les programmes réels, il peut être très difficile de comprendre la relation entre ce que le code fait et ce qu'il aurait dû faire, même pour les programmeurs expérimentés.

    Contrairement à Java, C ne vérifie pas les limites des tableaux, c’est-à-dire qu’il n’ya pas d’ ArrayIndexOutOfBoundsException , le travail de ArrayIndexOutOfBoundsException la validité de l’index du tableau est laissé au programmeur. Le faire express 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éder au array[10] ici, changer la condition à i < 10

    Vous avez une violation de limites, et sur les plates-formes non terminées, je pense que vous avez par inadvertance mis à zéro à la fin de la boucle, de sorte qu’il recommence.

    array[10] n’est pas valide; il contient 10 éléments, le array[0] travers le array[9] , et le array[10] est le 11ème. Votre boucle doit être écrite pour s’arrêter avant 10 , comme suit:

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

    Là où le array[10] atterrit est défini par l'implémentation, et de manière amusante, sur deux de vos plates-formes, il se pose sur i , que ces plates-formes disposent apparemment directement après le array . i mis à zéro et la boucle continue pour toujours. Pour vos autres plates-formes, il se peut que i sois situé avant le array ou que le array soit rempli après celui-ci.

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

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

    va boucler 0 à 10 signifie 11 fois. Par conséquent, lorsque i = 10 il débordera le tampon et provoquera un comportement indéfini .

    Alors essayez ceci:

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

    ou,

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

    Il n’est pas défini au array[10] et donne un comportement indé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 article apparent est un article qui n’est pas réellement dans le panier. Le 11ème article indique ce que je vais appeler un “article poltergeist”. Cela n’a jamais existé, mais c’était là.

    Pourquoi certains compilateurs donnent un index du array[10] ou du array[11] ou même le array[-1] est dû à votre déclaration d’initialisation / déclaration. Certains compilateurs interprètent cela comme:

    • “Allouez 10 blocs de int pour le array[10] et un autre bloc int . Pour le rendre plus facile, placez-les les uns à côté des autres.”
    • Comme avant, mais déplacez-le d’un ou deux espaces, de sorte que le array[10] ne pointe pas vers i .
    • Faites la même chose qu’avant, mais allouez i au array[-1] (car un index d’un tableau ne peut pas ou ne doit pas être négatif) ou l’allouer à 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 compilateurs préfèrent la sécurité. Tout est dans le contexte. Si, par exemple, je développais une application pour l’ancienne OS BREW (le système d’exploitation d’un téléphone de base), cela ne ferait aucun cas de la sécurité. Si je développais pour un iPhone 6, il pourrait alors fonctionner rapidement, peu importe ce que je faisais, donc il faudrait que je mette l’accent sur la sécurité. (Sérieusement, avez-vous lu les directives Apple Store, ou lisez le développement de Swift et Swift 2.0?)

    Depuis que vous avez créé un tableau de taille 10, pour la condition de boucle devrait être comme suit:

     int array[10],i; for (i = 0; i <10 ; i++) { 

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

    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 stack 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 problème de segmentation n’est levé et c’est ce que vous semblez ressentir. Comme d’autres l’ont fait remarquer, il s’agit d’un comportement indéfini dans C et votre code peut être considéré comme irrégulier. Comme vous apprenez C, il vaut mieux prendre l’habitude de vérifier les limites de votre code.

    Au-delà de la possibilité que la mémoire soit disposée de telle sorte qu’une tentative d’écriture sur a[10] écrase réellement i , il serait également possible qu’un compilateur optimisant détermine que le test de boucle ne peut pas être atteint avec une valeur supérieure à dix sans code ayant d’abord accédé à l’élément de tableau inexistant a[10] .

    Puisqu’une tentative d’access à cet élément serait un comportement indéfini, le compilateur n’aurait aucune obligation quant à ce que le programme pourrait faire après ce point. Plus précisément, comme le compilateur n’aurait aucune obligation de générer du code pour vérifier l’indice de boucle dans tous les cas où il pourrait être supérieur à dix, il ne serait pas obligé de générer du code pour le vérifier. au lieu de cela, il pourrait supposer que le test <=10 produira toujours la vérité. Notez que cela serait vrai même si le code lisait a[10] plutôt que de l'écrire.

    Lorsque vous dépassez i==9 vous affectez zéro aux «éléments de tableau» qui se trouvent en fait au- delà du tableau , de sorte que vous écrasez d’autres données. Vous écrasez probablement la variable i , située après a[] . De cette façon, il vous suffit de remettre la variable i à zéro et de redémarrer 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 dépend à son tour d’un compilateur et de ses parameters. Il s’agit donc généralement d’un comportement non défini.

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

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

    Essayez d’atsortingbuer le 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 à la mémoire stompenant une par exemple]

    Il y a deux choses qui ne vont pas. Le int i est en fait un élément de tableau, array [10], comme vu sur la stack. Comme vous avez autorisé l’indexation à créer réellement le tableau [10] = 0, l’index de boucle, i, ne dépassera jamais 10. Faites-le for(i=0; i<10; i+=1) .

    i ++ est, comme l'appellerait K & R , un «mauvais style». Il incrémente i de la taille de i, pas de 1. i ++ est pour les maths de pointeur et i + = 1 est pour l'algèbre. Bien que cela dépende du compilateur, ce n'est pas une bonne convention pour la portabilité.