比goto还混乱的跳转

  • Post author:
  • Post category:其他


一 前言

在初学c的时候,学到goto这一部分内容,无论是书中还是老师们的讲课一般都告诫学生慎用goto语句,后继的语言多数取消了goto这个关键字,比如java,采用break加标签的形式来实现深层嵌套的退出。原因是乱用goto语句常常让程序乱如面条,非常可怕。C语言的创造者对goto的跳转范围还是做了限制,即只能在同一个函数中跳转,不能跨越函数随便跳转。这次来聊的跳转函数

setjmp



longjmp

,以及

sigsetjump



siglongjump

这两对函数可以完成更加疯狂的举动,在C程序内任意跳转。如果说原来的goto语句还只是如面条在碗里散乱,还好是在一个碗里,而这两对函数就如同面条在不同的碗里乱,想捋顺程序的逻辑将更加困难。

所以虽然初学c的时候就了解了这两个特殊函数,这么多年的开发中却从来没有使用过。这次看CSAPP刚好看到,顺便写几个例子来巩固下,或许以后用的到。

二 简介

上面说的跳转函数被创造出来的初衷是为了C语言的异常控制跳转,可以完成从一个函数直接跳转到另外一个函数,而不要经过层层的返回,简化异常处理的流程。

#include <setjump.h>
int setjump(jmp_buf env);
int sigsetjump(sigjmp_buf env,int savesigs);

我们知道函数的调用其实就是压栈,函数返回就是出栈,上述函数是将函数的栈等信息保存到环境变量中,然后在跳转的时候直接恢复,恢复函数或跳转函数如下:

#include <setjmp.h>
void longjump(jmp_buf env,int retval);
void siglongjump(sigjmp_buf env, int retval);

函数还比较好理解,

setjump

保存此时的栈信息等到env中,

longjump

跳转到env指定的栈位置。

setjump

比较特殊,设置的时候返回0,如果是从

longjump

跳过来的,

setjump

会返回

longjump

中的参数:

retval

。这个有点类似

fork

返回两次,在子进程中返回0,在父进程中返回子进程的id。

为什么要有另外一对跳转函数那?

sigsetjump



siglongjump

,那是因为如果在信号处理函数里面利用

longjump

跳转,就不会清理此信号,就导致这个信号会被程序一直屏蔽(程序在处理一个信号的时候再来相同的信号是被忽略的,处理完毕后,才可以再次接受这个信号),这个就像个bug。

savesigs

为非0的时候,表示同时阻塞的信号集合也被保存在env中。

三 实践

我们按照csapp书中的例子练习下。

#include "csapp.h"

jmp_buf buf;

int error1 = 1;
int error2 = 1;

void foo(int*), bar(void);

int main()
{
    int a = 123;
    int rc=5;
    rc = setjmp(buf);
    // 第一次设置buf 返回0
    if (rc == 0) {
        printf("a is %d\n",a);
        foo(&a);
    } else if (rc == 1) {
        printf("longjmp ret a is %d\n",a);
        printf("Detect an error1 condition in foo.\n");
    } else if (rc == 2) {
        printf("Detect an error2 condition in foo.\n");
    } else {
        printf("unkown eror condition in foo.\n");
    }
    exit(0);
}

void foo(int * pa)
{
    if (error1) {
        *pa=456;
        longjmp(buf, 1);
    }
    bar();
}

void bar(void)
{
    if (error2) {
        longjmp(buf, 2);
    }
}

返回:

[root@localhost tinyweb]# ./setjmp
a is 123
longjmp ret a is 456
Detect an error1 condition in foo.

从上面的情况我们可以看出,setjmp只保存栈的信息和寄存器的信息,并没有恢复局部变量的值。

再看一个清理信号跳转:

#include "csapp.h"

sigjmp_buf buf;
int i = 0;

void exithandle(int sig)
{
  exit(0);
}

void handler(int sig)
{

    if (i++ < 5) {
        siglongjmp(buf, 1);
    } else {
        printf("loop %d times.", i);
        signal(SIGINT,exithandle);
    }
}

int main()
{

    signal(SIGINT, handler);
    if (!sigsetjmp(buf, 1)) {
        printf("Starting.\n");
    } else {
        printf("Restarting .\n");
    }
    while (1) {
        sleep(1);
        printf("Sleeping..\n");
    }
    exit(0);
}

运行结果(按ctrl+C发送SIGINT信号),默认行为是程序终止。

[root@localhost tinyweb]# ./sigjmp
Starting.
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
^CRestarting .
Sleeping..
^CRestarting .
Sleeping..
^Cloop 6 times.Sleeping..
Sleeping..
Sleeping..
^C

假如我们用不支持信号保存的跳转函数来修改第二个程序,打印内容如下:

[root@localhost tinyweb]# ./sigjmp
Starting.
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
^CSleeping..
^CSleeping..
Sleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^C^CSleeping..

这是因为用longjmp会跳过信号屏蔽的清理,所以除了第一次收到了信号外,后续的信号都被屏蔽了,无法接收到,只能被kill掉。

四 诗词欣赏

木兰花·拟古决绝词柬友
      - 纳兰性德

人生若只如初见,何事秋风悲画扇。
等闲变却故人心,却道故人心易变。
骊山语罢清宵半,泪雨霖铃终不怨。
何如薄幸锦衣郎,比翼连枝当日愿。