กองคลี่คลายคืออะไร?


193

กองคลี่คลายคืออะไร? ค้นหาผ่าน แต่ไม่พบคำตอบที่กระจ่างแจ้ง!


76
ถ้าเขาไม่รู้ว่ามันคืออะไรคุณจะคาดหวังให้เขารู้ได้อย่างไรว่ามันไม่เหมือนกันสำหรับ C และ C ++
dreamlax

@dreamlax: ดังนั้นแนวคิดของ "stack unwinding" แตกต่างกันใน C & C ++ อย่างไร
Destructor

2
@PravasiMeet: C ไม่มีการจัดการข้อยกเว้นดังนั้น stack unwinding จึงมีความตรงมากอย่างไรก็ตามใน C ++ หากมีการโยนข้อยกเว้นหรือออกจากฟังก์ชัน Stack unwinding จะเกี่ยวข้องกับการทำลายวัตถุ C ++ ใด ๆ ที่มีระยะเวลาการจัดเก็บอัตโนมัติ
Dreamlax

คำตอบ:


150

การคลายสแตกนั้นมักจะพูดถึงเกี่ยวกับข้อยกเว้น นี่คือตัวอย่าง:

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

หน่วยความจำที่จัดสรรไว้ที่นี่pleakจะหายไปหากมีการโยนข้อยกเว้นในขณะที่หน่วยความจำที่จัดสรรให้sจะถูกปล่อยโดยstd::stringdestructor อย่างเหมาะสมในทุกกรณี ออบเจ็กต์ที่จัดสรรในสแต็กคือ "คลายออก" เมื่อออกจากขอบเขต (นี่คือขอบเขตของฟังก์ชั่นfunc) สิ่งนี้กระทำโดยคอมไพเลอร์ที่แทรกการเรียกไปยัง destructors ของตัวแปรอัตโนมัติ (สแต็ค)

ตอนนี้นี่เป็นแนวคิดที่ทรงพลังมากซึ่งนำไปสู่เทคนิคที่เรียกว่าRAIIนั่นคือการได้รับทรัพยากรคือการเริ่มต้นซึ่งช่วยให้เราจัดการทรัพยากรเช่นหน่วยความจำการเชื่อมต่อฐานข้อมูลตัวอธิบายไฟล์แบบเปิดเป็นต้นใน C ++

ตอนนี้ที่ช่วยให้เราสามารถให้การรับประกันความปลอดภัยยกเว้น


นั่นคือการตรัสรู้จริงๆ! ดังนั้นฉันจึงได้รับสิ่งนี้: หากกระบวนการของฉันล้มเหลวโดยไม่คาดคิดระหว่างออกจากบล็อกใด ๆ ที่สแต็กเวลาถูกผุดแล้วอาจเกิดขึ้นได้ว่ารหัสหลังจากรหัสตัวจัดการข้อยกเว้นจะไม่ถูกประมวลผลเลยและอาจทำให้หน่วยความจำรั่ว กองการทุจริต ฯลฯ
Rajendra Uppal

15
หากโปรแกรม "ขัดข้อง" (เช่นถูกยกเลิกเนื่องจากข้อผิดพลาด) แสดงว่าการรั่วไหลของหน่วยความจำหรือความเสียหายของฮีปฮีปนั้นไม่เกี่ยวข้องเนื่องจากหน่วยความจำจะถูกปล่อยเมื่อสิ้นสุดการทำงาน
Tyler McHenry

1
เผง ขอบคุณ วันนี้ฉันแค่เป็นคนขี้เกียจนิดหน่อย
Nikolai Fetissov

11
@TylerMcHenry: มาตรฐานไม่รับประกันว่าทรัพยากรหรือหน่วยความจำจะถูกปล่อยเมื่อสิ้นสุด ระบบปฏิบัติการส่วนใหญ่เกิดขึ้นอย่างไรก็ตาม
Mooing Duck

3
delete [] pleak;จะทำได้ก็ต่อเมื่อ x == 0
Jib

71

ทั้งหมดนี้เกี่ยวข้องกับ C ++:

คำจำกัดความ : เมื่อคุณสร้างวัตถุแบบสแตติก (บนสแต็กซึ่งตรงข้ามกับการจัดสรรวัตถุเหล่านั้นในหน่วยความจำฮีป) และดำเนินการเรียกใช้ฟังก์ชันพวกเขาจะ "ซ้อนกัน"

เมื่อขอบเขต (สิ่งใดคั่นด้วย{และ}) ถูกออก (โดยใช้return XXX;ให้ถึงจุดสิ้นสุดของขอบเขตหรือส่งข้อยกเว้น) ทุกอย่างภายในขอบเขตนั้นจะถูกทำลาย (destructors ถูกเรียกใช้สำหรับทุกสิ่ง) กระบวนการทำลายวัตถุในท้องถิ่นและการเรียก destructors นี้เรียกว่าการคลี่คลายสแตก

คุณมีปัญหาต่อไปนี้ที่เกี่ยวข้องกับการคลี่คลายสแตก:

  1. หลีกเลี่ยงการรั่วไหลของหน่วยความจำ (สิ่งใดจัดสรรแบบไดนามิกที่ไม่ได้รับการจัดการโดยวัตถุในท้องถิ่นและทำความสะอาดใน destructor จะรั่วไหล) - ดู RAII ที่อ้างถึงโดย Nikolai และเอกสารสำหรับ boost :: scoped_ptrหรือตัวอย่างของการใช้boost :: mutex ::

  2. ความสอดคล้องของโปรแกรม: สถานะข้อมูลจำเพาะ C ++ ที่คุณไม่ควรทิ้งข้อยกเว้นก่อนที่จะจัดการข้อยกเว้นที่มีอยู่ ซึ่งหมายความว่ากระบวนการคลายสแต็กไม่ควรทิ้งข้อยกเว้น (อาจใช้เฉพาะรหัสที่รับประกันว่าจะไม่โยนเข้าไปใน destructors หรือล้อมรอบทุกอย่างใน destructors ด้วยtry {และ} catch(...) {})

หากผู้ทำลายใด ๆ ขว้างข้อยกเว้นระหว่างการคลี่คลายความรู้สึกของคุณคุณจะพบกับพฤติกรรมที่ไม่ได้กำหนดซึ่งอาจทำให้โปรแกรมของคุณยุติการทำงานโดยไม่คาดคิด (พฤติกรรมที่พบบ่อยที่สุด) หรือเอกภพสิ้นสุด


2
ในทางตรงกันข้าม. ในขณะที่ gotos ไม่ควรถูกทารุณกรรมพวกเขาทำให้เกิดการคลี่คลายคลาดเคลื่อนใน MSVC (ไม่ใช่ใน GCC ดังนั้นจึงอาจเป็นส่วนขยาย) setjmp และ longjmp ทำสิ่งนี้ในลักษณะข้ามแพลตฟอร์มโดยมีความยืดหยุ่นน้อยกว่า
Patrick Niedzielski

10
ฉันเพิ่งทดสอบสิ่งนี้ด้วย gcc และมันเรียกอย่างถูกต้องว่า destructors เมื่อคุณออกจากบล็อกรหัส ดูstackoverflow.com/questions/334780/… - ดังที่กล่าวไว้ในลิงค์นี้เป็นส่วนหนึ่งของมาตรฐานเช่นกัน
Damyan

1
อ่านของ Nikolai, jrista และคำตอบของคุณตามลำดับนี้ตอนนี้มันสมเหตุสมผลแล้ว!
n611x007

@sashoalm คุณคิดว่าจำเป็นต้องแก้ไขโพสต์เจ็ดปีต่อมาหรือไม่
David Hoelzer

41

ในความหมายทั่วไปสแต็ก "ผ่อนคลาย" มีความหมายเหมือนกันมากเมื่อสิ้นสุดการเรียกใช้ฟังก์ชันและการ popping ของสแต็กตามมา

อย่างไรก็ตามโดยเฉพาะอย่างยิ่งในกรณีของ C ++ สแต็คคลี่คลายจะทำอย่างไรกับวิธี C ++ เรียก destructors สำหรับวัตถุที่จัดสรรตั้งแต่เริ่มต้นของบล็อกรหัสใด ๆ วัตถุที่สร้างขึ้นภายในบล็อกจะถูกจัดสรรคืนในลำดับย้อนหลังของการจัดสรร


4
ไม่มีอะไรพิเศษเกี่ยวกับtryบล็อก วัตถุสแต็คที่จัดสรรในบล็อกใด ๆ ( tryหรือไม่ก็ตาม) อาจมีการคลี่คลายเมื่อบล็อกออก
Chris Jester-Young

เป็นเวลานานแล้วที่ฉันได้ทำ C ++ มาก ฉันต้องขุดคำตอบนั้นออกมาจากส่วนลึกที่เป็นสนิม ; P
jrista

ไม่ต้องกังวล ทุกคนมี "ไม่ดี" เป็นครั้งคราว
bitc

13

การคลี่คลายสแตกเป็นส่วนใหญ่เป็นแนวคิด C ++ การจัดการกับวิธีการที่วัตถุที่จัดสรรสแต็คจะถูกทำลายเมื่อออกจากขอบเขตของมัน (ไม่ว่าจะเป็นปกติหรือผ่านข้อยกเว้น)

สมมติว่าคุณมีรหัสส่วนนี้:

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

สิ่งนี้มีผลกับบล็อกใด ๆ หรือไม่? ฉันหมายถึงหากมีเพียง {// วัตถุในท้องถิ่นบางตัว}
Rajendra Uppal

@Rajendra: ใช่บล็อกที่ไม่ระบุชื่อจะกำหนดขอบเขตของขอบเขตดังนั้นมันจึงนับด้วย
Michael Myers

12

ฉันไม่รู้ว่าคุณอ่านสิ่งนี้หรือไม่ แต่บทความของ Wikipedia ใน call stackมีคำอธิบายที่เหมาะสม

คลี่คลาย:

กลับมาจากฟังก์ชั่นที่เรียกว่าจะปรากฏกรอบด้านบนออกจากสแต็กอาจทิ้งค่าตอบแทน การกระทำโดยทั่วไปของการ popping หนึ่งเฟรมขึ้นไปจากสแต็กเพื่อกลับไปดำเนินการที่อื่นในโปรแกรมเรียกว่าstack unwindingและจะต้องดำเนินการเมื่อใช้โครงสร้างการควบคุมที่ไม่ใช่แบบโลคัลเช่นที่ใช้สำหรับการจัดการข้อยกเว้น ในกรณีนี้เฟรมสแต็กของฟังก์ชันมีหนึ่งรายการขึ้นไปที่ระบุตัวจัดการข้อยกเว้น เมื่อมีข้อผิดพลาดเกิดขึ้นสแต็กจะคลายออกจนกว่าจะพบตัวจัดการที่พร้อมที่จะจัดการ (จับ) ชนิดของข้อยกเว้นที่ส่งออกมา

บางภาษามีโครงสร้างการควบคุมอื่น ๆ ที่ต้องการการคลายตัวทั่วไป Pascal อนุญาตให้ใช้คำสั่ง goto ทั่วโลกเพื่อถ่ายโอนการควบคุมออกจากฟังก์ชั่นที่ซ้อนกันและเป็นฟังก์ชั่นด้านนอกที่เรียกใช้ก่อนหน้านี้ การดำเนินการนี้ต้องการสแต็กที่จะคลายออกลบสแต็กเฟรมได้มากเท่าที่จำเป็นเพื่อกู้คืนบริบทที่เหมาะสมในการถ่ายโอนการควบคุมไปยังคำสั่งเป้าหมายภายในฟังก์ชั่นด้านนอกที่ล้อมรอบ ในทำนองเดียวกัน C มีฟังก์ชัน setjmp และ longjmp ที่ทำหน้าที่เป็น gotos ที่ไม่ใช่แบบโลคัล Common Lisp ช่วยให้สามารถควบคุมสิ่งที่เกิดขึ้นเมื่อกองกำลังคลายโดยใช้ตัวดำเนินการพิเศษป้องกันการคลาย

เมื่อใช้ความต่อเนื่องสแต็กคือ (เชิงตรรกะ) คลายออกแล้วย้อนกลับด้วยสแต็คของความต่อเนื่อง นี่ไม่ใช่วิธีเดียวที่จะใช้การต่อเนื่อง ตัวอย่างเช่นการใช้สแต็คจำนวนมากอย่างชัดเจนการใช้ความต่อเนื่องสามารถเปิดใช้งานสแต็กและกำหนดค่าให้ส่งผ่านได้ ภาษาการเขียนโปรแกรม Scheme อนุญาตให้ดำเนินการ thunks ตามอำเภอใจในจุดที่ระบุบน "คลี่คลาย" หรือ "rewinding" ของกองควบคุมเมื่อมีการเรียกใช้ความต่อเนื่อง

ตรวจสอบ [แก้ไข]


9

ฉันอ่านโพสต์บล็อกที่ช่วยให้ฉันเข้าใจ

กองคลี่คลายคืออะไร?

ในทุกภาษาที่รองรับฟังก์ชั่นแบบเรียกซ้ำ (เช่นทุกอย่างยกเว้น Fortran 77 และ Brainf * ck) รันไทม์ภาษาจะเก็บสแต็กของฟังก์ชันที่กำลังดำเนินการอยู่ การคลี่คลายสแต็กเป็นวิธีการตรวจสอบและอาจแก้ไขสแต็กนั้น

ทำไมคุณต้องการทำเช่นนั้น?

คำตอบอาจดูเหมือนชัดเจน แต่มีหลายสถานการณ์ที่เกี่ยวข้อง แต่แตกต่างกันอย่างละเอียดสถานการณ์ที่คลี่คลายเป็นประโยชน์หรือจำเป็น:

  1. เป็นกลไกการควบคุมการไหลแบบรันไทม์ (ข้อยกเว้น C ++, C longjmp (), ฯลฯ )
  2. ในตัวดีบักเกอร์เพื่อแสดงสแต็กผู้ใช้
  3. ใน profiler เพื่อนำตัวอย่างของสแต็ก
  4. จากโปรแกรมเอง (เช่นจากตัวจัดการความผิดพลาดเพื่อแสดงสแต็ก)

สิ่งเหล่านี้มีข้อกำหนดที่แตกต่างกันเล็กน้อย บางส่วนของสิ่งเหล่านี้มีความสำคัญต่อประสิทธิภาพ แต่บางตัวก็ไม่ใช่ บางคนต้องการความสามารถในการสร้างรีจิสเตอร์จากเฟรมด้านนอกบางตัวไม่ได้ แต่เราจะเข้าสู่ทุกสิ่งภายในไม่กี่วินาที

คุณสามารถพบการโพสต์เต็มรูปแบบที่นี่


7

ทุกคนได้พูดคุยเกี่ยวกับการจัดการข้อยกเว้นใน C ++ แต่ฉันคิดว่ามีความหมายอื่นสำหรับการคลายสแต็กและเกี่ยวข้องกับการดีบัก ดีบักเกอร์จะต้องทำการคลี่คลายสแต็กเมื่อใดก็ตามที่ควรจะไปที่เฟรมก่อนหน้าเฟรมปัจจุบัน อย่างไรก็ตามนี่เป็นเสมือนการคลี่คลายเนื่องจากต้องย้อนกลับเมื่อมาถึงเฟรมปัจจุบัน ตัวอย่างสำหรับสิ่งนี้อาจเป็นคำสั่ง up / down / bt ใน gdb


5
การดำเนินการดีบักเกอร์โดยทั่วไปเรียกว่า "Stack Walking" ซึ่งเป็นการแยกสแต็ก "Stack Unwinding" ไม่เพียง แต่หมายถึง "Stack Walking" แต่ยังเรียก destructors ของวัตถุที่มีอยู่ใน stack
Adisak

@Adisak ฉันไม่รู้ว่ามันถูกเรียกว่า "stack stack" ฉันเคยเห็น "stack unwinding" ในบริบทของบทความดีบักทั้งหมดและยังอยู่ในโค้ด gdb ฉันรู้สึกว่า "ไม่สะดวกในการสแต็ค" ที่เหมาะสมมากกว่าเพราะมันไม่ได้เป็นเพียงการแอบดูข้อมูลสแต็กสำหรับทุกฟังก์ชั่น แต่เกี่ยวข้องกับการคลี่คลายของข้อมูลเฟรม (cf CFI ในคนแคระ) กระบวนการนี้ประมวลผลตามลำดับ
bbv

ฉันเดาว่า "การเดินบนกองซ้อน" นั้นมีชื่อเสียงมากขึ้นโดย Windows นอกจากนี้ฉันพบว่าเป็นตัวอย่างcode.google.com/p/google-breakpad/wiki/StackWalkingนอกเหนือจากเอกสารมาตรฐานของคนแคระเองใช้คำที่คลี่คลายไม่กี่ครั้ง แม้ว่าเห็นด้วยมันเป็นเสมือนการคลี่คลาย ยิ่งไปกว่านั้นคำถามดูเหมือนว่าจะถามถึงความหมายที่เป็นไปได้ "กองซ้อนคลี่คลาย" สามารถแนะนำ
bbv

7

IMO แผนภาพด้านล่างที่ให้ไว้ในบทความนี้อธิบายถึงผลกระทบของการคลี่คลายสแต็คบนเส้นทางของคำสั่งถัดไป (จะถูกดำเนินการเมื่อข้อยกเว้นถูกโยนทิ้งซึ่งไม่ถูกตรวจจับ):

ป้อนคำอธิบายรูปภาพที่นี่

ในรูป:

  • หนึ่งในนั้นคือการดำเนินการโทรปกติ (โดยไม่มีข้อยกเว้น)
  • ด้านล่างหนึ่งเมื่อโยนข้อยกเว้น

ในกรณีที่สองเมื่อเกิดข้อยกเว้นสแตกการเรียกฟังก์ชันจะค้นหาตัวจัดการข้อยกเว้นเชิงเส้น การค้นหาจะจบลงที่ฟังก์ชันด้วยตัวจัดการข้อยกเว้นเช่นmain()กับtry-catchบล็อกที่ล้อมรอบแต่ไม่ใช่ก่อนที่จะลบรายการทั้งหมดก่อนที่จะออกจากสแต็กการเรียกใช้ฟังก์ชัน


ไดอะแกรมนั้นดี แต่คำอธิบายก็คือความสับสนเล็กน้อย ... พร้อมบล็อก try-catch ที่ล้อมรอบ แต่ไม่ใช่ก่อนที่จะลบรายการทั้งหมดก่อนที่จะออกจาก stack call function ...
Atul

3

C ++ รันไทม์ทำลายตัวแปรอัตโนมัติทั้งหมดที่สร้างขึ้นระหว่างระหว่างการส่ง & การจับ ในตัวอย่างง่ายๆด้านล่างนี้ f1 () throws และ main () catches ระหว่างวัตถุประเภท B และ A ถูกสร้างขึ้นบนสแต็กตามลำดับนั้น เมื่อ f1 () ขว้าง destructors ของ B และ 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;
}

ผลลัพธ์ของโปรแกรมนี้จะเป็น

B's dtor
A's dtor

นี่เป็นเพราะ callstack ของโปรแกรมเมื่อ f1 () พ่นหน้าตา

f1()
f()
main()

ดังนั้นเมื่อ f1 () ผุดแล้วตัวแปรอัตโนมัติ b จะถูกทำลายแล้วเมื่อ f () เป็นตัวแปรอัตโนมัติที่ผุดแล้วจะถูกทำลาย

หวังว่านี่จะช่วยได้ขอให้มีความสุขในการเข้ารหัส!


2

เมื่อมีข้อผิดพลาดเกิดขึ้นและการควบคุมส่งผ่านจากบล็อกลองไปยังตัวจัดการ C ++ รันไทม์จะเรียกใช้ destructors สำหรับวัตถุอัตโนมัติทั้งหมดที่สร้างขึ้นตั้งแต่เริ่มต้นบล็อกลอง กระบวนการนี้เรียกว่าการคลี่คลายสแตก วัตถุอัตโนมัติถูกทำลายในลำดับย้อนหลังของการก่อสร้าง (วัตถุอัตโนมัติเป็นวัตถุในท้องถิ่นที่ได้รับการประกาศโดยอัตโนมัติหรือลงทะเบียนหรือไม่ประกาศคงที่หรือภายนอกวัตถุอัตโนมัติ x จะถูกลบเมื่อใดก็ตามที่โปรแกรมออกจากบล็อกที่มีการประกาศ x)

หากมีข้อผิดพลาดเกิดขึ้นระหว่างการสร้างวัตถุที่ประกอบด้วย subobjects หรือองค์ประกอบอาเรย์ destructors จะถูกเรียกใช้เฉพาะสำหรับ subobjects หรืออิลิเมนต์อาร์เรย์ที่สร้างสำเร็จก่อนที่จะทำการยกเว้น destructor สำหรับวัตถุสแตติกท้องถิ่นจะถูกเรียกใช้เฉพาะถ้าวัตถุนั้นสร้างสำเร็จ


คุณควรให้ลิงค์ไปยังบทความต้นฉบับที่คุณคัดลอกคำตอบนี้จาก: IBM Knowledge Base - Unwinding Stack
w128

0

ในจาวาสแต็กที่ไม่รู้จักหรือไม่เปิดไม่สำคัญมาก ในเอกสารการจัดการข้อยกเว้นหลายอย่างฉันเห็นแนวคิดนี้ (การคลี่คลายสแต็ก) โดยเฉพาะนักเขียนเหล่านั้นจะจัดการกับข้อยกเว้นใน C หรือ C ++ กับtry catchบล็อกเรา shouln't ลืม: สแต็คฟรีจากวัตถุทั้งหมดหลังจากบล็อกท้องถิ่น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.