it-swarm.com.de

Kann auf den Speicher einer lokalen Variablen außerhalb ihres 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 der Speicher einer lokalen Variablen außerhalb ihrer Funktion nicht unzugänglich?

974
Avi Shukron

Wie kann es sein? Ist der Speicher einer lokalen Variablen außerhalb ihrer Funktion nicht unzugänglich?

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

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

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

Nun, natürlich kann dieses Szenario in der realen Welt problemlos passieren. Es gibt keine mysteriöse Kraft, die dazu führt, dass Ihr Buch verschwindet, wenn Sie nicht mehr berechtigt sind, sich im Raum aufzuhalten. Es gibt auch keine mysteriöse Kraft, die Sie daran hindert, einen Raum mit einem gestohlenen Schlüssel zu betreten.

Die Hotelleitung ist nicht verpflichtet , Ihr Buch zu entfernen. Sie haben mit ihnen keinen Vertrag geschlossen, der besagt, dass sie das Zeug für Sie zerkleinern, wenn Sie es zurücklassen. Wenn Sie Ihr Zimmer mit einem gestohlenen Schlüssel illegal wieder betreten, um ihn zurückzubekommen, ist das Sicherheitspersonal des Hotels nicht verpflichtet , Sie beim Einschleichen zu erwischen. Sie haben keinen Vertrag mit abgeschlossen diejenigen, die sagten: "Wenn ich mich später in mein Zimmer zurückschleichen will, musst du mich aufhalten." Vielmehr haben Sie mit ihnen einen Vertrag unterzeichnet, in dem stand: "Ich verspreche, mich nicht später in mein Zimmer zurückzuziehen", einen Vertrag, den Sie gebrochen haben .

In dieser Situation kann alles passieren . Das Buch kann da sein - Sie haben Glück. Jemand anderes Buch kann da sein und Ihr Buch könnte im Ofen des Hotels sein. Jemand könnte da sein, wenn Sie hereinkommen und Ihr Buch in Stücke reißen. Das Hotel hätte den Tisch und das Buch komplett entfernen und durch einen Kleiderschrank ersetzen können. Das gesamte Hotel könnte gerade abgerissen und durch ein Fußballstadion ersetzt werden, und Sie werden bei einer Explosion sterben, während Sie sich herumschleichen.

Sie wissen nicht, was passieren wird. Als Sie aus dem Hotel ausgecheckt und einen Schlüssel gestohlen haben, um ihn später illegal zu verwenden, haben Sie das Recht auf ein Leben in einer vorhersehbaren, sicheren Welt aufgegeben, weil Sie beschlossen haben, die Regeln der System.

C++ ist keine sichere Sprache . Es wird Ihnen munter erlauben, die Regeln des Systems zu brechen. Wenn Sie versuchen, etwas Illegales und Dummes zu tun, wie in einen Raum zurückzukehren, in dem Sie keine Berechtigung mehr haben, und einen Schreibtisch zu durchsuchen, der möglicherweise nicht mehr vorhanden ist, wird C++ Sie nicht aufhalten. Sicherere Sprachen als C++ lösen dieses Problem, indem Sie Ihre Leistungsfähigkeit einschränken, indem Sie beispielsweise die Tasten viel strenger steuern.

AKTUALISIEREN

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

Ich dachte, es wäre vielleicht wichtig, dies ein wenig mit ein paar weiteren technischen Überlegungen zu aktualisieren.

Compiler haben die Aufgabe, Code zu generieren, der die Speicherung der von diesem Programm manipulierten Daten verwaltet. Es gibt viele verschiedene Möglichkeiten, Code zum Verwalten des Speichers zu generieren, aber im Laufe der Zeit haben sich zwei grundlegende Techniken durchgesetzt.

Die erste Möglichkeit besteht darin, eine Art "langlebigen" Speicherbereich zu haben, in dem die "Lebensdauer" jedes Bytes im Speicher - dh der Zeitraum, in dem es einer Programmvariablen gültig zugeordnet ist - nicht ohne Weiteres vorhergesagt werden kann von Zeit. Der Compiler generiert Aufrufe an einen "Heap-Manager", der weiß, wie Speicher dynamisch zugewiesen wird, wenn er benötigt wird, und fordert ihn zurück, wenn er nicht mehr benötigt wird.

Die zweite Methode ist ein "kurzlebiger" Speicherbereich, in dem die Lebensdauer jedes Bytes bekannt ist. Hier folgen die Lebensdauern einem "Nesting" -Muster. Die langlebigste dieser kurzlebigen Variablen wird vor allen anderen kurzlebigen Variablen zugewiesen und zuletzt freigegeben. Kürzere Variablen werden nach den längsten Variablen zugewiesen und vor ihnen freigegeben. Die Lebensdauer dieser kurzlebigen Variablen ist innerhalb der Lebensdauer von kurzlebigen Variablen „verschachtelt“.

Lokale Variablen folgen dem letztgenannten 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 lebendig. Sie sind tot, bevor die lokalen Variablen der ersten Methode tot sind. Die relative Reihenfolge von Beginn und Ende der Lebensdauer von Speichern, die lokalen Variablen zugeordnet sind, kann im Voraus berechnet werden.

Aus diesem Grund werden lokale Variablen in der Regel als Speicher in einer "Stapel" -Datenstruktur generiert, da ein Stapel die Eigenschaft hat, dass das erste Objekt, das darauf abgelegt wird, das letzte Objekt ist, das herausgesprungen ist.

Es ist, als würde das Hotel nur nacheinander Zimmer vermieten, und Sie können erst auschecken, wenn alle Zimmer eine höhere Nummer haben als Sie ausgecheckt haben.

Denken wir also über den Stapel nach. In vielen Betriebssystemen erhalten Sie einen Stapel pro Thread und dem Stapel wird eine bestimmte feste Größe zugewiesen. Wenn Sie eine Methode aufrufen, wird das Zeug auf den Stapel geschoben. Wenn Sie dann aus Ihrer Methode heraus einen Zeiger auf den Stapel übergeben, wie es das ursprüngliche Poster hier tut, ist dies nur ein Zeiger auf die Mitte eines vollständig gültigen Millionen-Byte-Speicherblocks. In unserer Analogie checken Sie aus dem Hotel aus; Wenn Sie dies tun, haben Sie gerade aus dem mit der höchsten Nummer belegten Raum ausgecheckt. Wenn niemand nach Ihnen eincheckt und Sie illegal in Ihr Zimmer zurückkehren, ist garantiert, dass all Ihre Sachen in diesem bestimmten Hotel noch da sind .

Wir verwenden Stapel für temporäre Läden, weil sie wirklich billig und einfach sind. Eine Implementierung von C++ ist nicht erforderlich, um einen Stack für die Speicherung von Lokalen zu verwenden. es könnte den Haufen gebrauchen. Das tut es nicht, weil das Programm dadurch langsamer würde.

Eine Implementierung von C++ ist nicht erforderlich, um den auf dem Stapel verbliebenen Müll unberührt zu lassen, damit Sie später illegal darauf zurückgreifen können. Es ist für den Compiler völlig legal, Code zu generieren, der alles in dem "Raum", den Sie gerade verlassen haben, auf Null zurücksetzt. Es tut es nicht, denn das wäre wieder teuer.

Eine Implementierung von C++ ist nicht erforderlich, um sicherzustellen, dass beim logischen Verkleinern des Stacks die früher gültigen Adressen weiterhin im Arbeitsspeicher zugeordnet werden. Die Implementierung darf dem Betriebssystem mitteilen, "dass wir diese Seite des Stapels jetzt nicht mehr verwenden. Setzen Sie, bis ich etwas anderes sage, eine Ausnahme ab, die den Prozess zerstört, wenn jemand die zuvor gültige Stapelseite berührt." Auch dies tun Implementierungen nicht, da dies langsam und unnötig ist.

Stattdessen können Sie durch Implementierungen Fehler machen und damit durchkommen. Meistens. Bis eines Tages etwas wirklich Schreckliches schief geht und der Prozess explodiert.

Das ist problematisch. Es gibt viele Regeln und es ist sehr leicht, sie versehentlich zu brechen. Ich habe sicherlich viele Male. Und noch schlimmer, das Problem tritt häufig erst auf, wenn festgestellt wird, dass der Speicher Milliarden von Nanosekunden nach dem Auftreten der Beschädigung beschädigt ist, wenn es sehr schwer ist, herauszufinden, wer es vermasselt hat.

Weitere speichersichere Sprachen lösen dieses Problem, indem Sie Ihre Leistung einschränken. In "normalem" C # gibt es einfach keine Möglichkeit, die Adresse eines Orts zu übernehmen und zurückzugeben oder für später zu speichern. Sie können die Adresse eines Einheimischen verwenden, die Sprache ist jedoch so geschickt gestaltet, dass sie nach Ablauf der Lebensdauer des Einheimischen nicht mehr verwendet werden kann. Um die Adresse eines Local zu übernehmen und zurückzugeben, müssen Sie den Compiler in einen speziellen "unsicheren" Modus versetzen und das Wort "unsicher" in Ihr Programm einfügen , um darauf aufmerksam zu machen, dass Sie wahrscheinlich etwas Gefährliches tun, das gegen die Regeln verstoßen könnte.

Zur weiteren Lektüre:

4724
Eric Lippert

Was Sie hier tun, ist einfach zu lesen und in das Gedächtnis zu schreiben gewöhnt an sei die Adresse von a. Jetzt, da Sie sich außerhalb von foo befinden, ist dies nur ein Zeiger auf einen zufälligen Speicherbereich. Es kommt einfach so vor, dass in Ihrem Beispiel dieser Speicherbereich existiert und im Moment von nichts anderem verwendet wird. Sie können nichts kaputt machen, indem Sie es weiterhin verwenden, und es wurde noch nicht überschrieben. Daher ist der 5 immer noch da. In einem echten Programm würde dieser Speicher fast sofort wiederverwendet und Sie würden damit etwas kaputt machen (obwohl die Symptome möglicherweise erst viel später auftreten!)

Wenn Sie von foo zurückkehren, teilen Sie dem Betriebssystem mit, dass Sie diesen Speicher nicht mehr verwenden, und er kann einer anderen Funktion zugewiesen werden. Wenn Sie Glück haben und es nie wieder neu zugewiesen wird und das Betriebssystem Sie nicht wieder davon abhält, es zu verwenden, dann werden Sie mit der Lüge davonkommen. Es besteht die Möglichkeit, dass Sie am Ende darüber schreiben, was sonst noch mit dieser Adresse endet.

Wenn Sie sich jetzt fragen, warum der Compiler sich nicht beschwert, liegt das wahrscheinlich daran, dass foo durch die Optimierung beseitigt wurde. Normalerweise warnt es Sie vor solchen Dingen. C setzt jedoch voraus, dass Sie wissen, was Sie tun, und technisch gesehen haben Sie hier nicht den Geltungsbereich verletzt (es gibt keinen Verweis auf a außerhalb von foo), sondern nur Speicherzugriffsregeln, die nur eine Warnung auslösen eher als ein Fehler.

Kurz gesagt: Dies funktioniert normalerweise nicht, aber manchmal durch Zufall.

271
Rena

Weil der Stauraum noch nicht voll war. Rechne nicht mit diesem Verhalten.

148
msw

Eine kleine Ergänzung 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 zerlegen, werden Sie es deutlich sehen.

80
Michael

In C++ können Sie auf jede Adresse zugreifen , aber das bedeutet nicht, dass Sie sollten . Die Adresse, auf die Sie zugreifen, ist nicht mehr gültig. Es funktioniert , weil nichts anderes den Speicher verschlüsselt hat, nachdem foo zurückgekehrt ist, aber es könnte unter vielen Umständen abstürzen. Analysieren Sie Ihr Programm mit Valgrind oder kompilieren Sie es einfach optimiert und sehen Sie ...

68
Charles Brunet

Sie lösen niemals eine C++ - Ausnahme aus, indem Sie auf ungültigen Speicher zugreifen. Sie geben nur ein Beispiel für die allgemeine Idee, auf einen beliebigen Speicherort zu verweisen. Ich könnte das auch so machen:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Hier behandle ich einfach 123456 als Adresse eines Doppelgänger und schreibe darauf. Es können beliebig viele Dinge passieren:

  1. q könnte tatsächlich eine gültige Adresse eines Doppelten sein, z. double p; q = &p;.
  2. q zeigt möglicherweise irgendwo in den zugewiesenen Speicher und ich überschreibe dort nur 8 Bytes.
  3. q zeigt außerhalb des zugewiesenen Speichers und der Speichermanager des Betriebssystems sendet ein Segmentierungsfehlersignal an mein Programm, wodurch die Laufzeitumgebung es beendet.
  4. Sie gewinnen die Lotterie.

So wie Sie es einrichten, ist es ein bisschen vernünftiger, dass die zurückgegebene Adresse auf einen gültigen Speicherbereich verweist, da sie wahrscheinlich etwas weiter unten im Stapel liegt, es sich jedoch immer noch um einen ungültigen Speicherort handelt, auf den Sie in a nicht zugreifen können deterministische Mode.

Während der normalen Programmausführung überprüft niemand automatisch die semantische Gültigkeit derartiger Speicheradressen. Ein Speicher-Debugger wie valgrind erledigt dies jedoch gerne. Führen Sie Ihr Programm durch und beobachten Sie die Fehler.

66
Kerrek SB

Haben Sie Ihr Programm mit aktiviertem Optimierer kompiliert? Die Funktion foo() ist recht einfach und wurde möglicherweise in den resultierenden Code eingefügt oder ersetzt.

Aber ich stimme Mark B zu, dass das resultierende Verhalten undefiniert ist.

28
gastush

Ihr Problem hat nichts mit scope zu tun. In dem Code, den Sie anzeigen, sieht die Funktion main die Namen in der Funktion foo nicht, so dass Sie nicht direkt mit this auf a in foo zugreifen können Name außerhalb foo.

Das Problem, das Sie haben, ist, warum das Programm keinen Fehler signalisiert, wenn es auf illegalen Speicher verweist. Dies liegt daran, dass C++ - Standards keine sehr klare Grenze zwischen illegalem Speicher und legalem Speicher festlegen. Das Verweisen auf etwas in einem herausgesprungenen Stapel verursacht manchmal Fehler und manchmal nicht. Es hängt davon ab, ob. Rechne nicht mit diesem Verhalten. Angenommen, es führt beim Programmieren immer zu Fehlern, aber beim Debuggen wird niemals ein Fehler gemeldet.

22
Chang Peng

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

Ja, wenn Sie versuchen, diese Speicheradresse zu dereferenzieren, tritt ein undefiniertes Verhalten auf.

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
}
17
Brian R. Bondy

Beachten Sie alle Warnungen. Beheben Sie nicht nur Fehler.
GCC zeigt diese Warnung an

warnung: Die Adresse der lokalen Variablen 'a' wurde zurückgegeben

Das ist die Stärke von C++. Sie sollten sich um das Gedächtnis kümmern. Mit dem Flag -Werror wird diese Warnung zu einem Fehler und Sie müssen sie jetzt debuggen.

17
sam

Es funktioniert, weil der Stack (noch) nicht geändert wurde, seit a dort abgelegt wurde. Rufen Sie einige andere Funktionen (die auch andere Funktionen aufrufen) auf, bevor Sie erneut auf a zugreifen, und Sie werden wahrscheinlich nicht mehr so ​​viel Glück haben ... ;-)

16
Adrian Grigore

Das ist klassisch ndefiniertes Verhalten das wurde hier vor nicht allzu langer Zeit besprochen - suchen Sie ein bisschen auf der Website. Kurz gesagt, Sie hatten Glück, aber alles hätte passieren können, und Ihr Code macht einen ungültigen Speicherzugriff.

16
Kerrek SB

Wie Alex betont hat, ist dieses Verhalten undefiniert. Tatsächlich werden die meisten Compiler davor warnen, weil es eine einfache Möglichkeit ist, Abstürze zu bekommen.

Als Beispiel für die Art von gruseligem Verhalten, das Sie wahrscheinlich bekommen, probieren Sie dieses Beispiel:

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 blockiert andere, nicht verwandte lokale Variablen.

16
AHelps

Sie haben tatsächlich undefiniertes Verhalten aufgerufen.

Rückgabe der Adresse eines temporären Werks, aber da temporäre Objekte am Ende einer Funktion zerstört werden, sind die Ergebnisse des Zugriffs auf diese nicht definiert.

Sie haben also nicht a geändert, sondern den Speicherort, an dem a einmal war. Dieser Unterschied ist dem Unterschied zwischen Abstürzen und Nichtabstürzen sehr ähnlich.

15

In typischen Compiler-Implementierungen können Sie sich den Code als "Ausdruck des Werts des Speicherblocks mit einer Adresse vorstellen, die früher von a belegt ist". Wenn Sie einer Funktion, die ein lokales int enthält, einen neuen Funktionsaufruf hinzufügen, ist es auch gut möglich, dass der Wert von a (oder die Speicheradresse, auf die a zeigte) Änderungen. Dies geschieht, weil der Stapel mit einem neuen Frame überschrieben wird, der andere Daten enthält.

Dies ist jedoch ndefiniert Verhalten und Sie sollten sich nicht darauf verlassen, dass es funktioniert!

13
larsmoa

Dies ist möglich, da a eine Variable ist, die vorübergehend für die Lebensdauer ihres Gültigkeitsbereichs zugewiesen wird (foo -Funktion). Nach der Rückkehr von foo ist der Speicher frei und kann überschrieben werden.

Was Sie tun, wird als ndefiniertes Verhalten beschrieben. Das Ergebnis kann nicht vorhergesagt werden.

13
littleadv

Die Dinge mit korrekter (?) Konsolenausgabe können sich dramatisch ändern, wenn Sie :: printf aber nicht cout verwenden. Sie können mit dem Debugger im folgenden Code herumspielen (getestet auf x86, 32-Bit, 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); 
}
11
Mykola

Nach der Rückkehr von einer Funktion werden alle Bezeichner zerstört, anstatt die Werte an einem Speicherort zu speichern, und wir können die Werte nicht finden, ohne einen Bezeichner zu haben. Dieser Ort enthält jedoch immer noch den Wert, der von der vorherigen Funktion gespeichert wurde.

Hier gibt die Funktion foo() die Adresse von a zurück, und a wird zerstört, nachdem die Adresse zurückgegeben wurde. Über diese zurückgegebene Adresse können Sie auf den geänderten Wert zugreifen.

Lassen Sie mich ein reales Beispiel nehmen:

Angenommen, ein Mann versteckt Geld an einem Ort und teilt Ihnen den Ort mit. Nach einiger Zeit stirbt der Mann, der Ihnen den Geldstandort mitgeteilt hat. Aber Sie haben immer noch Zugang zu diesem versteckten Geld.

Es ist eine "schmutzige" Art, Speicheradressen zu verwenden. Wenn Sie eine Adresse (einen Zeiger) zurückgeben, wissen Sie nicht, ob sie zum lokalen Bereich einer Funktion gehört. Es ist nur eine Adresse. Nachdem Sie die Funktion 'foo' aufgerufen haben, wurde diese Adresse (Speicherort) von 'a' bereits im (vorerst sicheren) adressierbaren Speicher Ihrer Anwendung (Prozess) vergeben. Nachdem die 'foo'-Funktion zurückgegeben wurde, kann die Adresse von' a 'als' schmutzig 'betrachtet werden, sie ist dort jedoch weder aufgeräumt noch durch Ausdrücke in einem anderen Programmteil gestört/modifiziert (zumindest in diesem speziellen Fall). Ein C/C++ - Compiler hindert Sie nicht an einem solchen "unsauberen" Zugriff (kann Sie jedoch warnen, wenn es Sie interessiert). Sie können jeden Speicherort, der sich im Datensegment Ihrer Programminstanz (Prozess) befindet, sicher verwenden (aktualisieren), es sei denn, Sie schützen die Adresse auf irgendeine Weise.

3
Ayub

Ihr Code ist sehr riskant. Sie erstellen eine lokale Variable (die nach Funktionsende als zerstört gilt) und geben die Speicheradresse dieser Variablen zurück, nachdem sie gelöscht wurde.

Das bedeutet, dass die Speicheradresse gültig sein kann oder nicht, und Ihr Code ist anfällig für mögliche Probleme mit der Speicheradresse (z. B. Segmentierungsfehler).

Dies bedeutet, dass Sie eine sehr schlechte Sache tun, da Sie eine Speicheradresse an einen Zeiger übergeben, der überhaupt nicht vertrauenswürdig ist.

Betrachten Sie stattdessen dieses Beispiel und testen Sie es:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

Im Gegensatz zu Ihrem Beispiel sind Sie mit diesem Beispiel:

  • zuweisen von Speicher für int zu einer lokalen Funktion
  • diese Speicheradresse ist auch dann noch gültig, wenn die Funktion abläuft (sie wird von niemandem gelöscht).
  • die Speicheradresse ist vertrauenswürdig (dieser Speicherblock wird nicht als frei betrachtet, sodass er erst überschrieben wird, wenn er gelöscht wird.)
  • die Speicheradresse sollte gelöscht werden, wenn sie nicht verwendet wird. (siehe das Löschen am Ende des Programms)
0
Nobun