Frage Kann auf den Speicher einer lokalen Variablen außerhalb seines Gültigkeitsbereichs zugegriffen werden?


Ich habe den folgenden Code.

#include <iostream>

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

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

Und der Code läuft nur ohne Laufzeitausnahmen!

Die Ausgabe war 58

Wie kann es sein? Ist die Erinnerung an eine lokale Variable nicht außerhalb ihrer Funktion zugänglich?


872
2018-06-22 20:01


Ursprung


Antworten:


Wie kann es sein? Ist die Erinnerung an eine lokale Variable nicht außerhalb ihrer Funktion zugänglich?

Du mietest ein Hotelzimmer. Du legst ein Buch in die oberste Schublade des Nachttisches und gehst schlafen. Sie checken am nächsten Morgen aus, aber "vergessen", Ihren Schlüssel zurückzugeben. Du stiehlst den Schlüssel!

Eine Woche später kehren Sie ins Hotel zurück, checken nicht ein, schleichen sich mit Ihrem gestohlenen Schlüssel in Ihr altes Zimmer und schauen in die Schublade. Dein Buch ist immer noch da. Erstaunlich!

Wie kann das sein? Ist der Inhalt einer Hotelzimmerschublade nicht zugänglich, wenn Sie das Zimmer nicht gemietet haben?

Nun, offensichtlich kann dieses Szenario in der realen Welt kein Problem passieren. Es gibt keine geheimnisvolle Kraft, die Ihr Buch zum Verschwinden bringt, wenn Sie nicht mehr im Raum sein dürfen. Es gibt auch keine mysteriöse Macht, die dich daran hindert, einen Raum mit einem gestohlenen Schlüssel zu betreten.

Das Hotelmanagement ist nicht erforderlich um dein Buch zu entfernen. Du hast keinen Vertrag mit ihnen gemacht, der gesagt hat, dass, wenn du Sachen zurücklässt, sie es für dich zerstören. Wenn Sie Ihr Zimmer illegal mit einem gestohlenen Schlüssel wieder betreten, ist das Sicherheitspersonal des Hotels nicht erforderlich um dich einzuholen. Du hast keinen Vertrag mit ihnen gemacht, der gesagt hat "wenn ich versuche, mich später in mein Zimmer zu schleichen, musst du mich aufhalten." Sie haben vielmehr einen Vertrag mit ihnen unterschrieben, in dem stand: "Ich verspreche, mich später nicht in mein Zimmer zurückzuschleichen", ein Vertrag, der du bist kaputt gegangen.

In dieser Situation Alles kann passieren. Das Buch kann da sein - du hast Glück. Das Buch eines anderen kann da sein und eines kann im Ofen des Hotels sein. Jemand könnte da sein, wenn du hereinkommst und dein Buch in Stücke reißt. Das Hotel hätte den Tisch und das Buch komplett entfernen und durch einen Kleiderschrank ersetzen können. Das ganze Hotel könnte gerade abgerissen und durch ein Fußballstadion ersetzt werden, und Sie werden in einer Explosion sterben, während Sie herumschleichen.

Du weißt nicht, was passieren wird; Als du aus dem Hotel ausgecheckt bist und später einen Schlüssel gestohlen hast, hast du das Recht aufgegeben, in einer vorhersehbaren, sicheren Welt zu leben Sie entschied sich, die Regeln des Systems zu brechen.

C ++ ist keine sichere Sprache. Es wird Ihnen fröhlich erlauben, die Regeln des Systems zu brechen. Wenn Sie versuchen, etwas Illegales und Törichtes zu tun, wie in einen Raum zurückzukehren, in dem Sie nicht erlaubt sind, in einem Schreibtisch zu sitzen, der vielleicht nicht mehr da ist, wird C ++ Sie nicht aufhalten. Safer Languages ​​als C ++ lösen dieses Problem, indem Sie Ihre Macht einschränken - indem Sie z. B. viel strengere Kontrolle über Schlüssel haben.

AKTUALISIEREN

Heilige Güte, diese Antwort bekommt viel Aufmerksamkeit. (Ich bin mir nicht sicher warum - ich hielt es für eine "lustige" kleine Analogie, aber was auch immer.)

Ich dachte, es wäre vielleicht sinnvoll, das ein wenig mit ein paar technischen Gedanken zu aktualisieren.

Compiler erzeugen Code, der die Speicherung der von diesem Programm manipulierten Daten verwaltet. Es gibt viele verschiedene Möglichkeiten, Code zu generieren, um Speicher zu verwalten, aber im Laufe der Zeit haben sich zwei grundlegende Techniken etabliert.

Der erste besteht darin, einen "langlebigen" Speicherbereich zu haben, in dem die "Lebensdauer" jedes Bytes im Speicher - dh der Zeitraum, in dem es einer bestimmten Programmvariablen gültig zugeordnet ist - nicht einfach vorhergesagt werden kann von Zeit. Der Compiler generiert Aufrufe in einen "Heap-Manager", der weiß, wie er Speicher bei Bedarf dynamisch zuweist und ihn zurückfordert, wenn er nicht mehr benötigt wird.

Die zweite besteht darin, eine Art "kurzlebigen" Speicherbereich zu haben, in dem die Lebensdauer jedes Bytes im Speicher gut bekannt ist, und insbesondere die Lebensdauern der Speicher einem "verschachtelten" Muster folgen. Das heißt, die Zuweisung der langlebigsten der kurzlebigen Variablen überschneidet sich streng mit den Zuweisungen von kürzerlebigen Variablen, die danach kommen.

Lokale Variablen folgen dem letzteren Muster; Wenn eine Methode eingegeben wird, werden ihre lokalen Variablen lebendig. Wenn diese Methode eine andere Methode aufruft, werden die lokalen Variablen der neuen Methode aktiviert. Sie sind tot, bevor die lokalen Variablen der ersten Methode tot sind. Die relative Reihenfolge der Anfänge und Enden der Lebensdauern von Speichern, die lokalen Variablen zugeordnet sind, kann im Voraus ausgearbeitet werden.

Aus diesem Grund werden lokale Variablen normalerweise als Speicher in einer "Stack" -Datenstruktur generiert, da ein Stack die Eigenschaft hat, dass das erste, was darauf gepusht wird, das Letzte sein wird, was abgeklopft wird.

Es ist so, als ob das Hotel beschließt, nur Zimmer nacheinander zu vermieten, und Sie können erst auschecken, wenn alle mit einer höheren Zimmernummer als Sie ausgecheckt haben.

Lasst uns also über den Stack nachdenken. In vielen Betriebssystemen erhalten Sie einen Stack pro Thread und der Stack wird einer bestimmten festen Größe zugewiesen. Wenn Sie eine Methode aufrufen, werden Daten auf den Stapel geschoben. Wenn Sie dann einen Zeiger auf den Stapel wieder aus Ihrer Methode übergeben, wie es das ursprüngliche Poster hier tut, ist das nur ein Zeiger auf die Mitte eines völlig gültigen Millionen-Byte-Speicherblocks. In unserer Analogie checken Sie aus dem Hotel aus; Wenn du das machst, hast du gerade den höchsten Raum belegt. Wenn kein anderer nach dir eincheckt und du illegal in dein Zimmer zurückgehst, sind alle deine Sachen garantiert immer noch da in diesem besonderen Hotel.

Wir verwenden Stapel für temporäre Geschäfte, weil sie wirklich billig und einfach sind. Eine Implementierung von C ++ ist nicht erforderlich, um einen Stack zum Speichern von Locals zu verwenden. es könnte den Haufen verwenden. Tut es nicht, denn das würde das Programm langsamer machen.

Eine Implementierung von C ++ ist nicht erforderlich, um den auf dem Stapel verbliebenen Müll unberührt zu lassen, so dass Sie später illegal darauf zurückkommen können. Es ist vollkommen legal für den Compiler, Code zu erzeugen, der alles in dem "Raum", den Sie gerade geräumt haben, auf Null zurückstellt. Es ist nicht weil es wieder teuer wäre.

Eine Implementierung von C ++ ist nicht erforderlich, um sicherzustellen, dass bei einer logischen Verkleinerung des Stapels die Adressen, die zuvor gültig waren, weiterhin im Speicher zugeordnet werden. Die Implementierung darf dem Betriebssystem mitteilen, "dass wir diese Seite des Stapels jetzt verwenden. Bis ich etwas anderes sage, geben Sie eine Ausnahme aus, die den Prozess zerstört, wenn jemand die zuvor gültige Stapelseite berührt". Wiederum tun Implementierungen das nicht, weil es langsam und unnötig ist.

Stattdessen machen Implementierungen Fehler und kommen damit durch. Meistens. Bis eines Tages etwas wirklich Schreckliches schief geht und der Prozess explodiert.

Dies ist problematisch. Es gibt viele Regeln und es ist sehr einfach, sie versehentlich zu brechen. Ich habe sicherlich viele Male. Und schlimmer noch, das Problem taucht oft nur dann auf, wenn Milliarden von Nanosekunden nach der Verfälschung erkannt werden, dass der Speicher beschädigt ist, und es ist sehr schwierig herauszufinden, wer es vermasselt hat.

Mehr speichersichere Sprachen lösen dieses Problem, indem Sie Ihre Macht einschränken. In "normalen" C # gibt es einfach keine Möglichkeit, die Adresse eines lokalen zu nehmen und es zurückzugeben oder es für später zu speichern. Sie können die Adresse eines Einheimischen nehmen, aber die Sprache ist geschickt so entworfen, dass es unmöglich ist, es nach der Lebenszeit der lokalen Enden zu verwenden. Um die Adresse eines Locals zu übernehmen und zurück zu übergeben, müssen Sie den Compiler in einen speziellen "unsafe" Modus versetzen, und Setzen Sie das Wort "unsicher" in Ihr Programm, um darauf aufmerksam zu machen, dass Sie wahrscheinlich etwas Gefährliches tun, das die Regeln brechen könnte.

Für weitere Informationen:


4577
2018-06-23 05:43



Was Sie hier tun, ist einfach zu lesen und zu schreiben, dass gewöhnt an sei die Adresse von a. Jetzt wo du draußen bist fooEs ist nur ein Zeiger auf einen zufälligen Speicherbereich. Es ist einfach so, dass in Ihrem Beispiel dieser Speicherbereich existiert und nichts anderes ihn gerade benutzt. Du unterbrichst nichts, indem du es weiter verwendest, und nichts anderes hat es bisher überschrieben. deshalb, die 5 ist immer noch hier. In einem echten Programm würde dieser Speicher fast sofort wiederverwendet werden und Sie würden dadurch etwas kaputt machen (obwohl die Symptome möglicherweise erst viel später auftreten!)

Wenn du zurückkommst foo, sagen Sie dem Betriebssystem, dass Sie diesen Speicher nicht mehr verwenden und dass er anderweitig zugewiesen werden kann. Wenn du Glück hast und es nie neu zugewiesen wird, und das Betriebssystem dich nicht dabei erwischt, wirst du mit der Lüge davonkommen. Wahrscheinlichkeiten sind, obwohl Sie am Ende über alles schreiben, was mit dieser Adresse endet.

Wenn Sie sich jetzt fragen, warum sich der Compiler nicht beschwert, liegt es wahrscheinlich daran foo wurde durch Optimierung eliminiert. Normalerweise wird es dich vor solchen Dingen warnen. C geht davon aus, dass Sie wissen, was Sie tun, und technisch gesehen haben Sie hier den Geltungsbereich nicht verletzt (es gibt keinen Hinweis darauf) a selbst außerhalb von foo), nur Speicherzugriffsregeln, die nur eine Warnung statt einen Fehler auslösen.

Kurz gesagt: Das wird normalerweise nicht funktionieren, aber manchmal wird es zufällig sein.


260
2018-05-19 02:33



Weil der Speicherplatz noch nicht gestampft wurde. Zählen Sie nicht auf dieses Verhalten.


134
2018-06-22 14:15



Ein kleiner Zusatz zu allen Antworten:

wenn du so etwas machst:

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

die Ausgabe wird wahrscheinlich sein: 7

Das liegt daran, dass der Stapel nach der Rückkehr von foo () freigegeben und dann von boo () wiederverwendet wird. Wenn Sie die ausführbare Datei demontieren, werden Sie es deutlich sehen.


71
2018-06-22 14:15



In C ++, Sie kann Zugriff auf eine beliebige Adresse, aber das bedeutet nicht Sie sollte. Die Adresse, auf die Sie zugreifen, ist nicht mehr gültig. Es funktioniert weil nichts anderes die Erinnerung durcheinander brachte, nachdem foo zurückgekommen war, aber es konnte unter vielen Umständen abstürzen. Versuchen Sie, Ihr Programm mit zu analysieren Valgrindoder kompilieren es einfach optimiert und sehen ...


62
2018-06-22 14:12



Sie werfen nie eine C ++ - Ausnahme durch Zugriff auf ungültigen Speicher. Sie geben nur ein Beispiel für die generelle Idee, auf einen beliebigen Speicherort zu verweisen. Ich könnte das genauso machen:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Hier behandle ich einfach 123456 als Adresse eines Double und schreibe darauf. Eine Menge Dinge könnten passieren:

  1. q könnte tatsächlich eine gültige Adresse eines Doppelten sein, z.B. double p; q = &p;.
  2. q könnte irgendwo innerhalb zugewiesenen Speicher zeigen und ich überschreibe nur 8 Bytes dort.
  3. q Punkte außerhalb des zugewiesenen Speichers und der Speichermanager des Betriebssystems sendet ein Segmentierungsfehlersignal an mein Programm, wodurch die Laufzeit ihn beendet.
  4. Sie gewinnen im Lotto.

Die Art, wie Sie es einrichten, ist ein wenig vernünftiger, dass die zurückgegebene Adresse in einen gültigen Speicherbereich zeigt, da es wahrscheinlich nur etwas weiter unten im Stapel liegt, aber es ist immer noch ein ungültiger Speicherort, auf den Sie nicht zugreifen können deterministische Mode.

Niemand wird automatisch die semantische Gültigkeit von Speicheradressen überprüfen, die für Sie während der normalen Programmausführung gelten. Ein Speicher-Debugger wie z valgrind macht das gerne, also solltest du dein Programm durchspielen und die Fehler mit ansehen.


58
2018-06-23 04:45



Haben Sie Ihr Programm mit aktiviertem Optimierer kompiliert?

Die Funktion foo () ist ziemlich einfach und könnte im resultierenden Code inline / ersetzt worden sein.

Aber ich stimme mit Mark B überein, dass das resultierende Verhalten undefiniert ist.


27
2018-05-19 02:33



Dein Problem hat nichts damit zu tun Umfang. Im Code zeigen Sie die Funktion an main sieht die Namen in der Funktion nicht foo, so dass du nicht darauf zugreifen kannst a in foo direkt mit Dies Name außerhalb foo.

Das Problem, das Sie haben, ist, warum das Programm keinen Fehler beim Verweisen auf illegalen Speicher signalisiert. Dies liegt daran, dass C ++ - Standards keine sehr klare Grenze zwischen illegalem Speicher und legalem Speicher angeben. Verweisen auf etwas in herausgefallenen Stapel verursacht manchmal Fehler und manchmal nicht. Es kommt darauf an. Zählen Sie nicht auf dieses Verhalten. Nehmen Sie an, dass es bei der Programmierung immer zu Fehlern führen wird, gehen Sie jedoch davon aus, dass es beim Debuggen niemals einen Fehler anzeigt.


20
2018-06-24 21:57



Sie geben nur eine Speicheradresse zurück, es ist aber wahrscheinlich ein Fehler erlaubt.

Ja, wenn Sie versuchen, die Speicheradresse zu dereferenzieren, haben Sie ein undefiniertes Verhalten.

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
2018-06-24 22:04



Das ist klassisch undefiniertes Verhalten das wurde hier vor zwei Tagen nicht besprochen - suche die Seite ein wenig durch. Kurz gesagt, Sie hatten Glück, aber irgendetwas hätte passieren können und Ihr Code macht ungültigen Zugriff auf Speicher.


14
2018-06-23 15:31



Dieses Verhalten ist undefiniert, worauf Alex hingewiesen hat - tatsächlich warnen die meisten Compiler davor, dies zu tun, weil es eine einfache Möglichkeit ist, Abstürze zu bekommen.

Für ein Beispiel für die Art von unheimlichem Verhalten bist du wahrscheinlich um zu bekommen, probiere dieses Beispiel aus:

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

Dies gibt "y = 123" aus, aber Ihre Ergebnisse können variieren (wirklich!). Ihr Zeiger ist über andere, nicht verwandte lokale Variablen verblüffend.


14
2018-06-24 21:57