Was ist das Abwickeln des Stapels?


193

Was ist das Abwickeln des Stapels? Durchsucht, aber keine aufschlussreiche Antwort gefunden!


76
Wenn er nicht weiß, was es ist, wie können Sie erwarten, dass er weiß, dass sie für C und C ++ nicht gleich sind?
Dreamlax

@dreamlax: Wie unterscheidet sich das Konzept des "Stack Unwinding" in C & C ++?
Zerstörer

2
@PravasiMeet: C hat keine Ausnahmebehandlung, daher ist das Abwickeln des Stapels sehr einfach. Wenn jedoch in C ++ eine Ausnahme ausgelöst wird oder eine Funktion beendet wird, werden beim Abwickeln des Stapels alle C ++ - Objekte mit automatischer Speicherdauer zerstört.
Dreamlax

Antworten:


150

Das Abwickeln von Stapeln wird normalerweise im Zusammenhang mit der Ausnahmebehandlung erwähnt. Hier ist ein Beispiel:

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

Hier Speicher für zugewiesen pleak verloren, wenn eine Ausnahme ausgelöst wird, während der zugewiesene Speicher in jedem Fall svom std::stringDestruktor ordnungsgemäß freigegeben wird. Die auf dem Stapel zugewiesenen Objekte werden beim Verlassen des Bereichs "abgewickelt" (hier ist der Bereich von der Funktion func). Dies erfolgt durch den Compiler, der Aufrufe an Destruktoren von automatischen (Stapel-) Variablen einfügt.

Dies ist ein sehr leistungsfähiges Konzept, das zur Technik RAII führt Ressourcenerfassung ist Initialisierung , mit der wir Ressourcen wie Speicher, Datenbankverbindungen, offene Dateideskriptoren usw. in C ++ verwalten können.

Dies ermöglicht es uns nun, Ausnahmesicherheitsgarantien zu geben .


Das war wirklich aufschlussreich! Ich verstehe Folgendes: Wenn mein Prozess beim Verlassen eines Blocks, zu dem der Stapel gestapelt wurde, unerwartet abstürzt, kann es vorkommen, dass der Code nach dem Ausnahmebehandlungscode überhaupt nicht ausgeführt wird und Speicherlecks verursacht. Haufen Korruption usw.
Rajendra Uppal

15
Wenn das Programm „abstürzt“ (dh endet aufgrund eines Fehler), dann ist jeder Speicher Leckage oder Heapbeschädigung ist irrelevant , da der Speicher bei Beendigung freigegeben ist.
Tyler McHenry

1
Genau. Vielen Dank. Ich bin heute nur ein bisschen Legastheniker.
Nikolai Fetissov

11
@ TylerMcHenry: Der Standard garantiert nicht, dass Ressourcen oder Speicher bei Beendigung freigegeben werden. Die meisten Betriebssysteme tun dies jedoch zufällig.
Mooing Duck

3
delete [] pleak;wird nur erreicht, wenn x == 0.
Fock

71

All dies bezieht sich auf C ++:

Definition : Wenn Sie Objekte statisch erstellen (auf dem Stapel, anstatt sie im Heapspeicher zuzuweisen) und Funktionsaufrufe ausführen, werden sie "gestapelt".

Wenn ein Bereich (alles, was durch {und begrenzt ist }) beendet wird (indem Sie verwenden return XXX;, das Ende des Bereichs erreichen oder eine Ausnahme auslösen), wird alles in diesem Bereich zerstört (Destruktoren werden für alles aufgerufen). Dieser Vorgang des Zerstörens lokaler Objekte und des Aufrufs von Destruktoren wird als Abwickeln des Stapels bezeichnet.

Sie haben die folgenden Probleme beim Abwickeln des Stapels:

  1. Vermeiden von Speicherlecks (alles, was dynamisch zugewiesen wird und nicht von einem lokalen Objekt verwaltet und im Destruktor bereinigt wird, geht verloren) - siehe RAII, auf das Nikolai verweist , und die Dokumentation zu boost :: scoped_ptr oder dieses Beispiel für die Verwendung von boost :: mutex :: scoped_lock .

  2. Programmkonsistenz: Die C ++ - Spezifikationen besagen, dass Sie niemals eine Ausnahme auslösen sollten, bevor eine vorhandene Ausnahme behandelt wurde. Dies bedeutet, dass der Stapelabwicklungsprozess niemals eine Ausnahme auslösen sollte (entweder nur Code verwenden, der garantiert keine Destruktoren einwirft, oder alles in Destruktoren mit try {und umgeben } catch(...) {}).

Wenn ein Destruktor beim Abwickeln des Stapels eine Ausnahme auslöst, befinden Sie sich im Land des undefinierten Verhaltens, das dazu führen kann, dass Ihr Programm unerwartet beendet wird (häufigstes Verhalten) oder das Universum endet (theoretisch möglich, in der Praxis jedoch noch nicht beobachtet).


2
Andererseits. Gotos sollten zwar nicht missbraucht werden, verursachen jedoch in MSVC ein Abwickeln des Stapels (nicht in GCC, daher handelt es sich wahrscheinlich um eine Erweiterung). setjmp und longjmp tun dies plattformübergreifend mit etwas weniger Flexibilität.
Patrick Niedzielski

10
Ich habe dies gerade mit gcc getestet und es ruft die Destruktoren korrekt auf, wenn Sie aus einem Codeblock herauskommen. Siehe stackoverflow.com/questions/334780/… - wie in diesem Link erwähnt, ist dies ebenfalls Teil des Standards.
Damyan

1
Lesen Sie Nikolais, Jristas und Ihre Antwort in dieser Reihenfolge, jetzt macht es Sinn!
n611x007

@sashoalm Glaubst du wirklich, dass es sieben Jahre später notwendig ist, einen Beitrag zu bearbeiten?
David Hoelzer

41

Im Allgemeinen ist ein "Abwickeln" eines Stapels so ziemlich gleichbedeutend mit dem Ende eines Funktionsaufrufs und dem anschließenden Aufspringen des Stapels.

Insbesondere im Fall von C ++ hat das Abwickeln des Stapels jedoch damit zu tun, wie C ++ die Destruktoren für die Objekte aufruft, die seit dem Start eines Codeblocks zugewiesen wurden. Objekte, die innerhalb des Blocks erstellt wurden, werden in umgekehrter Reihenfolge ihrer Zuordnung freigegeben.


4
tryBlöcke haben nichts Besonderes . Stapelobjekte, die in einem Block zugewiesen sind (unabhängig davon, ob sie vorhanden sind tryoder nicht), werden beim Beenden des Blocks abgewickelt.
Chris Jester-Young

Es ist schon eine Weile her, dass ich viel C ++ codiert habe. Ich musste diese Antwort aus den rostigen Tiefen graben. ; P
jrista

Mach dir keine Sorgen. Jeder hat gelegentlich "sein schlechtes".
Bitc

13

Das Abwickeln von Stapeln ist hauptsächlich ein C ++ - Konzept, das sich mit der Zerstörung von Objekten mit Stapelzuweisung befasst, wenn der Gültigkeitsbereich beendet wird (entweder normal oder durch eine Ausnahme).

Angenommen, Sie haben dieses Codefragment:

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

Gilt das für einen Block? Ich meine, wenn es nur {// einige lokale Objekte} gibt
Rajendra Uppal

@ Rajendra: Ja, ein anonymer Block definiert einen Bereich, also zählt er auch.
Michael Myers

12

Ich weiß noch nicht, ob du das gelesen hast, aber der Artikel von Wikipedia über den Aufrufstapel enthält eine anständige Erklärung.

Abwickeln:

Wenn Sie von der aufgerufenen Funktion zurückkehren, wird der obere Frame vom Stapel entfernt und möglicherweise ein Rückgabewert hinterlassen. Der allgemeinere Vorgang, einen oder mehrere Frames vom Stapel zu entfernen, um die Ausführung an anderer Stelle im Programm fortzusetzen, wird aufgerufen Abwickeln des Stapels bezeichnet und muss ausgeführt werden, wenn nicht lokale Steuerstrukturen verwendet werden, wie sie beispielsweise für die Ausnahmebehandlung verwendet werden. In diesem Fall enthält der Stapelrahmen einer Funktion einen oder mehrere Einträge, die Ausnahmebehandlungsroutinen angeben. Wenn eine Ausnahme ausgelöst wird, wird der Stapel abgewickelt, bis ein Handler gefunden wird, der bereit ist, den Typ der ausgelösten Ausnahme zu behandeln (abzufangen).

Einige Sprachen haben andere Kontrollstrukturen, die ein allgemeines Abwickeln erfordern. Mit Pascal kann eine globale goto-Anweisung die Kontrolle aus einer verschachtelten Funktion in eine zuvor aufgerufene äußere Funktion übertragen. Diese Operation erfordert, dass der Stapel abgewickelt wird, wobei so viele Stapelrahmen wie nötig entfernt werden, um den richtigen Kontext wiederherzustellen und die Kontrolle an die Zielanweisung innerhalb der umschließenden äußeren Funktion zu übertragen. In ähnlicher Weise verfügt C über die Funktionen setjmp und longjmp, die als nicht lokale gotos fungieren. Common Lisp ermöglicht die Steuerung dessen, was passiert, wenn der Stapel abgewickelt wird, indem der spezielle Bediener zum Abwickeln verwendet wird.

Beim Anwenden einer Fortsetzung wird der Stapel (logisch) abgewickelt und dann mit dem Stapel der Fortsetzung zurückgespult. Dies ist nicht die einzige Möglichkeit, Fortsetzungen zu implementieren. Wenn Sie beispielsweise mehrere explizite Stapel verwenden, kann die Anwendung einer Fortsetzung einfach ihren Stapel aktivieren und einen zu übergebenden Wert aufwickeln. Die Programmiersprache Scheme ermöglicht die Ausführung beliebiger Thunks an bestimmten Punkten beim "Abwickeln" oder "Zurückspulen" des Steuerungsstapels, wenn eine Fortsetzung aufgerufen wird.

Inspektion [Bearbeiten]


9

Ich habe einen Blog-Beitrag gelesen, der mir das Verständnis erleichtert hat.

Was ist das Abwickeln des Stapels?

In jeder Sprache, die rekursive Funktionen unterstützt (dh so ziemlich alles außer Fortran 77 und Brainf * ck), enthält die Sprachlaufzeit einen Stapel der Funktionen, die gerade ausgeführt werden. Das Abwickeln des Stapels ist eine Möglichkeit, diesen Stapel zu überprüfen und möglicherweise zu ändern.

Warum willst du das tun?

Die Antwort mag offensichtlich erscheinen, aber es gibt mehrere verwandte, aber subtil unterschiedliche Situationen, in denen das Abwickeln nützlich oder notwendig ist:

  1. Als Laufzeit-Kontrollflussmechanismus (C ++ - Ausnahmen, C longjmp () usw.).
  2. In einem Debugger, um dem Benutzer den Stapel anzuzeigen.
  3. In einem Profiler eine Stichprobe des Stapels entnehmen.
  4. Aus dem Programm selbst (wie von einem Crash-Handler, um den Stack anzuzeigen).

Diese haben subtil unterschiedliche Anforderungen. Einige davon sind leistungskritisch, andere nicht. Einige erfordern die Fähigkeit, Register aus dem äußeren Rahmen zu rekonstruieren, andere nicht. Aber wir werden gleich darauf eingehen.

Den vollständigen Beitrag finden Sie hier .


7

Alle haben über die Ausnahmebehandlung in C ++ gesprochen. Aber ich denke, es gibt eine andere Konnotation für das Abwickeln von Stapeln, die mit dem Debuggen zusammenhängt. Ein Debugger muss das Abwickeln des Stapels durchführen, wenn er zu einem Frame vor dem aktuellen Frame wechseln soll. Dies ist jedoch eine Art virtuelles Abwickeln, da es zurückgespult werden muss, wenn es zum aktuellen Frame zurückkehrt. Das Beispiel hierfür könnten up / down / bt-Befehle in gdb sein.


5
Die Debugger-Aktion wird normalerweise als "Stack Walking" bezeichnet, bei dem lediglich der Stack analysiert wird. "Stack Unwinding" bedeutet nicht nur "Stack Walking", sondern auch das Aufrufen der Destruktoren von Objekten, die auf dem Stapel vorhanden sind.
Adisak

@Adisak Ich wusste nicht, dass es auch "Stack Walking" genannt wird. Ich habe immer "Stack Unwinding" im Kontext aller Debugger-Artikel und sogar innerhalb von GDB-Code gesehen. Ich fand "Abwickeln des Stapels" angemessener, da es nicht nur darum geht, für jede Funktion in Stapelinformationen zu spähen, sondern auch darum, Bildinformationen abzuwickeln (vgl. CFI in Zwerg). Dies wird in der Reihenfolge einer Funktion nach der anderen verarbeitet.
BBV

Ich denke, das "Stack Walking" wird durch Windows bekannter gemacht. Außerdem habe ich als Beispiel festgestellt, dass code.google.com/p/google-breakpad/wiki/StackWalking, abgesehen davon, dass das Dokument von dwarf standard selbst einige Male den Begriff "Abwickeln" verwendet. Obwohl einverstanden, ist es virtuelles Abwickeln. Darüber hinaus scheint die Frage nach jeder möglichen Bedeutung zu fragen, die "Abwickeln des Stapels" nahe legen kann.
BBV

7

IMO, das unten stehende Diagramm in diesem Artikel erklärt auf wunderbare Weise die Auswirkung des Abwickelns des Stapels auf die Route des nächsten Befehls (wird ausgeführt, sobald eine Ausnahme ausgelöst wird, die nicht erfasst wurde):

Geben Sie hier die Bildbeschreibung ein

Auf dem Bild:

  • Das oberste ist eine normale Anrufausführung (ohne Ausnahme).
  • Unten, wenn eine Ausnahme ausgelöst wird.

Im zweiten Fall, wenn eine Ausnahme auftritt, wird der Funktionsaufrufstapel linear nach dem Ausnahmebehandler durchsucht. Die Suche endet bei der Funktion mit Ausnahmebehandlungsroutine, dh main()mit umschließendem try-catchBlock, jedoch nicht vor dem Entfernen aller vorhergehenden Einträge aus dem Funktionsaufrufstapel.


Diagramme sind gut, aber die Erklärung ist etwas verwirrend. ... mit eingeschlossenem Try-Catch-Block, aber nicht bevor alle Einträge davor aus dem Funktionsaufrufstapel entfernt wurden ...
Atul

3

Die C ++ - Laufzeit zerstört alle automatischen Variablen, die zwischen throw & catch erstellt wurden. In diesem einfachen Beispiel unten werden f1 () -Würfe und main () -Fänge zwischen Objekten des Typs B und A in dieser Reihenfolge auf dem Stapel erstellt. Wenn f1 () wirft, werden die Destruktoren von B und A aufgerufen.

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

Die Ausgabe dieses Programms wird sein

B's dtor
A's dtor

Dies liegt daran, dass der Aufruf des Programms beim Auslösen von f1 () so aussieht

f1()
f()
main()

Wenn also f1 () gepoppt wird, wird die automatische Variable b zerstört, und wenn f () gepoppt wird, wird die automatische Variable a zerstört.

Hoffe das hilft, viel Spaß beim Codieren!


2

Wenn eine Ausnahme ausgelöst wird und die Steuerung von einem try-Block an einen Handler übergeben wird, ruft die C ++ - Laufzeit Destruktoren für alle automatischen Objekte auf, die seit Beginn des try-Blocks erstellt wurden. Dieser Vorgang wird als Abwickeln des Stapels bezeichnet. Die automatischen Objekte werden in umgekehrter Reihenfolge ihrer Konstruktion zerstört. (Automatische Objekte sind lokale Objekte, die als automatisch oder registriert deklariert oder nicht als statisch oder extern deklariert wurden. Ein automatisches Objekt x wird gelöscht, wenn das Programm den Block verlässt, in dem x deklariert ist.)

Wenn während der Erstellung eines Objekts, das aus Unterobjekten oder Array-Elementen besteht, eine Ausnahme ausgelöst wird, werden Destruktoren nur für die Unterobjekte oder Array-Elemente aufgerufen, die erfolgreich erstellt wurden, bevor die Ausnahme ausgelöst wurde. Ein Destruktor für ein lokales statisches Objekt wird nur aufgerufen, wenn das Objekt erfolgreich erstellt wurde.


Sie sollten einen Link zu dem Originalartikel bereitstellen, in den Sie diese Antwort kopiert haben: IBM Knowledge Base - Abwickeln des
Stapels

0

Im Java-Stack ist Unwiding oder Unwounding nicht sehr wichtig (mit Garbage Collector). In vielen Ausnahmebehandlungspapieren habe ich dieses Konzept gesehen (Stack Unwinding), insbesondere befassen sich diese Writer mit der Ausnahmebehandlung in C oder C ++. Bei try catchBlöcken sollten wir nicht vergessen: Freier Stapel von allen Objekten nach lokalen Blöcken .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.