Peut-on Accéder à la mémoire d'une variable locale en dehors de sa portée?

j'ai le code suivant.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

et le code ne fait que tourner sans exception!

la sortie était 58

Comment est-ce possible? La mémoire d'une variable locale n'est-elle pas inaccessible en dehors de sa fonction?

886
demandé sur 23 revs, 14 users 27%unknown 2011-06-22 18:05:48
la source

19 ответов

Comment est-ce possible? 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 premier tiroir de la table de chevet et d'aller dormir. Tu sors le lendemain matin, mais "oublie" de rendre ta clé. Vous voler la clé!

une semaine plus tard, vous retournez à l'hôtel, ne vous enregistrez pas, entrez dans votre ancienne chambre avec votre clé volée, et regardez dans le tiroir. Votre le livre est toujours là. Étonnante!

Comment est-ce possible? Ne sont pas le contenu d'une chambre d'hôtel tiroir inaccessible si vous n'avez pas loué la chambre?

Eh bien, évidemment ce scénario peut se produire dans le monde réel pas de problème. Il n'y a pas de force mystérieuse qui provoque votre livre à disparaître lorsque vous n'êtes plus autorisé à être présent dans la salle. Il n'y a pas non plus de 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 requis pour supprimer votre livre. Tu n'as pas passé de contrat avec eux qui disait que si tu laissais des choses derrière toi, ils les déchiquetteraient pour toi. Si vous entrez 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 requis pour vous attraper en se faufilant dans. Vous n'avez pas fait un contrat avec eux qui dit "si j'essaie de me faufiler dans ma chambre plus tard, vous êtes tenus d'arrêter je."Plutôt, vous avez signé un contrat avec eux qui dit "je promets de ne pas revenir dans ma chambre plus tard", un contrat que vous avez cassé .

Dans cette situation tout peut arriver . Le livre peut être là ... tu as eu de la chance. Le livre de quelqu'un d'autre peut être là et le tien peut être dans la fournaise de l'hôtel. Quelqu'un pourrait être là quand vous entrerez, déchirant votre livre en morceaux. L'hôtel aurait pu enlever la table et réserver entièrement et remplacé par une armoire. L'ensemble de l'hôtel pourrait être sur le point d'être démoli et remplacé par un stade de football, et vous allez mourir dans une explosion pendant que vous faufiler.

vous ne savez pas ce qui va se passer; quand vous avez vérifié hors de l'hôtel et volé une clé à utiliser illégalement plus tard, vous avez abandonné le droit de vivre dans un monde prévisible et sûr parce que vous choisi d'enfreindre les règles du système.

C++ n'est pas un langage sûr . Il vous permettra joyeusement d'enfreindre 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 où vous n'êtes pas autorisé à être et fouiller dans un bureau qui pourrait même ne plus être là, C++ ne va pas vous arrêter. Des langages plus sûrs que C++ résolvent ce problème en limitant votre pouvoir -- en ayant un contrôle beaucoup plus strict sur les clés, par exemple.

Mise à jour

bonté divine, cette réponse reçoit beaucoup d'attention. (Je ne sais pas pourquoi -- j'ai considéré que c'était juste une petite analogie "amusante", mais peu importe.)

j'ai pensé qu'il pourrait être pertinent de mettre à jour ceci un peu avec quelques réflexions plus techniques.

Compilateurs sont dans l'entreprise de générer du code qui gère le stockage des données manipulées par le programme. Il ya beaucoup de différentes façons de générer du code pour gérer de mémoire, mais au fil du temps deux techniques de base ont marqué.

la première est d'avoir une sorte de zone de stockage" de longue durée "où la" durée de vie " de chaque octet dans le stockage -- c'est-à-dire la période de temps où il est valablement associé à une variable de programme -- ne peut pas être facilement prédite à l'avance. Le compilateur génère appelle un "gestionnaire de tas" qui sait comment allouer dynamiquement de la mémoire lorsque c'est nécessaire, et le récupérer quand il n'est plus nécessaire.

la seconde est d'avoir une sorte d'aire de stockage" de courte durée "où la durée de vie de chaque octet dans le stockage est bien connue, et, en particulier, les durées de vie des entrepôts suivent un modèle de" nidification". C'est-à-dire que l'attribution des variables à plus longue durée de vie des variables à courte durée de vie chevauche strictement les attributions des variables à plus courte durée de vie qui viennent après elle.

variables locales suivent ce dernier modèle; quand une méthode est entrée, ses variables locales viennent vivant. 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 commencements et des terminaisons des durées de vie des stockages associés aux variables locales peut être établi à l'avance.

pour cette raison, les variables locales sont généralement générées comme stockage sur une structure de données" pile", parce qu'une pile a la propriété que la première chose poussée sur elle va pour être la dernière chose sauté.

c'est comme si l'hôtel décidait de ne louer les chambres que de façon séquentielle, et vous ne pouvez pas partir avant que tous ceux qui ont un numéro de chambre plus élevé que vous ne l'ayez fait.

alors pensons à la pile. Dans de nombreux systèmes d'exploitation, vous obtenez une pile par thread et la pile est allouée à une certaine taille fixe. Quand vous appelez une méthode, des trucs sont poussés sur la pile. Si vous passez alors un pointeur à la pile de retour votre méthode, comme le fait l'affiche originale ici, c'est juste un pointeur vers le milieu d'un bloc de mémoire de millions d'octets entièrement valide. Dans notre analogie, vous sortez de l'hôtel; quand vous le faites, vous venez de sortir de la chambre occupée la plus haute numérotée. Si personne ne vérifie après vous, et vous retournez à votre chambre illégalement, tous vos trucs, est garanti d'être encore là dans cet hôtel particulier .

Nous utilisons des piles pour les magasins temporaires parce que ils sont vraiment bon marché et facile. Une implémentation de C++ n'est pas nécessaire pour utiliser une pile pour le stockage des locaux; elle pourrait utiliser le tas. Non, parce que ça ralentirait le programme.

une implémentation de C++ n'est pas nécessaire pour laisser les déchets que vous avez laissés sur la pile intacts afin que vous puissiez revenir pour eux plus tard illégalement; il est parfaitement légal pour le compilateur de générer du code qui retourne à zéro tout ce qui est dans la" chambre " que vous venez de quitter. Il non, car encore une fois, ça coûterait cher.

une implémentation de C++ n'est pas nécessaire pour s'assurer que lorsque la pile se rétrécit logiquement, les adresses qui étaient valides sont toujours mappées en mémoire. L'implémentation est autorisée pour dire au système d'exploitation "nous avons terminé en utilisant cette page de stack maintenant. Jusqu'à ce que je dise le contraire, émettre une exception qui détruit le processus si quelqu'un touche la page stack précédemment valide". Encore une fois, les implémentations ne font pas que parce qu'il est lent et inutile.

à la place, les implémentations vous permettent de faire des erreurs et de vous en tirer. La plupart du temps. Jusqu'à ce qu'un jour quelque chose de vraiment terrible se passe mal et que le processus explose.

C'est problématique. Il y a beaucoup de règles et il est très facile de les casser accidentellement. Je l'ai certainement fait plusieurs fois. Et pire encore, le problème ne se pose souvent que lorsque la mémoire est détectée comme corrompue des milliards de nanosecondes après le la corruption s'est produite, quand il est très difficile de comprendre qui a tout gâché.

plus de langages mémoire-sûrs résoudre ce problème en limitant votre puissance. Dans "normal" C# il n'y a tout simplement aucun moyen de prendre l'adresse d'un local et de la retourner ou de la stocker pour plus tard. Vous pouvez prendre l'adresse d'un local, mais la langue est habilement conçue de sorte qu'il est impossible de l'utiliser après la vie des fins locales. Pour prendre l'adresse d'un local et la transmettre, vous devez mettez le compilateur dans un mode spécial" dangereux", et mettez le mot" dangereux " dans votre programme, pour attirer l'attention sur le fait que vous faites probablement quelque chose de dangereux qui pourrait enfreindre les règles.

pour une lecture ultérieure:

4600
répondu Eric Lippert 2016-04-04 14:37:17
la source

ce que vous faites ici, c'est simplement lire et écrire en mémoire que utilisé pour être 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 l'a remplacé. Par conséquent, le 5 est toujours là. Dans un vrai programme, que la mémoire serait réutilisée presque immédiatement et vous briseriez quelque chose en faisant cela (bien que les symptômes ne peuvent apparaître que beaucoup plus tard!)

quand vous revenez de foo , vous dites à L'OS que vous n'utilisez plus cette mémoire et qu'elle peut être réaffectée à autre chose. Si vous avez de la chance et qu'il n'est jamais réaffecté, et que le système D'exploitation ne vous voit pas l'utiliser à nouveau, alors vous vous en sortirez avec le mensonge. Il y a des Chances que tu finisses par écrire tout ce qui finit avec cette adresse.

maintenant si vous vous demandez pourquoi le compilateur ne se plaint pas, c'est probablement parce que foo a été éliminé par optimisation. Elle va vous avertir de ce genre de chose. C suppose que vous savez ce que vous faites cependant, et techniquement vous n'avez pas violé la portée ici (il n'y a aucune référence à a lui-même en dehors de foo ), seulement les règles d'accès à la mémoire, qui ne déclenche qu'un avertissement plutôt qu'une erreur.

En bref: ce ne sera pas pour habitude de travailler, mais parfois par hasard.

262
répondu Rena 2013-10-07 23:23:41
la source

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

136
répondu msw 2010-05-19 06:33:30
la source

un petit ajout à toutes les réponses:

si vous faites quelque chose comme ça:

#include<stdio.h>
#include <stdlib.h>
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

c'est parce qu'après le retour de foo() la pile est libérée puis réutilisée par boo(). Si vous désassemblez l'exécutable, vous le verrez clairement.

72
répondu Michael 2011-06-25 18:35:20
la source

En C++, vous peut accéder à n'importe quelle adresse, mais il ne signifie pas que vous devrait . L'adresse à laquelle vous accédez n'est plus valide. Il fonctionne parce que rien d'autre brouillé la mémoire après foo retourné, mais il pourrait se planter dans de nombreuses circonstances. Essayez d'analyser votre programme avec Valgrind , ou même simplement de le compiler optimisé, et voir...

62
répondu Charles Brunet 2011-06-25 01:33:42
la source

vous ne lancez jamais une exception C++ en accédant à une mémoire invalide. Vous donnez juste un exemple de l'idée générale de faire référence à un emplacement de mémoire arbitraire. Je pourrais faire la même chose:

unsigned int q = 123456;

*(double*)(q) = 1.2;

ici je traite simplement 123456 comme l'adresse d'un double et je lui écris. Beaucoup de choses peuvent arriver:

  1. q pourrait en fait être une adresse valide d'un double, par exemple double p; q = &p; .
  2. q pourrait pointer quelque part à l'intérieur de la mémoire allouée et j'écrirai juste 8 bytes là.
  3. q points en dehors de la mémoire allouée et le gestionnaire de mémoire du système d'exploitation envoie un signal de défaillance de segmentation à mon programme, provoquant l'exécution de l'arrêt.
  4. Vous gagnez à la loterie.

la façon dont vous l'avez configuré il est un peu plus raisonnable que l'adresse retournée points dans une zone valide de mémoire, car il sera probablement juste un peu plus loin dans la pile, mais il est toujours un emplacement invalide que vous ne pouvez pas accéder d'une manière déterministe.

personne ne vérifiera automatiquement la validité sémantique des adresses mémoire comme cela pour vous pendant l'exécution normale du programme. Cependant, un débogueur en mémoire tel que valgrind le fera avec plaisir, donc vous devriez exécuter votre programme et assister aux erreurs.

59
répondu Kerrek SB 2016-06-20 18:29:18
la source

avez-vous compilé votre programme avec l'optimiseur activé ?

la fonction foo () est très 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 résultant n'est pas défini.

27
répondu gastush 2011-06-22 18:12:51
la source

Votre problème n'a rien à voir avec champ d'application . Dans le code que vous montrez, la fonction main ne voit pas les noms dans la fonction foo , de sorte que vous ne pouvez pas accéder a dans foo directement avec ce nom extérieur foo .

le problème que vous avez est de savoir pourquoi le programme ne signale pas une erreur quand vous faites référence à de la mémoire illégale. C'est parce que les standards C++ ne spécifient pas une frontière très claire entre mémoire illégale et mémoire légale. Faire référence à quelque chose dans la pile affichée provoque parfois des erreurs et parfois pas. Il dépend. Ne comptez pas sur ce comportement. Supposons qu'il en résultera toujours une erreur lorsque vous programmez, mais supposons qu'il ne signalera jamais d'erreur lorsque vous déboguez.

20
répondu Chang Peng 2011-06-23 08:45:29
la source

vous retournez juste une adresse mémoire, c'est autorisé mais probablement une erreur.

Oui, si vous essayez de déréférencement de l'adresse mémoire vous permettra d'avoir 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
}
16
répondu Brian R. Bondy 2010-05-19 16:56:09
la source

c'est classique comportement non défini cela a été discuté ici il n'y a pas deux jours -- cherchez un peu dans le site. En un mot, vous avez eu de la chance, mais n'importe quoi aurait pu arriver et votre code rend invalide l'accès à la mémoire.

14
répondu Kerrek SB 2011-06-25 01:57:55
la source

ce comportement n'est pas défini, comme Alex l'a souligné--en fait, la plupart des compilateurs préviendront de ne pas le faire, parce que c'est un moyen facile d'obtenir des accidents.

pour un exemple du genre de comportement effrayant vous êtes probablement pour obtenir, essayez cet échantillon:

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

cela imprime "y=123", mais vos résultats peuvent varier (vraiment!). Votre pointeur bouscule d'autres variables locales sans rapport.

14
répondu AHelps 2011-06-25 02:04:21
la source

cela fonctionne parce que la pile n'a pas été modifiée (encore) depuis que a y a été mis. Appelez quelques autres fonctions (qui appellent aussi d'autres fonctions) avant d'accéder à nouveau à a et vous ne serez probablement plus aussi chanceux... ;- )

13
répondu Adrian Grigore 2011-06-23 19:31:51
la source

vous avez en fait invoqué un comportement non défini.

renvoie l'adresse d'une œuvre temporaire, mais comme les œuvres temporaires sont détruites à la fin d'une fonction, les résultats de leur accès ne seront pas définis.

donc vous n'avez pas modifié a mais plutôt l'emplacement de la mémoire où a était une fois. Cette différence est très similaire à la différence entre tomber et de ne pas tomber.

13
répondu Alexander Gessler 2011-06-25 01:57:13
la source

dans les implémentations de compilateurs typiques, vous pouvez penser au code comme "Imprimer la valeur du bloc mémoire avec l'adresse que était occupé par a". De plus, si vous ajoutez une nouvelle fonction invocation à une fonction qui constitue un int local, c'est une bonne chance que la valeur de a (ou l'adresse mémoire que a utilisé pour pointer vers) change. Cela se produit parce que la pile sera écrasée avec un nouveau cadre contenant différents données.

Cependant, c'est undefined comportement et vous ne devriez pas compter sur elle pour travailler!

12
répondu larsmoa 2011-06-23 12:02:35
la source

attention à tous les Avertissements . Ne résolvez pas seulement les erreurs.

GCC affiche cet avertissement

avertissement: adresse de la variable locale 'a' retournée

c'est le pouvoir du c++. Tu devrais te soucier de la mémoire. Avec le drapeau -Werror , cet avertissement devient une erreur et maintenant vous devez le déboguer.

12
répondu sam 2015-08-28 05:42:17
la source

il peut, parce que a est une variable attribuée temporairement pour la durée de sa portée ( foo fonction). Après le retour de foo la mémoire est libre et peut être écrasé.

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

11
répondu littleadv 2011-06-25 01:57:54
la source

Les choses à corriger (?) la sortie de la console peut 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); 
}
9
répondu Mykola 2011-06-24 19:07:13
la source

après le retour d'une fonction, Tous les identificateurs sont détruits au lieu des valeurs conservées dans un emplacement de mémoire et nous ne pouvons pas localiser les valeurs sans avoir un identifiant.Mais le lieu 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 renvoyé son adresse. Et vous pouvez accéder à la valeur modifiée par cette adresse retournée.

laissez-moi prendre un exemple réel:

supposons qu'un homme cache de l'argent à un endroit et vous en indique l'emplacement. Après un certain temps, l'homme qui avait dit que l'argent de l'emplacement meurt. Mais vous avez quand même accès à cet argent caché.

1
répondu Ghulam Moinul Quadir 2017-07-19 10:07:55
la source

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 portée locale d'une fonction. C'est juste une adresse. Maintenant que vous avez invoqué la fonction 'foo', cette adresse (Emplacement mémoire) de 'a' était déjà attribuée dans la (sécurité, pour l'instant au moins) mémoire adressable 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 est là, pas nettoyée, ni perturbé / modifié par des expressions dans d'autres parties du programme (dans ce cas précis au moins). Un compilateur C / C++ ne vous empêche pas d'accéder à ce type d'accès "sale" (mais il pourrait vous avertir si vous vous en souciez). Vous pouvez utiliser (Mettre à jour) en toute sécurité n'importe quel emplacement de mémoire qui est dans le segment de données de votre instance de programme (processus) à moins que vous protégiez l'adresse par quelque moyen.

0
répondu Ayub 2017-03-08 18:25:53
la source