使用库函数setjmp()和 longjmp()可执行非局部跳转。术语“非局部”是指跳转的目标为当前执行函数之外的某个位置。
C语言的goto存在一个限制,即不能从当前函数跳转到另一函数。然而,偶尔还是需要这一功能的。考虑错误处理中经常出现的如下场景:**在一个深度嵌套的函数调用中发生了错误,需要放弃当前任务,从多层函数调用中返回,并在较高层级的函数中继续执行**。要做到这一点,可以让每个函数都返回一个状态值,由函数的调用者检查并做相应处理。这一方法完全有效,而且,在许多情况下,是处理这类场景的理想方法。然而,有时候如果能从嵌套函数调用中跳出,返回该函数的调用者之一(当前调用者或者调用者的调用者,等等),编码会更为简单。setjmp()和 longjmp()就提供了这一功能
由于在C语言中,所有函数作用域的层级相同(即标准C语言不支持嵌套函数申明,尽管gcc将此功能作为其扩展功能),所以goto不能应用于函数间跳转。给定两个函数X和Y,编译器无从知晓当调用Y时,X函数的栈帧是否在栈上,所以也无法判断从Y函数跳转到X函数是否可行。支持嵌套函数声明的语言,比如 Pascal 语言,允许 goto从一个嵌套函数跳转到其调用者,编译器得以根据函数的静态作用域来确定函数动态作用域的某些信息。因此,编译器若在词法解析时获悉函数 Y 嵌套于函数 X 之内(即静态作用域),也必然能够推断当调用 Y 时,X 函数的栈帧一定已然在栈中存在(即动态作用域),并能为函数 Y 产生 goto 代码,从 Y 中跳转到 X 函数的某处
NAME
setjmp, sigsetjmp - 为非本地goto保存堆栈上下文
SYNOPSIS
#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);
glibc的特性测试宏要求 (see feature_test_macros(7)):
setjmp(): see NOTES.
sigsetjmp(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_C_SOURCE
DESCRIPTION
setjmp() 、longjmp(3) 用于处理在程序的低级子例程中遇到的错误和中断。setjmp()
将堆栈上下文/环境保存在env中,供longjmp(3)以后使用。如果调用setjmp()的函数返回,
堆栈上下文将失效。
sigsetjmp() 和 setjmp()相似. 当且仅当savesigs不为零时,进程的当前信号掩码将
被保存在env中,并在随后使用此env执行siglongjmp(3)时将被恢复。
RETURN VALUE
setjmp() 、sigsetjmp() 如果直接返回则返回0,如果使用保存的上下文从longjmp(3)
或siglongjmp(3)返回则返回非0。
setjmp()调用为后续由 longjmp()调用执行的跳转确立了跳转目标。该目标正是程序发起setjmp()调用的位置。从编程角度看来,调用 longjmp()函数后,看起来就和从第二次调用 setjmp()返回时完全一样。通过查看 setjmp()返回的整数值,可以区分 setjmp 调用是初始返回还是第二次“返回”。初始调用返回值为 0,后续“伪”返回的返回值为 longjmp()调用中 val 参数所指定的任意值。通过对 val 参数使用不同值,能够区分出程序中跳转至同一目标的不同起跳位置。
如果指定 longjmp()函数的 val 参数值为 0,而 longjmp 函数对此又不做检查,就会导致模 拟 setjmp()时返回值为 0,如同初次调用 setjmp()函数返回时一样。出于这一原因,如果指定 val 参数值为 0,则 longjmp()调用实际会将其替换为 1
这两个函数的入参 env 为成功实现跳转提供了黏合剂。setjmp()函数把当前进程环境的各种 信息保存到 env 参数中。调用 longjmp()时必须指定相同的 env 变量,以此来执行“伪”返回。 由于对 setjmp()函数和 longjmp()函数的调用分别位于不同函数(否则,使用简单的 goto 即可), 所以应该将 env 参数定义为全局变量,或者将 env 作为函数入参来传递,后一种做法较为少见。
调用 setjmp()时,env 除了存储当前进程的其他信息外,还保存了程序计数寄存器(指向 当前正在执行的机器语言指令)和栈指针寄存器(标记栈顶)的副本。这些信息能够使后续 的 longjmp()调用完成两个关键步骤的操作
- 将发起 longjmp()调用的函数与之前调用 setjmp()的函数之间的函数栈帧从栈上剥离。 有时又将此过程称为“解开栈(unwinding the stack)”,这是通过将栈指针寄存器重置 为 env 参数内的保存值来实现的
- 重置程序计数寄存器,使程序得以从初始的 setjmp()调用位置继续执行。同样,此功 能是通过 env 参数中的保存值(程序计数寄存器)来实现的
C 库宏 int setjmp(jmp_buf environment) :创建本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值。
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void) {
printf("second\n"); // 打印
longjmp(buf,1); // 跳回setjmp的调用处 - 使得setjmp返回值为1
}
void first(void) {
second();
printf("first\n"); // 不可能执行到此行
}
int main() {
if ( ! setjmp(buf) ) {
first(); // 进入此行前,setjmp返回0
} else { // 当longjmp跳转回,setjmp返回1,因此进入此行
printf("main\n"); // 打印
}
return 0;
}
longjump的第二个参数是影响(而不是被影响)setjmp的返回值的。
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
int main(void)
{
int i;
printf("%d\n",i = setjmp(buf));//第一次输出0,第二次输出longjmp的第二个参数。
if (i==0)
longjmp(buf,3);//可自行修改第二参数查看不同结果。
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf env;
static void f2(void)
{
longjmp(env, 2);
}
static void f1(int argc)
{
if (argc == 1)
longjmp(env, 1);
f2();
}
int main(int argc, char *argv[])
{
switch (setjmp(env)) {
case 0: /* This is the return after the initial setjmp() */
printf("Calling f1() after initial setjmp()\n");
f1(argc); /* Never returns... */
break; /* ... but this is good form */
case 1:
printf("We jumped back from f1()\n");
break;
case 2:
printf("We jumped back from f2()\n");
break;
}
exit(EXIT_SUCCESS);
}
对 setjmp()函数的使用限制
SUSv3 和 C99 规定,对 setjmp()的调用只能在如下语境中使用。
- 构成选择或迭代语句中(if、switch、while 等)的整个控制表达式。
- 作为一元操作符!(not)的操作对象,其最终表达式构成了选择或迭代语句的整个控 制表达式。
- 作为比较操作(==、!=、<等)的一部分,另一操作对象必须是一个整数常量表达式, 且其最终表达式构成选择或迭代语句的整个控制表达式。
- 作为独立的函数调用,且没有嵌入到更大的表达式之中。
注意:C 语言赋值语句不在上述列表之列。以下形式的语句是不符合标准的
s = setjmp(env);
之所以规定这些限制,是因为作为常规函数的 setjmp()实现无法保证拥有足够信息来保存所 有寄存器值和封闭表达式中用到的临时栈位置,以便于在longjmp()调用后此类信息能得以正确恢 复。因此,仅允许在足够简单且无需临时存储的表达式中调用 setjmp()
滥用 longjmp()
如果将 env 缓冲区定义为全局变量,对所有函数可见(这也是通常用法),那么就可以执 行如下操作序列。
- 调用函数 x(),使用 setjmp()调用在全局变量 env 中建立一个跳转目标。
- 从函数 x()中返回。
- 调用函数 y(),使用 env 变量调用 longjmp()函数。
这是一个严重错误,因为 longjmp()调用不能跳转到一个已经返回的函数中。思考一下, 在这种情况下,longjmp()函数会对栈打什么主意—尝试将栈解开,恢复到一个不存在的栈 帧位置,这无疑将引起混乱。如果幸运的话,程序会一死(crash)了之。然而,取决于栈的 状态,也可能会引起调用与返回间的死循环,而程序好像真地从一个当前并未执行的函数中 返回了。(在多线程程序中有与之相类似的滥用,在线程某甲中调用 setjmp()函数,却在线程 某乙中调用 longjmp()。
SUSv3 规定,如果从嵌套的信号处理器(signal handler)(即信号某甲的处理器正在运行 时,又发起对信号某乙处理器的调用)中调用 longjmp()函数,则该程序的行为未定义。
优化编译器的问题
优化编译器会重组程序的指令执行顺序,并在 CPU 寄存器中,而非 RAM 中存储某些变 量。这种优化一般依赖于反映了程序词法结构的运行时(run-time)控制流程。由于 setjmp() 和 longjmp()的跳转操作需在运行时才能得以确立和执行,并未在程序的词法结构中有所反映, 故而编译器在进行优化时也无法将其考虑在内。此外,某些应用程序二进制接口(ABI)实现 的语义要求 longjmp()函数恢复先前 setjmp()调用所保存的 CPU 寄存器副本。这意味着 longjmp() 操作会致使经过优化的变量被赋以错误值。看个例子:
以常规方式编译程序,输出结果符合预期
然而,若以优化方式编译该程序,结果就有些出乎预料了。
此处,在 longjmp()调用后,nvar 和 rvar 参数被重置为 setjmp()初次调用时的值。起因是 优化器对代码的重组受longjmp()调用的干扰。作为候选优化对象的任一局部变量可能都 难免会遇到这类问题,一般包含指针变量和 char、int、float、long等任何简单类型的变量
将变量声明为 volatile,是告诉优化器不要对其进行优化,从而避免了代码重组。在上面 的程序输出中,无论编译优化否,声明为 volatile 的变量 vvar 都得到了正确处理。
因为不同的优化器有着不同的优化方法,具备良好移植性的程序应在调用 setjmp()的函数 中,将上述类型的所有局部变量都声明为 volatile。
若在 GNU C 语言编译器中加入–Wextra(产生额外的警告信息)选项,setjmp_vars.c 程序 的编译结果将显示有帮助的警告信息如下
尽可能避免使用 setjmp()函数和 longjmp()函数
果说 goto 语句会使程序难以阅读,那么非局部跳转会让事情的糟糕程度增加一个数 量级,因为它能在程序中任意两个函数间传递控制。因此,应当慎用 setjmp()函数和 longjmp() 函数。在设计和编码时花点心思来避免使用这两个函数,这通常是值得的。程序更具可读 性,可能会更具可移植性。话虽如此,但在编写信号处理器时,这些函数偶尔还会派上用 场—讨论信号时将重新论及这些函数的变体(sigsetjmp()函数和siglongjmp() 函数)
总结
setjmp()函数和 longjmp()函数提供了从函数某甲执行非局部跳转到函数某乙(栈解开)的方 法。在调用这些函数时,为避免编译器优化所引发的问题,应使用 volatile 修饰符声明变量。 非局部跳转会使程序难于阅读和维护,应尽量避免使用。