longjmp 应该恢复堆栈吗?

Is longjmp supposed to restore the stack?

据我了解,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++...:/

对于 setjmplongjmp "context" 是 执行 上下文,而不是堆栈的实际内容(其中局部变量是通常存储)。

使用 setjmplongjmp 您不能 "roll back" 对局部变量进行更改。

那是 expected behavior

Upon return to the scope of setjmp, all accessible objects, floating-point status flags, and other components of the abstract machine have the same values as they had when std::longjmp was executed, except for the non-volatile local variables in setjmp's scope, whose values are indeterminate if they have been changed since the setjmp invocation.

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

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

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

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

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

除了 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.

“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() returns 0 的原因,当你检查条件时,它分配 a=15,一旦过程完成,下一步它将给出 42。

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

我只想回答问题的另一部分,通过推测为什么 NASA 会禁止这些功能(基本上链接来自 SO 的相关答案)。由于关于 automatic object destruction, see this SO thread 的未定义行为,特别是对已接受答案的评论:

,因此在 C++ 中比在 C 代码中更不鼓励使用 setjmplongjmp

Generally, whenever there's some way to exit a scope in C++ (return, throw, or whatever), the compiler will place instructions to call the dtors for any automatic variables that need to be destroyed as a result of leaving that block. longjmp() just jumps to a new location in the code, so it will not provide any chance for the dtors to be called. The standard is actually less specific than that - the standard doesn't say that dtors won't be called - it says that all bets are off. You can't depend on any particular behavior in this case.

[...]

Since smart pointers depend on being destroyed, you will get undefined behavior. It's likely that that undefined behavior would include a refcount not getting decremented. You're 'safe' using longjmp() as long as you don't longjmp out of code that should cause dtors to be invoked. However, as David Thornley noted in a comment, setjmp()/longjmp() can be tricky to use right even in straight C - in C++ they're downright dangerous. Avoid them if at all possible.

那么是什么让 setjmp()/longjmp() 在 C 语言中变得棘手?看看可能的use cases we can see that one of them is implementation of coroutines. The answer was already given here in the comments @StoryTeler, but could you use goto across different functions?

You can't in Standard C; labels are local to a single function.

The nearest standard equivalent is the setjmp() and longjmp() pair of functions.

但是,您在 setjmplongjmp 方面的能力也很有限,您可能很快就会 运行 变成 segfault。宝物又可以在评论中找到了:

You can think of a longjmp() as an "extended return". A successful longjmp() works like a series of successive returns, unwinding the call stack until it reaches the corresponding setjmp(). Once the call stack frames are unwound, they are no longer valid. This is in contrast to implementations of coroutines (eg. Modula-2) or continuations (eg. Scheme) where the call stack remains valid after jumping somewhere else. C and C++ only support a single linear call stack, unless you use threads where you create multiple independent call stacks.