La mémoire d’une variable locale peut-elle être accessible en dehors de sa scope?

J’ai le code suivant.

#include  int * foo() { int a = 5; return &a; } int main() { int* p = foo(); std::cout << *p; *p = 8; std::cout << *p; } 

Et le code est juste en cours d’exécution sans exceptions à l’exécution!

La sortie était de 58

Comment peut-il être? La mémoire d’une variable locale n’est-elle pas inaccessible en dehors de sa fonction?

    Comment peut-il être? La mémoire d’une variable locale n’est-elle pas inaccessible en dehors de sa fonction?

    Vous louez une chambre d’hôtel. Vous mettez un livre dans le tiroir du haut de la table de nuit et vous vous couchez. Vous partez le lendemain matin, mais “oubliez” de rendre votre clé. Vous volez la clé!

    Une semaine plus tard, vous rentrez à l’hôtel, ne vous enregistrez pas, vous glissez dans votre ancienne chambre avec votre clé volée et regardez dans le tiroir. Votre livre est toujours là. Étonnant!

    Comment ça peut être? Le contenu d’un tiroir de chambre d’hôtel n’est-il pas inaccessible si vous n’avez pas loué la chambre?

    Eh bien, évidemment, ce scénario peut se produire dans le monde réel sans aucun problème. Il n’y a pas de force mystérieuse qui fait disparaître votre livre lorsque vous n’êtes plus autorisé à être dans la pièce. Il n’y a pas non plus une force mystérieuse qui vous empêche d’entrer dans une pièce avec une clé volée.

    La direction de l’hôtel n’est pas obligée de retirer votre livre. Vous n’avez pas conclu de contrat avec eux pour dire que si vous laissez des choses derrière vous, ils le déchireront pour vous. Si vous rentrez illégalement dans votre chambre avec une clé volée pour la récupérer, le personnel de sécurité de l’hôtel n’est pas obligé de vous surprendre. Vous n’avez pas conclu de contrat avec eux pour dire “si j’essayais de revenir dans ma chambre”. chambre plus tard, vous devez m’arrêter. ” Au contraire, vous avez signé un contrat avec eux qui disait: “Je promets de ne pas rentrer plus tard dans ma chambre”, un contrat que vous avez rompu .

    Dans cette situation, tout peut arriver . Le livre peut être là – vous avez de la chance. Le livre de quelqu’un d’autre peut être là et le vôtre pourrait être dans la fournaise de l’hôtel. Quelqu’un pourrait être là quand vous entrez, déchirant votre livre en morceaux. L’hôtel aurait pu enlever complètement la table et le livre et l’a remplacé par une armoire. Tout l’hôtel pourrait être démoli et remplacé par un stade de football, et vous allez mourir dans une explosion pendant que vous vous faufilez.

    Vous ne savez pas ce qui va se passer; Lorsque vous avez quitté l’hôtel et volé une clé pour l’utiliser illégalement plus tard, vous avez abandonné le droit de vivre dans un monde prévisible et sûr parce que vous avez choisi de ne pas respecter les règles du système.

    C ++ n’est pas un langage sûr . Cela vous permettra de briser les règles du système. Si vous essayez de faire quelque chose d’illégal et de stupide, comme retourner dans une pièce que vous n’êtes pas autorisé à visiter et fouiller dans un bureau qui pourrait ne plus être là, le C ++ ne vous arrêtera pas. Des langages plus sûrs que le C ++ résolvent ce problème en limitant votre puissance – en ayant un contrôle beaucoup plus ssortingct sur les clés, par exemple.

    METTRE À JOUR

    Bonté sainte, cette réponse attire beaucoup d’attention. (Je ne sais pas pourquoi – je considérais que c’était juste une petite analogie “amusante”, mais peu importe.)

    Je pensais que cela pourrait être pertinent de mettre à jour cela un peu avec quelques reflections plus techniques.

    Les compilateurs sont en train de générer du code qui gère le stockage des données manipulées par ce programme. Il existe de nombreuses manières différentes de générer du code pour gérer la mémoire, mais au fil du temps, deux techniques de base se sont imposées.

    La première consiste à prévoir une sorte de zone de stockage “à longue durée de vie” où la “durée de vie” de chaque octet du stockage – c’est-à-dire la période pendant laquelle de temps. Le compilateur génère des appels dans un “gestionnaire de segments de mémoire” qui sait allouer dynamicment le stockage lorsque cela est nécessaire et le récupérer lorsqu’il n’est plus nécessaire.

    La seconde consiste à avoir une sorte de zone de stockage “à durée de vie courte” où la durée de vie de chaque octet dans le stockage est bien connue, et, en particulier, la durée de vie des stockages suit un modèle “d’imbrication”. En d’autres termes, l’allocation de la durée de vie la plus longue des variables à court terme chevauche ssortingctement les allocations des variables à durée de vie plus courte qui la suivent.

    Les variables locales suivent ce dernier modèle; lorsqu’une méthode est entrée, ses variables locales prennent vie. Lorsque cette méthode appelle une autre méthode, les variables locales de la nouvelle méthode prennent vie. Ils seront morts avant que les variables locales de la première méthode soient mortes. L’ordre relatif des débuts et des fins de vie des stockages associés aux variables locales peut être déterminé à l’avance.

    Pour cette raison, les variables locales sont généralement générées en tant que stockage sur une structure de données “stack”, car une stack a la propriété que la première chose qui y est lancée sera la dernière à être lancée.

    C’est comme si l’hôtel décidait de ne louer que des chambres de manière séquentielle, et vous ne pouvez pas vérifier jusqu’à ce que tout le monde avec un numéro de chambre supérieur à celui que vous avez vérifié.

    Alors réfléchissons à la stack. Dans de nombreux systèmes d’exploitation, vous obtenez une stack par thread et la stack est allouée à une certaine taille fixe. Lorsque vous appelez une méthode, des éléments sont envoyés sur la stack. Si vous transmettez ensuite un pointeur à la stack en dehors de votre méthode, comme le fait l’affiche originale, il ne s’agit que d’un pointeur au milieu d’un bloc de mémoire de millions d’octets entièrement valide. Dans notre analogie, vous quittez l’hôtel; Lorsque vous le faites, vous venez de quitter la pièce occupée la plus élevée. Si personne ne se présente après vous et que vous rentrez dans votre chambre illégalement, toutes vos affaires sont toujours présentes dans cet hôtel particulier .

    Nous utilisons des stacks pour les magasins temporaires car ils sont vraiment bon marché et faciles. Une implémentation de C ++ n’est pas requirejse pour utiliser une stack pour le stockage des locaux; il pourrait utiliser le tas. Ce n’est pas le cas, car cela ralentirait le programme.

    Une implémentation de C ++ n’est pas nécessaire pour ne pas toucher à la poubelle que vous avez laissée sur la stack afin que vous puissiez la récupérer plus tard illégalement; Il est parfaitement légal que le compilateur génère du code qui revient à zéro dans la “pièce” que vous venez de libérer. Ce n’est pas parce que, encore une fois, ce serait cher.

    Une implémentation de C ++ n’est pas nécessaire pour garantir que, lorsque la stack diminue de manière logique, les adresses qui étaient valides sont toujours mappées dans la mémoire. L’implémentation est autorisée à indiquer au système d’exploitation “nous avons fini d’utiliser cette page de stack. Jusqu’à ce que je dise le contraire, émettez une exception qui détruit le processus si quelqu’un touche la page de stack précédemment valide”. Encore une fois, les implémentations ne le font pas réellement car elles sont lentes et inutiles.

    Au lieu de cela, les implémentations vous permettent de faire des erreurs et de vous en sortir. La plupart du temps. Jusqu’au jour où quelque chose de vraiment terrible se passe mal et le processus explose.

    C’est problématique. Il y a beaucoup de règles et il est très facile de les casser accidentellement. J’ai certainement plusieurs fois. Et pire encore, le problème ne survient souvent que lorsque la mémoire est détectée comme étant corrompue, des milliards de nanosecondes après la corruption, alors qu’il est très difficile de déterminer qui a tout gâché.

    Plus de langages sécurisés par la mémoire résolvent ce problème en limitant votre consommation d’énergie. Dans “normal” C #, il n’y a tout simplement aucun moyen de prendre l’adresse d’un local et de la renvoyer ou de la stocker pour plus tard. Vous pouvez prendre l’adresse d’un local, mais le langage est intelligemment conçu pour qu’il soit impossible de l’utiliser après la fin de la vie locale. Pour prendre l’adresse d’un local et la renvoyer, vous devez mettre le compilateur dans un mode “dangereux” et mettre le mot “dangereux” dans votre programme pour attirer votre attention sur le fait que vous le faites probablement quelque chose de dangereux qui pourrait enfreindre les règles.

    Pour plus de lecture:

    Ce que vous faites ici est simplement de lire et d’écrire dans la mémoire qui était jadis l’adresse de a . Maintenant que vous êtes en dehors de foo , c’est juste un pointeur vers une zone de mémoire aléatoire. Il se trouve que dans votre exemple, cette zone de mémoire existe et que rien d’autre ne l’utilise pour le moment. Vous ne cassez rien en continuant à l’utiliser, et rien d’autre ne l’a encore écrasé. Par conséquent, le 5 est toujours là. Dans un vrai programme, cette mémoire serait réutilisée presque immédiatement et vous casseriez quelque chose en faisant cela (bien que les symptômes puissent n’apparaître que beaucoup plus tard!)

    Lorsque vous revenez de foo , vous dites au système d’exploitation que vous n’utilisez plus cette mémoire et qu’il peut être réaffecté à autre chose. Si vous avez de la chance et que cela ne se réatsortingbue jamais, et que le système d’exploitation ne vous intercepte plus, alors vous allez vous en sortir. Il y a des chances que vous finissiez par écrire quoi que ce soit d’autre avec cette adresse.

    Maintenant, si vous vous demandez pourquoi le compilateur ne se plaint pas, c’est probablement que foo été éliminé par optimisation. Il vous avertira généralement de ce genre de choses. C suppose que vous savez ce que vous faites, et techniquement, vous n’avez pas violé la scope ici (il n’y a aucune référence à elle a même en dehors de foo ), seules les règles d’access à la mémoire déclenchent un avertissement plutôt qu’une erreur.

    En bref: cela ne fonctionnera pas habituellement, mais parfois par hasard.

    Parce que l’espace de stockage n’a pas encore été piétiné. Ne comptez pas sur ce comportement.

    Un petit ajout à toutes les réponses:

    si vous faites quelque chose comme ça:

     #include #include  int * foo(){ int a = 5; return &a; } void boo(){ int a = 7; } int main(){ int * p = foo(); boo(); printf("%d\n",*p); } 

    la sortie sera probablement: 7

    En effet, après son retour de foo (), la stack est libérée puis réutilisée par boo (). Si vous démontez l’exécutable, vous le verrez clairement.

    En C ++, vous pouvez accéder à n’importe quelle adresse, mais cela ne signifie pas que vous devriez . L’adresse à laquelle vous accédez n’est plus valide. Cela fonctionne parce que rien d’autre n’a brouillé la mémoire après le retour de foo, mais cela pourrait tomber en panne dans de nombreuses circonstances. Essayez d’parsingr votre programme avec Valgrind , ou même simplement le comstackr optimisé, et voyez …

    Vous ne lancez jamais une exception C ++ en accédant à une mémoire non valide. Vous donnez juste un exemple de l’idée générale de référencer un emplacement de mémoire arbitraire. Je pourrais faire la même chose comme ceci:

     unsigned int q = 123456; *(double*)(q) = 1.2; 

    Ici, je traite simplement 123456 comme l’adresse d’un double et y écris. Un certain nombre de choses peuvent arriver:

    1. q pourrait en fait être une adresse valide d’un double, par exemple double p; q = &p; double p; q = &p; .
    2. q peut pointer quelque part à l’intérieur de la mémoire allouée et je ne remplace que 8 octets.
    3. q pointe en dehors de la mémoire allouée et le gestionnaire de mémoire du système d’exploitation envoie un signal d’erreur de segmentation à mon programme, provoquant la fermeture du moteur d’exécution.
    4. Vous gagnez à la loterie.

    La façon dont vous le configurez est un peu plus raisonnable que l’adresse renvoyée pointe dans une zone de mémoire valide, car il se trouvera probablement un peu plus bas dans la stack, mais il s’agit toujours d’un emplacement invalide auquel vous ne pouvez pas accéder. mode déterministe.

    Personne ne vérifiera automatiquement la validité sémantique des adresses mémoire comme celle-ci lors de l’exécution normale du programme. Cependant, un débogueur de mémoire tel que valgrind fera un plaisir de le faire. Par conséquent, vous devez exécuter votre programme et voir les erreurs.

    Avez-vous compilé votre programme avec l’optimiseur activé?

    La fonction foo () est assez simple et pourrait avoir été insérée / remplacée dans le code résultant.

    Mais je suis d’accord avec Mark B que le comportement qui en résulte n’est pas défini.

    Votre problème n’a rien à voir avec la scope . Dans le code que vous affichez, la fonction main ne voit pas les noms dans la fonction foo , vous ne pouvez donc pas accéder directement à a in foo avec ce nom en dehors de foo .

    Le problème que vous rencontrez est la raison pour laquelle le programme ne signale pas une erreur lors du référencement de mémoire illégale. Cela est dû au fait que les normes C ++ ne spécifient pas une frontière très claire entre la mémoire illégale et la mémoire légale. Référencer quelque chose dans une stack éclatée provoque parfois des erreurs et parfois pas. Ça dépend. Ne comptez pas sur ce comportement. Supposons que cela provoquera toujours une erreur lors de la programmation, mais supposons qu’elle ne signalera jamais d’erreur lors du débogage.

    Vous retournez simplement une adresse mémoire, elle est autorisée mais probablement une erreur.

    Oui, si vous essayez de déréférencer cette adresse mémoire, vous aurez un comportement indéfini.

     int * ref () { int tmp = 100; return &tmp; } int main () { int * a = ref(); //Up until this point there is defined results //You can even print the address returned // but yes probably a bug cout < < *a << endl;//Undefined results } 

    C’est un comportement indéfini classique qui a été discuté ici il n’y a pas deux jours – effectuez quelques recherches sur le site. En un mot, vous avez eu de la chance, mais tout a pu se passer et votre code rend l’access à la mémoire non valide.

    Ce comportement est indéfini, comme Alex l’a fait remarquer – en fait, la plupart des compilateurs préviennent cela, car il s’agit d’un moyen facile d’obtenir des pannes.

    Pour un exemple du type de comportement fantasmagorique que vous êtes susceptible d’avoir, essayez cet exemple:

     int *a() { int x = 5; return &x; } void b( int *c ) { int y = 29; *c = 123; cout < < "y=" << y << endl; } int main() { b( a() ); return 0; } 

    Ceci imprime "y = 123", mais vos résultats peuvent varier (vraiment!). Votre pointeur claque d'autres variables locales non liées.

    Cela fonctionne parce que la stack n’a pas encore été modifiée depuis qu’un Appelez quelques autres fonctions (qui appellent également d’autres fonctions) avant d’accéder à a autre et vous n’aurez probablement plus de chance … 😉

    Vous avez en fait appelé un comportement indéfini.

    En renvoyant l’adresse d’une œuvre temporaire, mais comme les tâches temporaires sont détruites à la fin d’une fonction, les résultats de leur access seront indéfinis.

    Donc, vous n’avez pas modifié mais plutôt l’emplacement de mémoire où se trouvait a fois. Cette différence est très similaire à la différence entre crash et non crash.

    Dans les implémentations classiques du compilateur, vous pouvez considérer le code comme “imprimer la valeur du bloc de mémoire avec l’adresse qui était occupée par un”. De même, si vous ajoutez un nouvel appel de fonction à une fonction qui contient un int local, il est probable que la valeur d’ a adresse (ou de l’adresse de mémoire utilisée pour pointer) change. Cela se produit car la stack sera remplacée par une nouvelle image contenant des données différentes.

    Cependant, c’est un comportement indéfini et vous ne devriez pas vous y fier pour travailler!

    Faites attention à tous les avertissements. Ne résolvez pas seulement les erreurs.
    GCC montre cet avertissement

    warning: adresse de la variable locale ‘a’ renvoyée

    C’est la puissance de C ++. Vous devriez vous soucier de la mémoire. Avec l’indicateur -Werror , cet avertissement devient une erreur et vous devez maintenant le déboguer.

    Cela peut, car a est une variable allouée temporairement pour la durée de vie de sa scope (fonction foo ). Après votre retour de foo la mémoire est libre et peut être écrasée.

    Ce que vous faites est décrit comme un comportement non défini . Le résultat ne peut pas être prédit.

    Les choses avec une sortie de console correcte (?) Peuvent changer radicalement si vous utilisez :: printf mais pas cout. Vous pouvez jouer avec le débogueur dans le code ci-dessous (testé sur x86, 32 bits, MSVisual Studio):

     char* foo() { char buf[10]; ::strcpy(buf, "TEST”); return buf; } int main() { char* s = foo(); //place breakpoint & check 's' varialbe here ::printf("%s\n", s); } 

    Après retour d’une fonction, tous les identificateurs sont détruits à la place des valeurs conservées dans un emplacement mémoire et nous ne pouvons pas localiser les valeurs sans avoir d’identifiant. Mais cet emplacement contient toujours la valeur stockée par la fonction précédente.

    Donc, ici, la fonction foo() renvoie l’adresse de a et a est détruite après avoir retourné son adresse. Et vous pouvez accéder à la valeur modifiée via cette adresse renvoyée.

    Permettez-moi de prendre un exemple concret:

    Supposons qu’un homme cache de l’argent à un endroit et vous indique l’emplacement. Après un certain temps, l’homme qui vous avait dit l’argent mourait. Mais vous avez toujours access à cet argent caché.

    C’est une façon «sale» d’utiliser les adresses mémoire. Lorsque vous retournez une adresse (pointeur), vous ne savez pas si elle appartient à la scope locale d’une fonction. C’est juste une adresse. Maintenant que vous avez appelé la fonction ‘foo’, cette adresse (emplacement de mémoire) de ‘a’ y était déjà allouée dans la mémoire adressable (en toute sécurité, pour le moment au moins) de votre application (processus). Après le retour de la fonction ‘foo’, l’adresse de ‘a’ peut être considérée comme ‘sale’, mais elle n’y est pas nettoyée, ni perturbée / modifiée par des expressions dans d’autres parties du programme (dans ce cas du moins). Le compilateur AC / C ++ ne vous empêche pas d’accéder à un tel access (peut vous avertir si vous le souhaitez). Vous pouvez utiliser en toute sécurité (mettre à jour) tout emplacement de mémoire se trouvant dans le segment de données de votre instance de programme (processus) à moins que vous ne protégiez l’adresse par un moyen quelconque.

    C’est définitivement un problème de timing! L ‘object sur lequel pointe le pointeur p est “programmé” pour être détruit quand il sort de la scope de foo . Cette opération ne se produit cependant pas immédiatement, mais plutôt un certain nombre de cycles de processeur plus tard. Que ce soit un comportement indéfini ou que C ++ soit en train de faire du pré-nettoyage en arrière-plan, je ne sais pas.

    Si vous insérez un appel à la fonction de sleep de votre système d’exploitation entre l’appel à foo et les instructions de cout , en faisant attendre le programme une seconde avant de déréférencer le pointeur, vous remarquerez que les données ont disparu ! Regardez mon exemple:

     #include  #include  using namespace std; class myClass { public: myClass() : i{5} { cout < < "myClass ctor" << endl; } ~myClass() { cout << "myClass dtor" << endl; } int i; }; myClass* foo() { myClass a; return &a; } int main() { bool doSleep{false}; auto p = foo(); if (doSleep) sleep(1); cout << p->i < < endl; p->i = 8; cout < < p->i < < endl; } 

    (Notez que j'ai utilisé la fonction de sleep depuis unistd.h , qui n'est présente que sur les systèmes Unix, vous devrez donc la remplacer par Sleep(1000) et Windows.h si vous êtes sous Windows.)

    J'ai remplacé votre int par une classe, donc je peux voir exactement quand le destructeur est appelé.

    La sortie de ce code est la suivante:

     myClass ctor myClass dtor 5 8 

    Toutefois, si vous modifiez doSleep à true :

     myClass ctor myClass dtor 0 8 

    Comme vous pouvez le voir, l'object qui est supposé être détruit est réellement détruit, mais je suppose qu'il y a des instructions de pré-nettoyage qui doivent être exécutées avant qu'un object (ou juste une variable) soit détruit. est toujours accessible pour une courte période de temps (cependant, il n'y a pas de garantie pour cela bien sûr, donc n'écrivez pas de code basé sur cela).

    This is very weird, since the destructor is called immediately upon exiting the scope, however, the actual destruction is slightly delayed.

    I never really read the part of the official ISO C++ standard that specifies this behavior, but it might very well be, that the standard only promises that your data will be destroyed once it goes out of scope, but it doesn't say anything about this happening immediately, before any other instruction is executed. If this is the case, than this behavior is completely fine, and people are just misunderstanding the standard.

    Or another cause could be cheeky comstackrs that don't follow the standard properly. Actually this wouldn't be the only case where comstackrs trade a little bit of standard conformance for extra performance!

    Whatever the cause of this is, it's clear that the data IS destroyed, just not immediately.