Question La mémoire d'une variable locale peut-elle être accédée 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 fonctionne juste sans exceptions d'exécution!

La sortie était 58

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


872
2018-06-22 20:01


origine


Réponses:


Comment ça peut ê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 chevet et vous couchez. Vous partez le lendemain matin, mais "oubliez" de rendre votre clé. Tu voles la clé!

Une semaine plus tard, vous revenez à l'hôtel, ne vous présentez pas, vous faufilez dans votre ancienne chambre avec votre clé volée et regardez dans le tiroir. Votre livre est toujours là. Étonnant!

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

Bien, évidemment ce scénario peut se produire dans le monde réel aucun problème. Il n'y a pas de force mystérieuse qui fait disparaître votre livre quand vous n'êtes plus autorisé à être dans la pièce. 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 Champs obligatoires pour retirer votre livre. Vous n'avez pas passé un contrat avec eux pour leur dire que si vous laissez des choses derrière vous, ils vous le détruiront. 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 Champs obligatoires pour vous attraper en se faufilant. Vous n'avez pas passé un contrat avec eux qui a dit "si j'essaie de me faufiler dans ma 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 dans ma chambre plus tard", un contrat qui vous avez cassé.

Dans cette situation tout peut arriver. Le livre peut être là - vous avez eu de la chance. Le livre de quelqu'un d'autre peut être là et le vôtre pourrait être dans le four de l'hôtel. Quelqu'un pourrait être là quand vous entrerez, déchirant votre livre. L'hôtel aurait pu enlever la table et le livre entièrement et l'a remplacé par une armoire. Tout 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 vous faufilez.

Vous ne savez pas ce qui va se passer; quand vous avez quitté l'hôtel et avez volé une clé pour l'utiliser illégalement plus tard, vous avez renoncé au droit de vivre dans un monde prévisible et sûr, car toi a choisi de casser les règles du système.

C ++ n'est pas une langue sûre. Il vous permettra joyeusement d'enfreindre les règles du système. Si vous essayez de faire quelque chose d'illégal et d'idiot comme de retourner dans une pièce, vous n'êtes pas autorisé à entrer et à fouiller dans un bureau qui pourrait ne plus être là, C ++ ne vous arrêtera pas. Des langages plus sûrs que C ++ résolvent ce problème en limitant votre puissance - en ayant par exemple un contrôle beaucoup plus strict sur les touches.

METTRE À JOUR

Mon Dieu, 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.)

J'ai pensé qu'il serait peut-être pertinent de mettre à jour ce point avec quelques idées plus techniques.

Les compilateurs ont pour tâche de générer du code qui gère le stockage des données manipulées par ce programme. Il y a beaucoup de façons différentes de générer du code pour gérer la mémoire, mais avec le temps, deux techniques de base se sont enracinées.

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

La seconde est d'avoir une sorte de zone 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 stockages suivent un modèle de "nidification". C'est-à-dire que l'allocation des variables à vie courte les plus anciennes chevauche strictement les allocations des variables à plus courte durée de vie qui suivent.

Les variables locales suivent le 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 deviennent vivantes. Ils seront morts avant que les variables locales de la première méthode ne soient mortes. L'ordre relatif des débuts et des fins de vies des stockages associés aux variables locales peut être élaboré à 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 "empilée", car une pile a la propriété que la première chose qui y est poussée sera la dernière chose sauté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 plus élevé que vous avez quitté.

Alors, pensons à la pile. Dans de nombreux systèmes d'exploitation, vous obtenez une pile par thread et la pile est allouée pour avoir une certaine taille fixe. Lorsque vous appelez une méthode, la substance est poussée sur la pile. Si vous passez ensuite un pointeur à la pile hors de votre méthode, comme le fait l'affiche originale ici, c'est juste un pointeur au milieu d'un bloc de mémoire d'un million d'octets entièrement valide. Dans notre analogie, vous vérifiez hors de l'hôtel; Quand vous le faites, vous venez de quitter la pièce la plus occupée. Si personne d'autre ne s'enregistre après vous, et que vous retournez dans votre chambre illégalement, tout votre matériel sera toujours là dans cet hôtel particulier.

Nous utilisons des piles pour les magasins temporaires, car 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; 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 laisser intacte la corbeille laissée sur la pile afin que vous puissiez y revenir plus tard illégalement; Il est parfaitement légal pour le compilateur de générer du code qui ramène à zéro tout ce qui se trouve dans la "pièce" que vous venez de quitter. Ce n'est pas parce que, encore une fois, cela coûterait cher.

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

Au lieu de cela, les implémentations vous permettent de faire des erreurs et de s'en tirer avec. La plupart du temps. Jusqu'à un jour, quelque chose de vraiment horrible 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 l'a gâché.

Des langues plus sûres pour la mémoire résolvent 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 le retourner ou de le stocker pour plus tard. Vous pouvez prendre l'adresse d'un local, mais la langue est intelligemment conçue de sorte qu'il est impossible de l'utiliser après la fin de la vie locale. Afin de prendre l'adresse d'un local et de le renvoyer, vous devez mettre le compilateur dans un mode spécial "non sécurisé", 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 plus de lecture:


4577
2018-06-23 05:43



Ce que vous faites ici, c'est simplement lire et écrire en mémoire habitué ê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 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, la 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 ceci (bien que les symptômes n'apparaissent que beaucoup plus tard!)

Quand vous revenez de foo, vous dites au système d'exploitation 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 que vous ne la réattribuez jamais, et que le système d'exploitation ne vous attrape plus, vous vous retrouvez avec le mensonge. Les chances sont que vous finirez par écrire sur 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 l'optimisation. Il vous avertira généralement de ce genre de chose. C suppose que vous savez ce que vous faites, et techniquement vous n'avez pas violé la portée ici (il n'y a pas de référence à a lui-même en dehors de foo), uniquement les règles d'accès à la mémoire, qui déclenchent uniquement un avertissement plutôt qu'une erreur.

En bref: cela ne fonctionnera généralement pas, mais le fera parfois par hasard.


260
2018-05-19 02:33



Parce que l'espace de stockage n'a pas encore été écrasé. Ne comptez pas sur ce comportement.


134
2018-06-22 14:15



Un petit plus à 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

En effet, 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.


71
2018-06-22 14:15



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


62
2018-06-22 14:12



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 j'écris dessus. Un certain nombre de choses pourraient arriver:

  1. q pourrait en fait être une adresse valide d'un double, par ex. double p; q = &p;.
  2. q pourrait pointer quelque part dans la mémoire allouée et je viens d'écraser 8 octets là-bas.
  3. q des 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éfaut de segmentation à mon programme, provoquant l'arrêt du runtime.
  4. Vous gagnez la loterie.

La façon dont vous l'avez configuré est un peu plus raisonnable que l'adresse retournée pointe vers une zone de mémoire valide, car elle sera probablement un peu plus loin dans la pile, mais c'est toujours un emplacement invalide que vous ne pouvez pas accéder dans un mode déterministe.

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


58
2018-06-23 04:45



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

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

Mais j'accepte avec Mark B que le comportement résultant est indéfini.


27
2018-05-19 02:33



Votre problème n'a rien à voir avec portée. Dans le code que vous montrez, la fonction main ne voit pas les noms dans la fonction foo, donc vous ne pouvez pas accéder a à foo directement avec ce nom dehors foo.

Le problème que vous avez est pourquoi le programme ne signale pas une erreur lors du référencement de la mémoire illégale. C'est parce que les normes C ++ ne spécifient pas une frontière très nette entre la mémoire illégale et la mémoire légale. Référencer quelque chose dans une pile déployée provoque parfois des erreurs et parfois non. Ça dépend. Ne comptez pas sur ce comportement. Supposons qu'il en résulte toujours une erreur lors de la programmation, mais supposons qu'il ne signalera jamais d'erreur lors du débogage.


20
2018-06-24 21:57