Che cos'è lo svolgersi dello stack?


193

Che cos'è lo svolgersi dello stack? Ho cercato ma non sono riuscito a trovare una risposta illuminante!


76
Se non sa cosa sia, come puoi aspettarti che sappia che non sono gli stessi per C e C ++?
dreamlax,

@dreamlax: Quindi, in che modo il concetto di "impilamento dello stack" è diverso in C & C ++?
Distruttore

2
@PravasiMeet: C non ha una gestione delle eccezioni, quindi lo svolgimento dello stack è molto diretto, tuttavia, in C ++, se viene generata un'eccezione o esce una funzione, lo svolgimento dello stack comporta la distruzione di qualsiasi oggetto C ++ con durata di memorizzazione automatica.
dreamlax,

Risposte:


150

Di solito viene discusso lo stack svolgendo in relazione alla gestione delle eccezioni. Ecco un esempio:

void func( int x )
{
    char* pleak = new char[1024]; // might be lost => memory leak
    std::string s( "hello world" ); // will be properly destructed

    if ( x ) throw std::runtime_error( "boom" );

    delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}

int main()
{
    try
    {
        func( 10 );
    }
    catch ( const std::exception& e )
    {
        return 1;
    }

    return 0;
}

In questo caso la memoria allocata pleakandrà persa se viene generata un'eccezione, mentre la memoria allocata sverrà correttamente rilasciata dal std::stringdistruttore in ogni caso. Gli oggetti allocati nello stack vengono "srotolati" quando si esce dall'ambito (qui l'ambito è della funzionefunc ). Questo viene fatto dal compilatore che inserisce le chiamate ai distruttori di variabili automatiche (stack).

Ora questo è un concetto molto potente che porta alla tecnica chiamata RAII , ovvero Resource Acquisition Is Initialization , che ci aiuta a gestire risorse come memoria, connessioni al database, descrittori di file aperti, ecc. In C ++.

Ora ciò ci consente di fornire garanzie di sicurezza eccezionali .


È stato davvero illuminante! Quindi ottengo questo: se il mio processo si interrompe in modo imprevisto durante l'uscita di QUALSIASI blocco nel momento in cui lo stack viene espulso, potrebbe accadere che il codice dopo il codice del gestore delle eccezioni, non verrà eseguito affatto e potrebbe causare perdite di memoria, mucchio di corruzione ecc.
Rajendra Uppal,

15
Se il programma "si arresta in modo anomalo" (ovvero termina a causa di un errore), qualsiasi perdita di memoria o danneggiamento dell'heap è irrilevante poiché la memoria viene rilasciata al termine.
Tyler McHenry,

1
Esattamente. Grazie. Sono solo un po 'dislessico oggi.
Nikolai Fetissov,

11
@TylerMcHenry: lo standard non garantisce che le risorse o la memoria vengano rilasciate al termine. La maggior parte dei sistemi operativi succede comunque.
Mooing Duck

3
delete [] pleak;viene raggiunto solo se x == 0.
Jib

71

Tutto ciò riguarda C ++:

Definizione : quando si creano oggetti staticamente (nello stack anziché allocarli nella memoria dell'heap) e si eseguono chiamate di funzione, vengono "impilati".

Quando si esce da un ambito (qualsiasi cosa delimitato da {e }) (usando return XXX;, raggiungendo la fine del campo o lanciando un'eccezione) tutto ciò che è all'interno di quel campo viene distrutto (i distruttori sono chiamati per tutto). Questo processo di distruzione degli oggetti locali e di chiamata ai distruttori si chiama impilamento dello stack.

Sono presenti i seguenti problemi relativi allo svolgimento dello stack:

  1. evitare perdite di memoria (qualsiasi cosa allocata dinamicamente che non è gestita da un oggetto locale e ripulito nel distruttore verrà trapelata) - vedere RAII a cui fa riferimento Nikolai e la documentazione per boost :: scoped_ptr o questo esempio di utilizzo boost :: mutex :: scoped_lock .

  2. coerenza del programma: le specifiche C ++ indicano che non si dovrebbe mai generare un'eccezione prima che sia stata gestita un'eccezione esistente. Ciò significa che il processo di svolgimento dello stack non dovrebbe mai generare un'eccezione (utilizzare solo il codice garantito per non gettare nei distruttori o circondare tutto nei distruttori con try {e } catch(...) {}).

Se un distruttore genera un'eccezione durante lo svolgimento dello stack, si finisce nella terra di un comportamento indefinito che potrebbe causare la chiusura imprevista del programma (comportamento più comune) o la fine dell'universo (teoricamente possibile ma non ancora osservato nella pratica).


2
Anzi. Sebbene i goto non debbano essere abusati, causano lo svolgersi dello stack in MSVC (non in GCC, quindi probabilmente è un'estensione). setjmp e longjmp lo fanno in modo multipiattaforma, con una flessibilità leggermente inferiore.
Patrick Niedzielski,

10
Ho appena provato questo con gcc e chiama correttamente i distruttori quando si esce da un blocco di codice. Vedi stackoverflow.com/questions/334780/… - come menzionato in quel link, anche questo fa parte dello standard.
Damyan,

1
leggere Nikolai, jrista e la tua risposta in questo ordine, ora ha senso!
n611x007

@sashoalm Pensi davvero che sia necessario modificare un post sette anni dopo?
David Hoelzer,

41

In senso generale, uno stack "svolgersi" è praticamente sinonimo di fine di una chiamata di funzione e del successivo pop-up dello stack.

Tuttavia, in particolare nel caso di C ++, lo svolgimento dello stack ha a che fare con il modo in cui C ++ chiama i distruttori per gli oggetti allocati dall'inizio di qualsiasi blocco di codice. Gli oggetti creati all'interno del blocco vengono deallocati in ordine inverso rispetto alla loro allocazione.


4
Non c'è niente di speciale nei tryblocchi. Stack oggetti allocati in qualsiasi blocco (indipendentemente dal tryfatto) è soggetto a svolgersi o meno quando il blocco esce.
Chris Jester-Young,

È passato un po 'di tempo da quando ho fatto molta codifica C ++. Ho dovuto scavare quella risposta dalle profondità arrugginite. ; P
jrista,

non ti preoccupare. Ognuno ha "il suo male" di tanto in tanto.
bitc,

13

Lo svolgendo dello stack è un concetto prevalentemente C ++, che tratta di come gli oggetti allocati nello stack vengono distrutti quando si esce dal suo ambito (normalmente o tramite un'eccezione).

Supponi di avere questo frammento di codice:

void hw() {
    string hello("Hello, ");
    string world("world!\n");
    cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"

Questo vale per qualsiasi blocco? Voglio dire se ci sono solo {// alcuni oggetti locali}
Rajendra Uppal,

@Rajendra: Sì, un blocco anonimo definisce un'area di ambito, quindi conta anche.
Michael Myers

12

Non so se hai ancora letto questo, ma l'articolo di Wikipedia sullo stack di chiamate ha una spiegazione decente.

di svolgimento:

Il ritorno dalla funzione chiamata farà apparire il frame superiore fuori dallo stack, forse lasciando un valore di ritorno. Viene chiamato l'atto più generale di estrarre uno o più frame dallo stack per riprendere l'esecuzione altrove nel programma stack svolgendo e deve essere eseguito quando si utilizzano strutture di controllo non locali, come quelle utilizzate per la gestione delle eccezioni. In questo caso, il frame dello stack di una funzione contiene una o più voci che specificano i gestori di eccezioni. Quando viene generata un'eccezione, lo stack viene srotolato fino a quando viene trovato un gestore pronto a gestire (catturare) il tipo di eccezione generata.

Alcune lingue hanno altre strutture di controllo che richiedono svolgimento generale. Pascal consente a un'istruzione goto globale di trasferire il controllo da una funzione nidificata a una funzione esterna precedentemente invocata. Questa operazione richiede che lo stack venga svolto, rimuovendo tutti i frame dello stack necessari per ripristinare il contesto appropriato per trasferire il controllo all'istruzione target all'interno della funzione esterna inclusa. Allo stesso modo, C ha le funzioni setjmp e longjmp che agiscono come gotos non locali. Common Lisp consente il controllo di ciò che accade quando lo stack viene svolto utilizzando l'operatore speciale di protezione da svolgimento.

Quando si applica una continuazione, lo stack viene (logicamente) srotolato e quindi riavvolto con lo stack della continuazione. Questo non è l'unico modo per implementare le continuazioni; ad esempio, usando più stack espliciti, l'applicazione di una continuazione può semplicemente attivare il suo stack e caricare un valore da trasmettere. Il linguaggio di programmazione Scheme consente l'esecuzione di thunk arbitrari in punti specifici su "svolgendo" o "riavvolgendo" la pila di controlli quando viene invocata una continuazione.

Ispezione [modifica]


9

Ho letto un post sul blog che mi ha aiutato a capire.

Che cos'è lo svolgersi dello stack?

In qualsiasi linguaggio che supporti funzioni ricorsive (cioè praticamente tutto tranne Fortran 77 e Brainf * ck) il runtime linguistico mantiene una pila di quali funzioni sono attualmente in esecuzione. Lo svolgimento della pila è un modo per ispezionare, e possibilmente modificare, quella pila.

Perché vorresti farlo?

La risposta può sembrare ovvia, ma ci sono diverse situazioni correlate, ma leggermente diverse, in cui lo svolgimento è utile o necessario:

  1. Come meccanismo di controllo del flusso di runtime (eccezioni C ++, C longjmp (), ecc.).
  2. In un debugger, per mostrare all'utente lo stack.
  3. In un profiler, per prendere un campione dello stack.
  4. Dal programma stesso (come da un gestore di crash per mostrare lo stack).

Questi hanno requisiti leggermente diversi. Alcuni di questi sono critici per le prestazioni, altri no. Alcuni richiedono la possibilità di ricostruire i registri dal frame esterno, altri no. Ma ci occuperemo di tutto ciò in un secondo.

Puoi trovare il post completo qui .


7

Tutti hanno parlato della gestione delle eccezioni in C ++. Ma penso che ci sia un'altra connotazione per lo svolgersi dello stack e che è correlata al debug. Un debugger deve eseguire lo svolgimento dello stack ogni volta che si suppone che debba passare a un frame precedente al frame corrente. Tuttavia, si tratta di una sorta di svolgimento virtuale in quanto deve riavvolgere quando torna al frame corrente. L'esempio per questo potrebbe essere i comandi su / giù / bt in gdb.


5
L'azione di debugger è in genere chiamata "Stack Walking" che sta semplicemente analizzando lo stack. "Stack Unwinding" implica non solo "Stack Walking", ma chiama anche i distruttori di oggetti esistenti nello stack.
Adisak,

@Adisak Non sapevo che si chiamasse anche "stack walking". Ho sempre visto "impilare lo stack" nel contesto di tutti gli articoli di debugger e anche all'interno del codice gdb. Ho ritenuto "improprio dello stack" più appropriato in quanto non si tratta solo di sbirciare nelle informazioni dello stack per ogni funzione, ma comporta lo svolgersi delle informazioni sul frame (cfr. CFI in nano).
bbv

Immagino che lo "stack walking" sia reso più famoso da Windows. Inoltre, ho trovato come esempio code.google.com/p/google-breakpad/wiki/Stack A parte il documento dello standard nano stesso usa il termine svolgersi poche volte. Anche se d'accordo, è svolgersi virtuale. Inoltre, la domanda sembra porsi per ogni possibile significato che può suggerire "impilamento dello stack".
bbv

7

IMO, il diagramma di seguito riportato in questo articolo spiega meravigliosamente l'effetto dello svolgersi dello stack sul percorso dell'istruzione successiva (da eseguire una volta generata un'eccezione non rilevata):

inserisci qui la descrizione dell'immagine

Nella foto:

  • La prima è una normale esecuzione della chiamata (senza eccezioni).
  • In basso quando viene generata un'eccezione.

Nel secondo caso, quando si verifica un'eccezione, lo stack di chiamate di funzione viene cercato in modo lineare per il gestore di eccezioni. La ricerca termina con la funzione con il gestore delle eccezioni, cioè main()con il try-catchblocco racchiuso , ma non prima di rimuovere tutte le voci precedenti dallo stack delle chiamate di funzione.


I diagrammi sono buoni ma la spiegazione è un po 'confusa, vale a dire. ... con racchiudere il blocco try-catch, ma non prima di rimuovere tutte le voci precedenti dallo stack di chiamate di funzione ...
Atul

3

Il runtime C ++ distrugge tutte le variabili automatiche create tra il lancio e il fermo. In questo semplice esempio sotto f1 () i tiri e le catture main (), tra gli oggetti di tipo B e A vengono creati nello stack in quell'ordine. Quando viene lanciato f1 (), vengono chiamati i distruttori di B e A.

#include <iostream>
using namespace std;

class A
{
    public:
       ~A() { cout << "A's dtor" << endl; }
};

class B
{
    public:
       ~B() { cout << "B's dtor" << endl; }
};

void f1()
{
    B b;
    throw (100);
}

void f()
{
    A a;
    f1();
}

int main()
{
    try
    {
        f();
    }
    catch (int num)
    {
        cout << "Caught exception: " << num << endl;
    }

    return 0;
}

L'output di questo programma sarà

B's dtor
A's dtor

Questo perché il callstack del programma quando f1 () lancia sembra

f1()
f()
main()

Quindi, quando f1 () viene visualizzato, la variabile automatica b viene distrutta, e quindi quando f () viene visualizzato, la variabile automatica a viene distrutta.

Spero che questo aiuti, buona programmazione!


2

Quando viene generata un'eccezione e il controllo passa da un blocco try a un gestore, il runtime C ++ chiama i distruttori per tutti gli oggetti automatici costruiti dall'inizio del blocco try. Questo processo è chiamato svolgitura dello stack. Gli oggetti automatici vengono distrutti in ordine inverso rispetto alla loro costruzione. (Gli oggetti automatici sono oggetti locali che sono stati dichiarati auto o registrati o non dichiarati statici o esterni. Un oggetto automatico x viene eliminato ogni volta che il programma esce dal blocco in cui viene dichiarato x.)

Se viene generata un'eccezione durante la costruzione di un oggetto costituito da oggetti secondari o elementi dell'array, i distruttori vengono chiamati solo per quegli oggetti secondari o elementi dell'array costruiti correttamente prima che venga generata l'eccezione. Un distruttore per un oggetto statico locale verrà chiamato solo se l'oggetto è stato costruito correttamente.


Dovresti fornire un link all'articolo originale in cui hai copiato questa risposta da: IBM Knowledge Base - Stack Unwinding
w128

0

Nello stack Java lo svolgersi o lo svolgersi non è molto importante (con Garbage Collector). In molti documenti sulla gestione delle eccezioni ho visto questo concetto (stack svolgendo), in particolare quegli scrittori si occupano della gestione delle eccezioni in C o C ++. con try catchblocchi che non dovremmo dimenticare: stack gratuito da tutti gli oggetti dopo i blocchi locali .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.