longjmp应该还原堆栈吗

Is longjmp supposed to restore the stack?

本文关键字:堆栈 还原 longjmp      更新时间:2023-10-16

根据我的理解,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;
}

免责声明:仅供好奇使用。我从来没有听说过这种结构,直到我最近读到一些关于美国国家航空航天局编码指南的论文,其中提到禁止使用这种控制结构

同时使用c和c++标记,因为代码是混合的,我认为实际的相关函数与c用户更相关,而不是c++…:/

这是预期的行为:

返回到setjmp的作用域后,所有可访问的对象,浮点状态标志和抽象的其他组件机器的值与std::longjmp执行,除了setjmp中的非易失性局部变量范围,如果自CCD_ 5调用

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

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

我只想回答问题的另一部分,推测美国国家航空航天局为什么会禁止这些功能(基本上是将SO的相关答案联系起来)。与C代码相比,C++中不鼓励使用setjmplongjmp,因为关于自动对象销毁的未定义行为,请参阅此so线程,特别是对已接受答案的注释:

通常,每当有某种方法退出C++中的作用域(return、throw或其他)时,编译器都会发出指令,为任何因离开该块而需要销毁的自动变量调用dtor。longjmp()只是跳转到代码中的一个新位置,因此它不会为dtor的调用提供任何机会。这个标准实际上没有那么具体——这个标准并没有说不会调用dtor——它说所有的赌注都没有了。在这种情况下,你不能依赖任何特定的行为。

[…]

由于智能指针依赖于被销毁,您将得到未定义的行为。这种未定义的行为很可能包括一个没有递减的refcount。使用longjmp()是"安全的",只要您没有从应该调用dtor的代码中提取jmp。然而,正如David Thornley在评论中指出的那样,setjmp()/longjmp()即使在直接的C语言中也很难正确使用——在C++中,它们是非常危险的。尽可能避开它们。

那么是什么让setjmp()/longjmp()在C中变得棘手呢?看看可能的用例,我们可以看到其中之一就是协同程序的实现。答案已经在评论@StoryTeler中给出了,但你能在不同的功能中使用goto吗?

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

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

但是,setjmplongjmp也非常有限,您可能很快就会遇到segfault。宝藏可以再次在评论中找到:

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

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

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

我想您可能看到了较旧的代码库。Where异常在某些编译器中不太流行或不可用。

您不应该使用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)
}
}
“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("secondn");         // prints
longjmp(buf,1);             // jumps back to where setjmp was called - making setjmp now return 1
}
void first() {
second();
printf("firstn");          // does not print
}
int main() {   
if (!setjmp(buf))
first();                // when executed, setjmp returned 0
else                        // when longjmp jumps back, setjmp returns 1
printf("mainn");       // prints
return 0;
}

这就是为什么setjump()为您返回0的原因,当您检查条件时,它会指定a=15,一旦过程完成,下一步它会给出42。

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