【问题标题】:What is stack unwinding?什么是堆栈展开?
【发布时间】:2011-01-20 20:08:39
【问题描述】:

什么是堆栈展开?搜索了一遍,但找不到有启发性的答案!

【问题讨论】:

  • 如果他不知道它是什么,你怎么能指望他知道它们对于 C 和 C++ 是不一样的?
  • @dreamlax:那么,“堆栈展开”的概念在 C 和 C++ 中有何不同?
  • @PravasiMeet:C 没有异常处理,因此堆栈展开非常简单,但是,在 C++ 中,如果抛出异常或函数退出,堆栈展开涉及破坏任何具有自动存储持续时间的 C++ 对象.

标签: c++ stack


【解决方案1】:

堆栈展开通常与异常处理有关。这是一个例子:

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::string析构函数正确释放。当退出范围时,分配在堆栈上的对象将“展开”(这里的范围是函数func)。这是通过编译器插入对自动(堆栈)变量的析构函数的调用来完成的。

现在这是一个非常强大的概念,导致了称为RAII 的技术,即资源获取即初始化,它可以帮助我们管理内存、数据库连接等资源, 在 C++ 中打开文件描述符等。

现在我们可以提供exception safety guarantees

【讨论】:

  • 这真的很有启发性!所以我明白了:如果我的进程在离开堆栈被弹出的任何块期间意外崩溃,那么可能会发生异常处理程序代码之后的代码根本不会执行,并且可能导致内存泄漏,堆损坏等
  • 如果程序“崩溃”(即因错误而终止),那么任何内存泄漏或堆损坏都无关紧要,因为内存在终止时被释放。
  • 没错。谢谢。我今天只是有点阅读障碍。
  • @TylerMcHenry:该标准不保证在终止时释放资源或内存。然而,大多数操作系统都碰巧这样做了。
  • delete [] pleak; 仅在 x == 0 时达到。
【解决方案2】:

所有这些都与 C++ 相关:

定义: 当您静态创建对象(在堆栈上而不是在堆内存中分配它们)并执行函数调用时,它们会“堆叠”起来。

当退出范围(由{} 分隔的任何内容)时(通过使用return XXX;,到达范围末尾或抛出异常)该范围内的所有内容都将被销毁(为所有内容调用析构函数)。 销毁本地对象并调用析构函数的过程称为堆栈展开。

您有以下与堆栈展开相关的问题:

  1. 避免内存泄漏(任何不由本地对象管理并在析构函数中清理的动态分配的东西都会泄漏) - 请参阅 Nikolai 的 RAII referred tothe documentation for boost::scoped_ptr 或这个使用 @ 的示例987654323@.

  2. 程序一致性:C++ 规范规定在处理任何现有异常之前绝不应抛出异常。这意味着堆栈展开过程永远不应该抛出异常(要么只使用保证不会抛出析构函数的代码,要么使用 try {} catch(...) {} 包围析构函数中的所有内容)。

如果任何析构函数在堆栈展开期间抛出异常,您最终会进入未定义行为的领域,这可能导致您的程序意外终止(最常见的行为)或宇宙结束(理论上可能但尚未在实践中观察到)。

【讨论】:

  • 相反。虽然不应该滥用 goto,但它们确实会导致 MSVC 中的堆栈展开(不是在 GCC 中,所以它可能是一个扩展)。 setjmp 和 longjmp 以跨平台方式执行此操作,灵活性稍差。
  • 我刚刚用 gcc 测试了这个,当你跳出代码块时它确实正确地调用了析构函数。请参阅stackoverflow.com/questions/334780/… - 如该链接中所述,这也是标准的一部分。
  • 按此顺序阅读 Nikolai、jrista 和您的答案,现在有意义了!
  • @sashoalm 你真的认为有必要在七年后编辑帖子吗?
  • @DavidHoelzer 我同意,大卫!当我看到编辑日期和发布日期时,我也在想。
【解决方案3】:

一般来说,堆栈“展开”几乎是函数调用结束和堆栈随后弹出的同义词。

但是,特别是在 C++ 的情况下,堆栈展开与 C++ 如何调用自任何代码块开始以来分配的对象的析构函数有关。在块内创建的对象按其分配的相反顺序被释放。

【讨论】:

  • try 块没有什么特别之处。在 any 块中分配的堆栈对象(无论是否try)在块退出时都会展开。
  • 我已经有一段时间没有做过很多 C++ 编码了。我不得不从生锈的深渊中挖掘出答案。 ;P
  • 别担心。每个人偶尔都有“自己的坏处”。
【解决方案4】:

我不知道你是否读过这篇文章,但Wikipedia's article on the call stack 有一个不错的解释。

展开:

从被调用函数返回将弹出栈顶帧,可能会留下一个返回值。将一帧或多帧从堆栈中弹出以恢复程序中其他位置的执行的更一般的行为称为堆栈展开,并且必须在使用非本地控制结构时执行,例如用于异常的控制结构处理。在这种情况下,函数的堆栈帧包含一个或多个指定异常处理程序的条目。抛出异常时,堆栈会展开,直到找到准备处理(捕获)抛出的异常类型的处理程序。

某些语言具有其他需要常规展开的控制结构。 Pascal 允许全局 goto 语句将控制从嵌套函数转移到先前调用的外部函数。此操作需要展开堆栈,根据需要删除尽可能多的堆栈帧以恢复正确的上下文,从而将控制权转移到封闭外部函数中的目标语句。类似地,C 具有充当非本地 goto 的 setjmp 和 longjmp 函数。 Common Lisp 允许通过使用 unwind-protect 特殊运算符来控制堆栈展开时发生的情况。

当应用延续时,堆栈(逻辑上)展开,然后与延续的堆栈一起重绕。这不是实现延续的唯一方法;例如,使用多个显式堆栈,延续的应用程序可以简单地激活其堆栈并缠绕要传递的值。 Scheme 编程语言允许在调用 continuation 时在控制堆栈的“展开”或“回退”的指定点执行任意 thunk。

检查[编辑]

【讨论】:

    【解决方案5】:

    堆栈展开是一个主要的 C++ 概念,处理堆栈分配的对象在其范围退出时如何被销毁(正常或通过异常)。

    假设你有这段代码:

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

    【讨论】:

    • 这适用于任何块吗?我的意思是如果只有 { // 一些本地对象 }
    • @Rajendra:是的,匿名块定义了一个范围区域,所以它也很重要。
    【解决方案6】:

    我读了一篇帮助我理解的博文。

    什么是堆栈展开?

    在任何支持递归函数的语言中(即几乎 除 Fortran 77 和 Brainf*ck 之外的所有内容)语言运行时保留 当前正在执行的函数的堆栈。堆栈展开是 一种检查并可能修改该堆栈的方法。

    您为什么要这样做?

    答案可能看起来很明显,但有几个相关的,但很微妙 不同的情况下,放松是有用的或必要的:

    1. 作为运行时控制流机制(C++ 异常、C longjmp() 等)。
    2. 在调试器中,向用户显示堆栈。
    3. 在分析器中,对堆栈进行采样。
    4. 来自程序本身(例如来自崩溃处理程序以显示堆栈)。

    这些要求略有不同。 其中一些对性能至关重要,而另一些则不是。有些需要 从外部框架重建寄存器的能力,有些没有。但 我们将在一秒钟内了解所有这些内容。

    你可以找到完整的帖子here

    【讨论】:

      【解决方案7】:

      IMO,article 中给出的下图精美地解释了堆栈展开对下一条指令路径的影响(一旦抛出未捕获的异常就执行):

      在图片中:

      • 第一个是正常的调用执行(没有抛出异常)。
      • 抛出异常时的底部。

      第二种情况,当异常发生时,线性搜索函数调用栈寻找异常处理程序。搜索在带有异常处理程序的函数处结束,即 main() 带有封闭的 try-catch 块,但不是在此之前从函数调用堆栈中删除它之前的所有条目。

      【讨论】:

      • 图表很好,但解释有点混乱。 ...带有封闭的try-catch块,但不是在从函数调用堆栈中删除它之前的所有条目之前...
      【解决方案8】:

      每个人都谈到了 C++ 中的异常处理。但是,我认为堆栈展开还有另一个含义,它与调试有关。每当调试器应该转到当前帧之前的帧时,它都必须进行堆栈展开。然而,这是一种虚拟的展开,因为当它回到当前帧时需要倒带。这方面的示例可以是 gdb 中的 up/down/bt 命令。

      【讨论】:

      • 调试器操作通常称为“堆栈遍历”,它只是解析堆栈。 “堆栈展开”不仅意味着“堆栈行走”,还意味着调用堆栈中存在的对象的析构函数。
      • @Adisak 我不知道它也被称为“堆栈行走”。我一直在所有调试器文章的上下文中甚至在 gdb 代码中都看到“堆栈展开”。我觉得“堆栈展开”更合适,因为它不仅涉及查看每个函数的堆栈信息,还涉及帧信息的展开(c.f. CFI in dwarf)。这是按函数逐个处理的。
      • 我猜 Windows 让“堆栈行走”更加出名。另外,我发现作为一个例子code.google.com/p/google-breakpad/wiki/StackWalking 除了矮人标准的文档本身使用术语展开几次。虽然同意,但实际上是放松。此外,这个问题似乎在询问“堆栈展开”可以暗示的所有可能含义。
      【解决方案9】:

      C++ 运行时会破坏在 throw 和 catch 之间创建的所有自动变量。在下面的这个简单示例中,f1() 抛出和 main() 捕获,在 B 和 A 类型的对象之间按顺序在堆栈上创建。当 f1() 抛出时,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
      

      这是因为 f1() 抛出时程序的调用栈看起来像

      f1()
      f()
      main()
      

      所以,当 f1() 被弹出时,自动变量 b 被破坏,然后当 f() 被弹出时,自动变量 a 被破坏。

      希望对您有所帮助,祝您编码愉快!

      【讨论】:

        【解决方案10】:

        当抛出异常并且控制从 try 块传递到处理程序时,C++ 运行时会为自 try 块开始以来构造的所有自动对象调用析构函数。这个过程称为堆栈展开。自动对象以其构造的相反顺序被销毁。 (自动对象是已声明为 auto 或 register,或未声明为 static 或 extern 的局部对象。只要程序退出声明 x 的块,自动对象 x 就会被删除。)

        如果在构造由子对象或数组元素组成的对象期间引发异常,则仅对在引发异常之前成功构造的子对象或数组元素调用析构函数。仅当对象构造成功时才会调用本地静态对象的析构函数。

        【讨论】:

        【解决方案11】:

        在 Java 中,堆栈展开或展开并不是很重要(使用垃圾收集器)。在许多异常处理论文中,我看到了这个概念(堆栈展开),特别是那些作者处理 C 或 C++ 中的异常处理。使用try catch 块,我们不应该忘记:在本地块之后释放所有对象的堆栈

        【讨论】:

          【解决方案12】:

          堆栈展开是在运行时从函数调用堆栈中删除函数条目的过程。它通常与异常处理有关。在 C++ 中 , 当 异常 发生 时 , 函数 调用 堆栈 会 线性 搜索 异常 处理 函数 之前 的 所有 条目 , 带有 异常 处理 的 函数 会 从 函数 调用 堆栈 中 移除 .

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-04-17
            • 2012-04-21
            • 2014-08-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-02-02
            • 2016-03-01
            相关资源
            最近更新 更多