C语言 关于setjmp/longjmp

i5desfxk  于 2023-06-05  发布在  其他
关注(0)|答案(6)|浏览(163)

我在研究setjmp/longjmp,发现setjmp保存了指令指针、堆栈指针等寄存器。
但是我在这里没有得到的是,在调用setjmplongjmp之间,线程本身的堆栈中的数据不能被修改吗?在这种情况下,longjmp不会像预期的那样工作。
为了清楚起见,例如,当longjmp恢复堆栈指针时,假设堆栈指针现在指向的内存中的数据与调用setjmp时的数据不同。这会发生吗?如果真的发生了我们不是有麻烦了吗
同样,语句“* 在调用setjmp()例程的例程返回后,longjmp()例程可能不会被调用。

92vpleto

92vpleto1#

堆栈指针标记堆栈的“已使用”和“未使用”部分之间的划分。当你调用setjmp时,所有当前的调用帧都在“已使用”的一侧,而在setjmp之后,但在调用setjmp的函数返回之前发生的任何调用,其调用帧都在保存的堆栈指针的“未使用”一侧。请注意,在调用setjmp的函数返回后调用longjmp会调用未定义的行为,因此不需要考虑这种情况。
现在,一些现有调用帧中的局部变量可能在setjmp之后被修改,无论是通过调用函数还是通过指针,这就是为什么在许多情况下需要使用volatile的原因之一。

p4tfgftt

p4tfgftt2#

setjmp()/longjmp()并不意味着保存堆栈,这就是setcontext()/getcontext()的作用。
该标准规定,在调用setjmp()的函数中定义的在setjmp()longjmp()调用之间更改的非易失性自动变量的值在longjmp()之后未指定。出于同样的原因,对如何调用setjmp()也有一些限制。

gev0vcfq

gev0vcfq3#

C中的setjmp/longjmp(以下简称slj)功能很难看,其行为可能因实现而异。尽管如此,由于没有异常,slj有时在C中是必要的(注意,C提供的异常几乎在所有方面都上级slj,而且slj与许多C特性的交互都很糟糕)。
在使用slj时,应该记住以下内容,假设例程Parent()调用例程Setter(),后者调用setjmp(),然后调用Jumper,后者又调用longjmp()。
1.代码可以合法地退出执行setjmp而没有执行longjmp的范围;然而,一旦范围退出,先前创建的jmp_buf就必须被视为无效。编译器可能不会做任何事情来标记它,但任何使用它的尝试都可能导致不可预知的行为,可能包括跳转到任意地址。

  1. Jumper()中的任何局部变量都将随着对longjmp()的调用而消失,从而使它们的值变得无关紧要。
    1.无论何时控制权通过何种方式返回到Parent,Parent的局部变量都将与调用Setter时一样,除非这些变量的地址已被获取并使用此类指针进行更改;在任何情况下,setjmp/longjmp都不会以任何方式影响它们的值。如果这样的变量没有获取它们的地址,则setjmp()可以缓存这样的变量的值,并且longjmp()可以恢复它们。然而,在这种情况下,变量在缓存时和恢复时之间没有办法改变,因此该高速缓存/恢复将没有可见的效果。
  2. Setter中的变量可能会也可能不会被setjmp()调用缓存。在longjmp()调用之后,这样的变量可以具有它们在执行setjmp()时具有的值,或者它们在调用最终调用longjmp()的例程时具有的值,或者它们的任何组合。在至少一些C语言中,这样的变量可以被声明为“volatile”,以防止它们被缓存。
    尽管setjmp/longjmp()有时候很有用,但它们也可能非常危险。在大多数情况下,没有保护错误的代码导致未定义的行为,并且在许多现实世界的场景中,不正确的使用可能会导致不好的事情发生(不像某些类型的未定义的行为,其中实际结果通常可能与程序员的意图一致)。
zazmityj

zazmityj4#

在下面的例子中,setjmp / longjump通过指针改变了位于main中的i的值。I在for循环中从不递增。如需更多乐趣,请参阅1992年IOCCC的http://www.ioccc.org/years-spoiler.html赢家者albert.c。(这是我为数不多的几次阅读C源代码的时候...)

#include <stdio.h>
#include <setjmp.h>

jmp_buf the_state;

void helper(int *p);
int main (void)
{
int i;

for (i =0; i < 10;    ) {
    switch (setjmp (the_state) ) {
    case 0:  helper (&i) ; break;
    case 1:    printf( "Even=\t"); break;
    case 2:    printf( "Odd=\t"); break;
    default: printf( "Oops=\t"); break;
        }
    printf( "I=%d\n", i);
    }

return 0;
}
void helper(int *p)
{
*p += 1;
longjmp(the_state, 1+ *p%2);
}
xvw2m8pv

xvw2m8pv5#

在数据处理的情况下使用setjmp()longjmp()有点没用。所以,这就是你可能会关心的自动变量的情况。变量可以在堆栈槽或寄存器中。如果它们在堆栈槽中,则不会通过沿着堆栈弹出上下文来恢复堆栈。相反,堆栈被立即倒回并且被调用方保存的寄存器被恢复。
例程使用的堆栈空间是保留的,其他例程不应使用它。因此,如果编译器知道该变量存储在堆栈(setjmp())之前,那么它可以在返回时检索它。它只会混淆基于“基本块”的分析,而setjmp()/longjmp()违背了这种分类。
我想问为什么有人会在这种情况下使用setjmp()/longjmp()。在libjpeg和Simon Tatham的use in co-routines中可以找到一个很好的用法。也就是说,例程是相当单一的目的。它们要么为可能有许多异常条件的长时间运行的操作设置上下文,要么用作原始调度程序。将实际数据处理与它们混合是一个等待发生的错误(由于其他地方提到的所有原因)。
C标准的子条款7.13.1.1详细说明了可以在何处使用setjmp()。它是一个异常函数,因此将其视为正常函数调用是主要问题。也许语言应该给它一个不同的语法。
同样,语句“在调用setjmp()例程的例程返回后,longjmp()例程可能不会被调用。”
下面是函数e()中的一个例子。

jmp_buf buf;
 
void k(void) {
  /* ... */
  longjmp(buf, 1);
}

void f(void) {
  if (setjmp(buf) == 0) {
    k();
  } else {
    /* longjmp was invoked */
  }
}
 

void e(void) {
  f();
  longjmp(buf, 1);  // ERROR! we will return to f(); popped stack slot.
}

代码序列是e()-> f()-> k()-> f()-> e()-> crash。这有点像使用关闭的文件句柄。也就是说,jmp_buf仍然设置为有效的外观值,但它们指向不存在的堆栈。

r1zhe5dt

r1zhe5dt6#

也就是说,“* 在调用setjmp()例程的例程返回后,longjmp()例程可能不会被调用。
这就是说,你只能longjmp向上调用树/调用堆栈,**到调用setjmp**的父函数。(或者在当前函数的同一个调用中,比如美化的goto,所以它不一定是父函数。
因此,就调用结构而言,它很像try{}catch/throw,其中setjmp设置了一个捕获点,而longjmp类似于throw。堆栈展开和局部变量的语义有些不同,但仍然只是展开调用堆栈。
这就是为什么重用堆栈空间用于其他事情不是问题的原因:在相同的生存期内,在调用setjmp时处于活动状态的局部变量(在自动存储中)必须仍然处于活动状态。
你不能将longjmp返回到一个已经返回的函数中。当然可以,但这是一种未定义的行为。不像try{}catch/throw,因为捕获是基于作用域和函数调用的嵌套,所以如果你不在try内,你就不能跳到catch{}块。
其他的答案进入了关于volatile和其他东西的更多细节;我发布这篇文章是因为其他人似乎缺乏一个简单而明确的声明,即只能跳转到一个尚未达到其生命周期终点的作用域中的setjmp调用站点。

相关问题