非局部跳转函数 setjmp 和 longjmp 介绍

    C 语言中的 goto 语句是不能跨越函数的,属于局部性跳转,若要执行跨函数跳转,可使用 setjmp 和 longjmp 函数,它们对于处理发生在很深层嵌套函数调用中的出错情况时非常有用。
#include <setjmp.h>
int setjmp(jmp_buf env);
                    /* 返回值:若直接调用,返回 0;若从 longjmp 返回,则为非 0 */
void longjmp(jmp_buf env, int val);

    setjmp 的参数 env 是一个特殊类型 jmp_buf,这种类型是某种形式的数组,其中存放了在调用 longjmp 时能用来恢复栈状态的所有信息。因为需要在另一函数中引用 env 变量,所以通常将 env 定义为全局变量。
    longjmp 的第一个参数就是在调用 setjmp 时所用的 env,第二个 val 应是一个非 0 值,它将成为从 setjmp 处返回的值。使用第二个参数是因为对于一个 setjmp 可以有多个 longjmp,因此通过测试返回值就可判断造成返回的 longjmp 是位于哪个函数。
    想象一下有这样的几个函数调用:main 函数调用 do_line 函数,do_line 函数又调用一个 cmd_add 函数,下图显示了调用 cmd_add 之后栈通常的大致使用情况(虽然栈并不一定要像低地址方向扩充,例如在某些没有对栈提供特殊硬件支持的系统上,栈帧可能是用链表实现的,但这是一种典型的栈安排)。

    如果某个时候在 cmd_add 函数中遇到了一个非致命性的错误,那可能不得不以检查返回值的方法逐层返回到 main,这可能会变得很麻烦。而如果利用 setjmp 和 longjmp 函数,就可在栈上跳过若干调用帧,直接返回到当前函数调用路径上的某一个函数中。所以如果在 main 中预先使用 setjmp 设置了跳转标记,那么当在 cmd_add 中遇到非致命性的错误时,就可调用 longjmp 使栈反绕到执行 main 函数时的情况,也就是抛弃了 cmd_add 和 do_line 的栈帧,如同下图所示。

    但接下来的问题是:当 longjmp 返回到 main 函数时,其中的自动变量和寄存器变量等的值是否能恢复到以前调用 setjmp 时的值?遗憾的是,对此问题的回答是“看情况”。大多数实现并不回滚这些自动变量和寄存器变量的值,而所有标准则称它们的值是不确定的。如果你有一个自动变量,而又不想使其值回滚,则可定义其为具有 volatile 属性。声明为全局变量和静态变量的值在调用 longjmp 时保持不变。
    下面这个程序说明了在调用 longjmp 后各种类型的变量的变化情况。
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static void f1(int, int, int, int);
static void f2(void);

static jmp_buf	jmpbuffer;
int	exteval;
static int	globval;

int main(void){
	int		autoval = 2;
	register int	regival	= 3;
	volatile int	volaval	= 4;
	static int	statval = 5;

	exteval = 0;
	globval = 1;

	if(setjmp(jmpbuffer) != 0){
		printf("after longjmp:\n");
		printf("exteval=%d, global=%d, autoval=%d, regival=%d, "
				"volaval=%d, statval=%d\n",
				exteval, globval, autoval, regival, volaval, statval);
		exit(0);
	}
	// Change variables after setjmp, buf before longjmp.
	exteval = 94; globval = 95; autoval = 96;
	regival = 97; volaval = 98; statval = 99;

	f1(autoval, regival, volaval, statval);	// never returns
	exit(0);
}

static void f1(int i, int j, int k, int l){
	printf("in f1():\n");
	printf("exteval=%d, global=%d, autoval=%d, regival=%d, "
			"volaval=%d, statval=%d\n",
			exteval, globval, i, j, k, l);
	f2();
}

static void f2(void){
	longjmp(jmpbuffer, 1);
}

    如果以带优化和不带优化选项编译后运行本程序,得到的结果是不一样的。
$ gcc longjmpDemo.c -o longjmpDemo.out    # 不进行任何优化的编译
$ ./longjmpDemo.out 
in f1():
exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99
after longjmp:
exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99
$ 
$ gcc -O longjmpDemo.c -o longjmpDemo2.out   # 进行全部优化的编译
$ ./longjmpDemo2.out 
in f1():
exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99
after longjmp:
exteval=94, global=95, autoval=2, regival=3, volaval=98, statval=99
$ 

    由此可见,全局变量、静态变量和易失变量不受优化的影响,在 longjmp 之后,它们的值是最近所呈现的值。setjmp 的手册页上说明,存放在存储器中的变量将具有 longjmp 时的值,而在 CPU 和浮点寄存器中的变量则恢复为调用 setjmp 时的值。由于当不进行优化时,这几个变量都存放在存储器中(即忽略了 register 存储类说明),而进行了优化后,autoval 和 regival 都存放在寄存器中(即使 autoval 没有用 register 说明),volatile 变量则仍存放在存储器中,所以才能看到上面的输出情况。因此若要编写一个使用非局部跳转的可移植程序,应该使用 volatile 属性。但是从一个系统移植到另一个系统,其他任何事情都可能改变。
    而关于自动变量,还有一种潜在的出错情况。基本规则是声明自动变量的函数返回后,不能再引用这些自动变量。下面这个函数就说明了自动变量的不正确使用的情况:它为它所打开的一个标准 I/O 流设置缓冲。
#include <stdio.h>

FILE *open_data(void){
	FILE *fp;
	char databuf[BUFSIZE];	// setvbuf makes this the stdio buffer.
	if((fp=fopen("datafile", "r")) == NULL)
		return NULL;
	if(setvbuf(fp, databuf, _IOLBF, BUFSIZE) != 0)
		return NULL;
	return fp;		// error
}

    这里存在的问题是,当 open_data 返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。但是,标准 I/O 库函数仍将使用这部分存储空间作为该流的缓冲区,这就产生了冲突和混乱。所以正确的做法应是在全局存储空间静态地(如 static 或 extern)或者动态地为数组 databuf 分配空间。

猜你喜欢

转载自aisxyz.iteye.com/blog/2391169