比goto还混乱的跳转

一 前言

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

为什么要有另外一对跳转函数那?sigsetjumpsiglongjump,那是因为如果在信号处理函数里面利用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掉。

四 诗词欣赏

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值