函数原型
#include <setjmp.c>
int setjmp(jmp_buf);
Retuns 0 on initial call, nonzero on return longjmp()
void longjmp(jmp_buf env, val);
功能
能够让函数从嵌套的函数中跳出,返回到函数的调用者之一(当前调用者或者调用者的调用者,等等)。
工作流程
setjmp()
为后续的longjmp()
确立跳转目标,该目标是发起setjmp()
调用的地方。- 初次调用
setjmp()
确立跳转目标时,setjmp()
返回0。后续返回会返回longjmp()
跳转时携带的参数val
的值。为了作以区分是初次调用还是后续返回,规定此值不为0,如果为0,longjmp()
实际上会将其替换为1。 env
为跳转的实现提供黏合剂。setjmp()
会把当前进程环境的各种信息保存进env
参数中,调用longjmp()
时必须指定相同的env
变量来执行“伪”返回。(由于setjmp()
和longjmp()
的调用分别位于不同函数,所以应该将env
参数定义为全局变量,或者作为函数入参来传递。)
原理
- 调用
setjmp()
时,env
会保存当前进程的环境信息,还保存程序计数寄存器(指向当前正在执行的及其语言指令)和栈指针寄存器(标记栈顶)的副本。这些信息后续用来完成longjmp()
中两个关键步骤。 - 将发起
longjmp()
调用的函数与之前调用setjmp()
的函数之间的函数栈帧从栈上剥离(相当于之间的函数从未调用过)。这个过程也称为“解开栈(unwinding the stack)”,是通过栈指针寄存器重置为env
参数内的保存值来实现的。 - 重置程序计数寄存器。这使得程序从原来的位置继续执行,也是通过
env
保存的值来实现的。
示例
/*jmp.c*/
#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include "myErr.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()*/
f1(argc); /*Never returns*/
break;
case 1:
printf("We jumped back from f1()\n");
break;
case 2:
printf("We jumped back from f2()\n");
break;
}
exit(EXIT_SUCCESS);
}
$gcc -o jmp jmp.c
$./jmp
We jumped back from f1()
$./jmp -abcdefg
We jumped back from f2()
标准限制
SUSv3和C99规定,对setjmp()
的调用只能在如下语境中使用:
- 构成选择或迭代语句中的(if、switch、while等)整个控制表达式。
- 作为一元操作符!(not)的操作对象(其最终表达式也构成了选择或迭代语句的整个控制表达式)
- 作为比较操作(==、!=、<等)的一部分,另一个操作数必须是一个整数常量表达式。且其最终表达式构成选择或迭代语句的整个控制表达式。
- 作为独立的函数调用,且没有嵌入到更大的表达式之中。
注意,C语言赋值语句不在上述列表之列,以下形式的语句是不符合标准的:
s = setjmp(env);
原因
之所以有这些限制,是因为作为常规函数的setjmp()
实现无法保证拥有足够信息来保存所有寄存器值和封闭表达式中用到的临时栈位置,以便于在longjmp()
调用后此类信息能得到正确恢复。因此,仅允许在足够简单且无需临时存储的表达式中调用setjmp()
。
滥用后果
如果将env
缓冲区定义为全局变量,对所有函数可见(这也是通常用法),那么就可以执行如下操作序列。
- 调用函数x(),使用
setjmp()
调用在全局变量env
中建立一个跳转目标。 - 从函数x()返回
- 调用函数y(),使用
env
变量调用longjmp()
函数
这是一个严重错误,因为longjmp()
调用不能跳转到一个已经返回的函数中。在这种情况下,longjmp()
会尝试将栈解开(也就是剥离setjmp()
和longjmp()
调用函数之间的栈帧),恢复到一个不存在的栈帧位置,这会引起混乱。程序可能崩溃(crash),也可能会引起调用和返回间的死循环,而程序好像真地从一个当前并未执行的函数中返回了。(多线程程序中有相类似的滥用,如在线程甲中调用setjmp()
,却在线程乙中调用longjmp()
。)
SUSv3规定,从嵌套的信号处理器(signal handler)(即信号甲的处理器正在运行时,又发起对信号乙处理器的调用)中调用longjmp()
函数,该程序的行为为定义。
优化编译器的问题
起因
优化编译器会重组程序的指令执行顺序,并在CPU寄存器中,而非RAM中存储某些变量。
这种优化一般依赖于反映了程序此法结构的运行时(run-time)控制流程。由于setjmp()
和longjmp()
的跳转操作需在运行时才能得以正确确立和执行,并未在程序的词法结构中有所反映,故而编译器在进行优化时也无法将其考虑在内。
此外,某些应用程序二进制接口(ABI)实现的语义又要求longjmp()
函数恢复先前setjmp()
调用所保存的CPU寄存器副本。这意味着,longjmp()
操作会致使经过优化的变量被赋以错误的值。
示例
/*volatile.c*/
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf env;
static void
doJump(int nvar, int rvar, int vvar)
{
printf("Inside doJump(): nvar=%d, rvar=%d, vvar=%d\n"
, nvar,rvar, vvar);
longjmp(env, 1);
}
int main(int argc, char** argv)
{
int nvar;
register int rvar; /*Allocated in register if possible*/
volatile int vvar;
nvar = 111;
rvar = 222;
vvar = 333;
if(setjmp(env) ** 0)
{
nvar = 777;
rvar = 888;
vvar = 999;
doJump(nvar, rvar, vvar);
}
else
{
printf("After longjmp(): nvar =%d, rvar=%d, vvar=%d\n", nvar, rvar, vvar);
}
exit(EXIT_SUCCESS);
}
/*不优化编译*/
$gcc -o volatile volatile.c
$./volatile
Inside doJump(): nvar=777, rvar=888, vvar=999
After longjmp(): nvar =777, rvar=222, vvar=999
/*优化编译*/
$gcc -O -o volatile volatile.c
$./volatile
Inside doJump(): nvar=777, rvar=888, vvar=999
After longjmp(): nvar =111, rvar=222, vvar=999
此处在longjmp()
调用后,nvar和rvar被重置为setjmp()
初次调用时的值。起因是优化器对代码的重组受到longjmp()
调用的干扰。作为候选优化对象的任一局部变量都难免遇到这类问题,一般包含指针变量和char、int、float、long等任何简单类型的变量。
将变量声明为volatile
,是告诉优化器,不要对其进行优化,从而避免了代码重组。无论编译优化与否,声明为volatile
的变量都得到正确处理。
因为不同的优化器有不同的优化方法,具备良好移植性的程序应该在调用setjmp()
的函数中,将上述类型的所有局部变量都声明为volatilw
。
若在GNU C语言编译器中加入 -Wextra(产生额外的警告信息选项),程序的编译结果将显示又帮助的警告信息。
参考资料:《Linux/UNIX系统编程手册》