美文网首页
比goto还混乱的跳转

比goto还混乱的跳转

作者: 明翼 | 来源:发表于2020-06-15 22:34 被阅读0次

    一 前言

    在初学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会跳过信号屏蔽的清理,所以除了第一次收到了信号外,后续的信号都被屏蔽了,无法接收到。

    四 诗词欣赏

    木兰花·拟古决绝词柬友
          - 纳兰性德
    
    人生若只如初见,何事秋风悲画扇。
    等闲变却故人心,却道故人心易变。 
    骊山语罢清宵半,泪雨霖铃终不怨。 
    何如薄幸锦衣郎,比翼连枝当日愿。
    

    相关文章

      网友评论

          本文标题:比goto还混乱的跳转

          本文链接:https://www.haomeiwen.com/subject/yahcxktx.html