È ansible accedere alla memoria di una variabile locale al di fuori del suo ambito?

Ho il codice seguente.

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

E il codice è in esecuzione senza eccezioni di runtime!

L’uscita era 58

Come può essere? La memoria di una variabile locale non è inaccessibile al di fuori della sua funzione?

Come può essere? La memoria di una variabile locale non è inaccessibile al di fuori della sua funzione?

Si affitta una stanza d’albergo. Metti un libro nel primo cassetto del comodino e vai a dormire. Il giorno dopo fai il check-out, ma “dimentica” di restituire la chiave. Tu rubi la chiave!

Una settimana dopo, ritorni in albergo, non effettuare il check in, entrare di nascosto nella tua vecchia stanza con la chiave rubata e guardare nel cassetto. Il tuo libro è ancora lì. Stupefacente!

Come può essere? Il contenuto del cassetto di una camera d’albergo non è inaccessibile se non hai affittato la stanza?

Bene, ovviamente questo scenario può accadere nel mondo reale senza problemi. Non c’è una forza misteriosa che fa sparire il tuo libro quando non sei più autorizzato ad essere nella stanza. Né esiste una forza misteriosa che ti impedisce di entrare in una stanza con una chiave rubata.

La direzione dell’hotel non è tenuta a rimuovere il tuo libro. Non hai stipulato un contratto con loro che dicesse che se ti lasci dietro qualcosa, lo distruggeranno per te. Se rientri illegalmente nella tua stanza con una chiave rubata per riaverla, lo staff della sicurezza dell’hotel non è obbligato a prenderti di nascosto. Non hai stipulato un contratto con loro che diceva “se tento di rientrare di nascosto nella mia stanza stanza dopo, è necessario fermarmi. ” Piuttosto, hai firmato un contratto con loro che diceva “Prometto di non rientrare più tardi nella mia stanza”, un contratto che hai rotto .

In questa situazione può succedere di tutto . Il libro può essere lì – sei stato fortunato. Il libro di qualcun altro può essere lì e il tuo potrebbe essere nella fornace dell’hotel. Qualcuno potrebbe essere lì giusto quando entri, facendo a pezzi il tuo libro. L’hotel avrebbe potuto rimuovere il tavolo e prenotare completamente e sostituito con un armadio. L’intero hotel potrebbe essere in procinto di essere abbattuto e sostituito con uno stadio di calcio, e tu morirai in un’esplosione mentre ti muovi furtivamente.

Tu non sai cosa sta per accadere; quando sei uscito dall’hotel e hai rubato una chiave per usarla illegalmente in un secondo momento, hai rinunciato al diritto di vivere in un mondo prevedibile e sicuro perché hai scelto di infrangere le regole del sistema.

Il C ++ non è un linguaggio sicuro . Ti permetterà allegramente di infrangere le regole del sistema. Se cerchi di fare qualcosa di illegale e sciocco come tornare in una stanza in cui non sei autorizzato a entrare e rovistare in una scrivania che potrebbe non essere più lì, C ++ non ti fermerà. Lingue più sicure del C ++ risolvono questo problema limitando la tua potenza – ad esempio, con un controllo molto più rigido sui tasti.

AGGIORNARE

Santo Dio, questa risposta sta ricevendo molta attenzione. (Non sono sicuro del perché – l’ho considerato una piccola analogia “divertente”, ma qualunque cosa sia.)

Ho pensato che potrebbe essere pertinente aggiornarlo un po ‘con alcuni pensieri più tecnici.

I compilatori sono nel business della generazione di codice che gestisce la memorizzazione dei dati manipolati da quel programma. Esistono molti modi diversi per generare codice per gestire la memoria, ma nel tempo due tecniche di base si sono radicate.

Il primo è quello di avere una sorta di area di archiviazione “longeva” in cui la “durata” di ciascun byte nella memoria – cioè il periodo di tempo in cui è validamente associata a qualche variabile di programma – non può essere facilmente predetta avanti di tempo. Il compilatore genera chiamate in un “gestore di heap” che sa allocare dynamicmente lo spazio di archiviazione quando è necessario e riprenderlo quando non è più necessario.

Il secondo è quello di avere una sorta di area di memoria “di breve durata” in cui la vita di ogni byte nello storage è ben nota, e, in particolare, la durata delle memorie segue un modello di “nidificazione”. Cioè, l’allocazione delle variabili a vita più lunga delle vite di breve durata si sovrappone strettamente alle allocazioni delle variabili a vita breve che vengono dopo di essa.

Le variabili locali seguono il secondo modello; quando viene immesso un metodo, le sue variabili locali diventano attive. Quando quel metodo chiama un altro metodo, le variabili locali del nuovo metodo prendono vita. Saranno morti prima che le variabili locali del primo metodo siano morte. L’ordine relativo degli inizi e delle terminazioni delle vite di depositi associati alle variabili locali può essere elaborato in anticipo.

Per questo motivo, le variabili locali vengono generalmente generate come memoria su una struttura di dati “stack”, perché una pila ha la proprietà che la prima cosa che ha spinto su di essa sarà l’ultima cosa spuntata.

È come se l’hotel decidesse di affittare solo le stanze in modo sequenziale, e non è ansible effettuare il check-out finché tutti con un numero di camera superiore a quello che si è verificato.

Quindi pensiamo allo stack. In molti sistemi operativi si ottiene uno stack per thread e lo stack viene assegnato in modo da avere una certa dimensione fissa. Quando chiami un metodo, la roba viene spinta in pila. Se si passa quindi un puntatore alla pila indietro rispetto al proprio metodo, come fa il poster originale, questo è solo un puntatore al centro di un blocco di memoria di milioni di byte interamente valido. Nella nostra analogia, controlli fuori dall’hotel; quando lo fai, hai appena controllato la stanza occupata dal numero più alto. Se nessun altro ti controlla dopo di te, e torni nella tua stanza illegalmente, tutte le tue cose sono garantite per essere ancora lì in questo particolare hotel .

Utilizziamo gli stack per i negozi temporanei perché sono davvero economici e facili. Un’implementazione di C ++ non è necessaria per utilizzare uno stack per l’archiviazione dei locali; potrebbe usare l’heap. Non lo fa, perché ciò renderebbe il programma più lento.

Un’implementazione di C ++ non è necessaria per lasciare intatta la spazzatura che si è lasciata in pila in modo da poterla tornare in seguito illegalmente; è perfettamente legale per il compilatore generare un codice che ritorni a zero tutto nella “stanza” che hai appena lasciato libero. Non perché, di nuovo, sarebbe costoso.

Un’implementazione di C ++ non è necessaria per garantire che quando lo stack si restringe logicamente, gli indirizzi che erano validi siano ancora mappati in memoria. L’implementazione è autorizzata a dire al sistema operativo che “abbiamo finito di usare questa pagina di stack ora. Fino a quando non dirò altrimenti, emetterò un’eccezione che distruggerà il processo se qualcuno tocca la pagina dello stack precedentemente valida”. Ancora una volta, le implementazioni in realtà non lo fanno perché è lento e non necessario.

Invece, le implementazioni ti permettono di fare errori e farla franca. La maggior parte delle volte. Finché un giorno qualcosa di veramente orribile va storto e il processo esplode.

Questo è problematico. Ci sono molte regole ed è molto facile romperle accidentalmente. Ho certamente molte volte. E peggio ancora, il problema spesso emerge solo quando la memoria viene rilevata come corrotta miliardi di nanosecondi dopo la corruzione, quando è molto difficile capire chi l’ha incasinato.

Più linguaggi sicuri per la memoria risolvono questo problema limitando la tua potenza. In “normale” C # non c’è semplicemente modo di prendere l’indirizzo di un locale e restituirlo o memorizzarlo per dopo. Puoi prendere l’indirizzo di un locale, ma la lingua è progettata in modo intelligente in modo che sia imansible utilizzarlo dopo la durata dei locali. Per prendere l’indirizzo di un locale e restituirlo, devi mettere il compilatore in una speciale modalità “non sicura” e mettere la parola “non sicuro” nel tuo programma, per richiamare l’attenzione sul fatto che probabilmente stai facendo qualcosa di pericoloso che potrebbe infrangere le regole.

Per ulteriori letture:

Quello che stai facendo qui è semplicemente leggere e scrivere nella memoria che era l’indirizzo di a . Ora che sei fuori di foo , è solo un puntatore a un’area di memoria casuale. Accade solo che nel tuo esempio, quell’area di memoria esista e che nient’altro la stia utilizzando al momento. Non rompi nulla continuando a usarlo, e nient’altro lo ha ancora sovrascritto. Pertanto, il 5 è ancora lì. In un programma reale, quella memoria sarebbe stata riutilizzata quasi immediatamente e avresti rotto qualcosa facendo ciò (anche se i sintomi potrebbero non apparire molto dopo!)

Quando ritorni da foo , dici al sistema operativo che non stai più utilizzando quella memoria e che può essere riassegnato a qualcos’altro. Se sei fortunato e non viene mai riassegnato, e il sistema operativo non ti sorprende ad usarlo di nuovo, allora ti libererai della bugia. È probabile che finirai a scrivere su qualunque altra cosa finisca con quell’indirizzo.

Ora, se ti stai chiedendo perché il compilatore non si lamenta, è probabilmente perché foo stato eliminato dall’ottimizzazione. Di solito ti avviserà di questo genere di cose. C presume che tu sappia cosa stai facendo, e tecnicamente non hai violato lo scope qui (non c’è riferimento a se stesso al di fuori di foo ), solo le regole di accesso alla memoria, che triggersno solo un avvertimento piuttosto che un errore.

In breve: questo di solito non funziona, ma a volte sarà per caso.

Perché lo spazio di archiviazione non è stato ancora calpestato. Non contare su questo comportamento.

Una piccola aggiunta a tutte le risposte:

se fai qualcosa del genere:

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

l’output probabilmente sarà: 7

Questo perché dopo essere ritornati da foo () lo stack viene liberato e quindi riutilizzato da boo (). Se si assembla l’eseguibile, lo vedrai chiaramente.

In C ++, puoi accedere a qualsiasi indirizzo, ma ciò non significa che dovresti . L’indirizzo che stai accedendo non è più valido. Funziona perché nient’altro ha strapazzato la memoria dopo il ritorno di Foo, ma potrebbe bloccarsi in molte circostanze. Prova ad analizzare il tuo programma con Valgrind , o anche solo a compilarlo ottimizzato, e vedi …

Non si lancia mai un’eccezione C ++ accedendo alla memoria non valida. Stai solo dando un esempio dell’idea generale di referenziare una posizione di memoria arbitraria. Potrei fare lo stesso in questo modo:

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

Qui sto semplicemente trattando 123456 come indirizzo di una doppia e scrivendo su di essa. Qualsiasi numero di cose potrebbe accadere:

  1. q potrebbe infatti essere veramente un indirizzo valido di un doppio, ad esempio double p; q = &p; double p; q = &p; .
  2. q potrebbe puntare da qualche parte all’interno della memoria allocata e sovrascrivo solo 8 byte.
  3. q punti al di fuori della memoria allocata e il gestore della memoria del sistema operativo invia un segnale di errore di segmentazione al mio programma, provocando il runtime di interromperlo.
  4. Vinci la lotteria.

Il modo in cui lo si imposta è un po ‘più ragionevole che l’indirizzo restituito punti in un’area di memoria valida, in quanto probabilmente sarà solo un po’ più in basso nello stack, ma è ancora una posizione non valida a cui non è ansible accedere in un moda deterministica.

Nessuno controllerà automaticamente la validità semantica degli indirizzi di memoria come quello per voi durante la normale esecuzione del programma. Tuttavia, un debugger di memoria come valgrind farà felicemente, quindi dovresti eseguire il programma attraverso di esso e verificare gli errori.

Hai compilato il tuo programma con l’ottimizzatore abilitato?

La funzione foo () è piuttosto semplice e potrebbe essere stata sostituita / sostituita nel codice risultante.

Ma sono d’accordo con Mark B che il comportamento risultante non è definito.

Il tuo problema non ha nulla a che fare con l’ ambito . Nel codice che mostri, la funzione main non vede i nomi nella funzione foo , quindi non puoi accedere a in foo direttamente con questo nome all’esterno di foo .

Il problema che si sta verificando è perché il programma non segnala un errore quando si fa riferimento alla memoria illegale. Questo perché gli standard C ++ non specificano un confine molto chiaro tra memoria illegale e memoria legale. Il riferimento a qualcosa nello stack saltato causa talvolta errori e talvolta no. Dipende. Non contare su questo comportamento. Supponiamo che si verificherà sempre un errore durante la programmazione, ma supponiamo che non segnalerà mai un errore durante il debug.

Stai solo restituendo un indirizzo di memoria, è permesso ma probabilmente un errore.

Sì, se provi a dereferenziare quell’indirizzo di memoria avrai un comportamento indefinito.

 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 } 

Questo è il classico comportamento indefinito che è stato discusso qui non due giorni fa: cerca un po ‘nel sito. In poche parole, sei stato fortunato, ma tutto potrebbe essere successo e il tuo codice sta rendendo non valido l’accesso alla memoria.

Questo comportamento è indefinito, come ha sottolineato Alex – in effetti, la maggior parte dei compilatori avviserà di non farlo, perché è un modo semplice per ottenere arresti anomali.

Per un esempio del tipo di comportamento spettrale che potresti ottenere, prova questo esempio:

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

Questo stampa "y = 123", ma i risultati possono variare (davvero!). Il tuo puntatore sta invadendo altre variabili locali non correlate.

Funziona perché lo stack non è stato alterato (ancora) da quando è stato messo lì. Chiama alcune altre funzioni (che chiamano anche altre funzioni) prima di accedere a a nuova e probabilmente non sarai più così fortunato … 😉

Hai effettivamente invocato un comportamento indefinito.

Restituendo l’indirizzo di un lavoro temporaneo, ma poiché i provvisori vengono distrutti alla fine di una funzione, i risultati dell’accesso non saranno definiti.

Quindi non hai modificato a ma piuttosto la posizione di memoria in cui si trovava a volta. Questa differenza è molto simile alla differenza tra arresto anomalo e arresto anomalo.

Nelle tipiche implementazioni del compilatore, è ansible pensare al codice come “stampare il valore del blocco di memoria con l’indirizzo che prima era occupato da un”. Inoltre, se si aggiunge una nuova funzione invocazione a una funzione che contiene un int locale, è buona probabilità che il valore di a (o dell’indirizzo di memoria a cui è abituato a puntare) cambi. Ciò accade perché lo stack verrà sovrascritto con un nuovo frame contenente dati diversi.

Tuttavia, questo è un comportamento indefinito e non si dovrebbe fare affidamento su di esso per funzionare!

Prestare attenzione a tutti gli avvertimenti. Non solo risolvere errori.
GCC mostra questo avvertimento

avviso: indirizzo della variabile locale ‘a’ restituito

Questo è potere del C ++. Dovresti preoccuparti della memoria. Con il flag -Werror , questo avviso diventa un errore e ora è necessario eseguirne il debug.

Può, perché a è una variabile allocata temporaneamente per la durata del suo ambito (funzione foo ). Dopo il ritorno da foo la memoria è gratuita e può essere sovrascritta.

Quello che stai facendo è descritto come comportamento non definito . Il risultato non può essere previsto.

Le cose con l’output della console corretto (?) Possono cambiare drasticamente se si utilizza :: printf ma non cout. Puoi giocare con il debugger all’interno del codice sottostante (testato su 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); } 

Dopo essere ritornati da una funzione, tutti gli identificatori vengono distrutti invece dei valori mantenuti in una posizione di memoria e non possiamo localizzare i valori senza avere un identificatore. Ma quella posizione contiene ancora il valore memorizzato dalla funzione precedente.

Quindi, qui function foo() sta restituendo l’indirizzo di a e a viene distrutto dopo aver restituito il suo indirizzo. E puoi accedere al valore modificato attraverso quell’indirizzo restituito.

Lasciatemi fare un esempio reale:

Supponiamo che un uomo nasconda denaro in un luogo e ti indichi la posizione. Dopo un po ‘di tempo, l’uomo che ti ha detto che la posizione del denaro muore. Ma tu hai ancora l’accesso a quei soldi nascosti.

È un modo “sporco” di usare gli indirizzi di memoria. Quando si restituisce un indirizzo (puntatore) non si sa se appartiene all’ambito locale di una funzione. È solo un indirizzo Ora che hai invocato la funzione “pippo”, quell’indirizzo (posizione di memoria) di “a” era già stato allocato lì nella memoria (sicura, per ora almeno) indirizzabile della tua applicazione (processo). Dopo che è stata restituita la funzione ‘foo’, l’indirizzo di ‘a’ può essere considerato ‘sporco’ ma è lì, non ripulito, né disturbato / modificato da espressioni in altre parti del programma (in questo caso specifico almeno). Il compilatore AC / C ++ non ti impedisce di accedere in modo così “sporco” (potresti avvisarti se ti interessa). Puoi tranquillamente usare (aggiornare) qualsiasi posizione di memoria che si trova nel segmento di dati dell’istanza del tuo programma (processo) a meno che non proteggi l’indirizzo in qualche modo.

Questo è sicuramente un problema di temporizzazione! L’object a cui punta il puntatore p è “programmato” per essere distrutto quando esce dall’ambito di foo . Questa operazione tuttavia non avviene immediatamente, ma piuttosto un numero di cicli CPU successivi. Se questo è un comportamento indefinito, o C ++ sta effettivamente facendo qualcosa di pre-pulizia in background, non lo so.

Se si inserisce una chiamata alla funzione sleep del sistema operativo tra la chiamata a foo e le istruzioni cout , facendo attendere al programma circa un secondo prima di debind il puntatore, si noterà che i dati sono scomparsi dal momento in cui si desidera leggerlo ! Guarda il mio esempio:

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

(Si noti che ho usato la funzione sleep da unistd.h , che è presente solo su sistemi di tipo Unix, quindi sarà necessario sostituirla con Sleep(1000) e Windows.h se si è su Windows.)

Ho sostituito il tuo int con una class, così posso vedere esattamente quando viene chiamato il distruttore.

L'output di questo codice è il seguente:

 myClass ctor myClass dtor 5 8 

Tuttavia, se si modifica doSleep su true :

 myClass ctor myClass dtor 0 8 

As you can see, the object that is supposed to be destroyed IS actually destroyed, but I suppose there are some pre-cleanup instructions that must execute before an object (or just a variable) gets destroyed, so until those are done, the data is still accessible for a short period of time (however there's no guarantee for that of course, so please don't write code that relies on this).

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 compilers that don't follow the standard properly. Actually this wouldn't be the only case where compilers 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.