【问题标题】:Is longjmp supposed to restore the stack?longjmp 应该恢复堆栈吗?
【发布时间】:2019-10-22 06:29:38
【问题描述】:

据我了解,setjmp 保存了当前上下文,并且应该在调用longjmp 时恢复它。然而,下一段代码打印 15 (我用 -g 编译并且没有任何优化)。我误解了这个结构还是我遗漏了什么?

#include <iostream>
#include <csetjmp>


std::jmp_buf jump_buffer;

int main()
{
    int a = 0;
    if (setjmp(jump_buffer) == 0) {
      a = 15;
      std::longjmp(jump_buffer, 42);
    }
    std::cerr << a << std::endl;
}

免责声明:仅出于好奇而尝试使用它。我从来没有听说过这个结构,直到我最近阅读了一些关于 NASA 编码指南的论文,其中提到禁止使用这种控制流结构

同时使用 c 和 c++ 标记,因为代码是混合的,我假设实际的相关功能与 c 重度用户更相关,而不是 c++...:/

【问题讨论】:

  • 参考。 std::jmp_buf 用于实际存储的内容(例如,没有您认为的局部变量值)。

标签: c++ c setjmp


【解决方案1】:

那是expected behavior

返回setjmp的范围后,所有可访问的对象, 浮点状态标志和抽象的其他组件 machine 具有与 std::longjmp 时相同的值 已执行,setjmp 中的非易失性局部变量除外 范围,如果它们的值从 setjmp 调用

执行longjmpa 的值是15,所以这是一个可以预期看到的值(通常是不确定的)。 jmp_buf 只存储执行点。不是程序中每个变量的状态。

【讨论】:

  • 谢谢。那么它与简单的 goto 有什么不同呢?
  • @user2717954 - 标准 C 和 C++ 中的 goto 不能跨越函数边界。
  • a 的值属于不确定类别,因为它不是 volatile 并且在 setjmp 和 longjmp 之间进行了修改
  • @Shawn - 一个人,从语言律师的角度来看,你是非常正确的。但另一方面,这种措辞基本上是说不知道被调用的函数如何在跳回之前修改值。我不确定longjmp 本身是否可以修改这些值。
  • 更复杂的例子:将 longjmp() 移动到一个不同的函数,除了调用它什么都不做,现在 gcc 8 为我打印 0 而不是 15。
【解决方案2】:

setjmp 范围内的非易失性局部变量除外,如果它们在 setjmp 调用后发生更改,则其值是不确定的。 部分描述非常重要,因为您的值重新看到属于那个不确定的类别。

考虑对您的程序稍作修改:

#include <iostream>
#include <csetjmp>

std::jmp_buf jump_buffer;

void func() {
  std::longjmp(jump_buffer, 42);
}

int main()
{
  int a = 0;
  volatile int b = 0;
  if (std::setjmp(jump_buffer) == 0) {
    a = 15;
    b = 1;
    func();
  }
  std::cout << a << ' ' << b << '\n';
}

当我编译并运行这个版本时(使用-O),我得到0 1作为输出,而不是15 1(因为a是不确定的,你的结果可能会有所不同)。

如果您想要在初始 setjmp() 调用和调用 longjmp() 之间更改的局部变量以可靠地保持该更改,则它需要是 volatile

【讨论】:

【解决方案3】:

对于setjmplongjmp,“上下文”是执行上下文,而不是堆栈的实际内容(通常存储局部变量的地方)。

使用 setjmplongjmp 您不能“回滚”对局部变量所做的更改。

【讨论】:

    【解决方案4】:

    我认为您可能会看到较旧的代码库。异常在某些编译器中不太流行或不可用的地方。

    在您更接近系统软件工作之前,您不应使用 setjmp 和 longjmp。

    • 至于控制流程:setjmp 返回两次,longjmp 从不返回 返回。
    • 当您第一次调用 setjmp 时,存储 环境,它返回零,
    • 然后,当您调用 longjmp 时,控制流将通过参数中提供的值从 setjmp 返回。
    • 用例通常被称为“错误处理”,并且 “不要使用这些功能”。

    setjmp 和 longjmp 存储和恢复 CPU SFR(即上下文寄存器)。

    这是一个小控制流示例:

    #include <stdio.h>
    #include <setjmp.h>
    jmp_buf env;
    void foo()
    {
        longjmp(&env, 10);                      +---->----+
    }                                           |         |
                                                |         |
    int main()              (entry)---+         ^         V
    {                                 |         |         |
        if(setjmp(&env) == 0)         | (= 0)   |         | (= 10)
        {                             |         ^         |
            foo();                    +---->----+         |
        }                                                 +---->----+
        else                                                        |
        {                                                           |
            return 0;                                               +--- (end)
        }
    }
    

    【讨论】:

      【解决方案5】:
      “Setjump” and “Longjump” are defined in setjmp.h, a header file in C standard library.
      
      setjump(jmp_buf buf) : uses buf to remember current position and returns 0.
      longjump(jmp_buf buf, i) : Go back to place buf is pointing to and return i .
      

      简单示例

       #include <stdio.h>
      #include <setjmp.h>
      
      static jmp_buf buf;
      
      void second() {
          printf("second\n");         // prints
          longjmp(buf,1);             // jumps back to where setjmp was called - making setjmp now return 1
      }
      
      void first() {
          second();
          printf("first\n");          // does not print
      }
      
      int main() {   
          if (!setjmp(buf))
              first();                // when executed, setjmp returned 0
          else                        // when longjmp jumps back, setjmp returns 1
              printf("main\n");       // prints
      
          return 0;
      }
      

      这就是为什么 setjump() 为您返回 0 并且当您检查条件时,它分配 a=15 并且一旦该过程完成,下一步它会给出 42。

      这些函数的主要特点是提供了一种偏离标准调用和返回顺序的方式。这主要用于在 C 中实现异常处理。 setjmp 可以像 try 一样使用(在 C++ 和 Java 等语言中)。对 longjmp 的调用可以像 throw 一样使用(注意 longjmp() 将控制转移到 setjmp() 设置的点)。

      【讨论】:

        【解决方案6】:

        我只想回答问题的另一部分,推测为什么 NASA 会禁止这些功能(基本上是链接来自 SO 的相关答案)。由于与 automatic object destruction 相关的未定义行为,在 C++ 中比在 C 代码中更不鼓励使用 setjmplongjmp,请参阅此 SO thread,尤其是接受答案的 cmets:

        通常,只要有某种方法可以退出 C++ 中的范围(返回、抛出或其他),编译器就会放置指令来调用 dtor,以获取因离开该块而需要销毁的任何自动变量。 longjmp() 只是跳转到代码中的新位置,因此它不会为调用 dtor 提供任何机会。该标准实际上没有那么具体——标准并没有说不会调用 dtors——它说所有的赌注都没有。在这种情况下,您不能依赖任何特定的行为。

        [...]

        由于智能指针依赖于被销毁,你会得到未定义的行为。未定义的行为很可能包括引用计数未递减。使用longjmp() 是“安全的”,只要您没有将导致调用 dtor 的代码中的 longjmp 退出。然而,正如 David Thornley 在评论中指出的那样,setjmp()/longjmp() 即使在直接的 C 语言中也很难正确使用——在 C++ 中它们是非常危险的。尽可能避免使用它们。

        那么是什么让setjmp()/longjmp() 在 C 中变得棘手?看看可能的use cases,我们可以看到其中之一是协程的实现。 cmets @StoryTeler 中已经给出了答案,但是您可以使用goto across different functions 吗?

        你不能在标准 C 中;标签是单个函数的本地标签。

        最接近的标准等效函数是 setjmp() 和 longjmp() 函数对。

        但是,setjmplongjmp 也非常有限,您可能很快就会遇到segfault。宝藏又可以在 cmets 中找到了:

        您可以将longjmp() 视为“扩展返回”。一个成功的longjmp() 就像一系列连续的返回一样,展开调用堆栈,直到它到达相应的setjmp()。一旦调用堆栈帧被展开,它们就不再有效。这与调用堆栈在跳转到其他地方后仍然有效的协程(例如 Modula-2)或延续(例如 Scheme)的实现形成对比。 C 和 C++ 仅支持单个线性调用堆栈,除非您使用创建多个独立调用堆栈的线程。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-14
          • 1970-01-01
          • 2014-04-20
          • 2013-09-07
          • 2021-11-28
          相关资源
          最近更新 更多