C语言之setjmp和longjmp详细剖析

我希望看这篇文章的你对C++的传统异常处理,即try...catch...throw有了解(不是Windows SEH),这样才能方便你最深入的理解这2个C语言的反人类函数。

当然如果不了解就先看下面的“C++式的异常处理”,如果感觉自己了解了,可以直接skip看到“C语言中的模拟”。

【C++式的异常处理】

首先,我们写一个类,请不要想这个类有什么特别的地方,其只是为了打印出来构造和析构。

class CFoo
{
public:
    CFoo()
    {
        printf("Create CFoo.\n");
    }
    ~CFoo()
    {
        printf("~Destroy CFoo.\n");
    }
};

然后我们写一个函数,这个函数foo是为了根据情况抛出异常:

void foo(int exp)
{
    if (exp == 'a')
        throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

我们来写第一个main:

int main()
{
    int val = getchar();
    foo(val);

    return 0;
}

此时我们输入b,其输出的肯定是:

foo ok 98.

这里98是b的ascii值。

而我们输入a,则会出情况了:

因为foo抛出了一个异常,但是没例程去处理他,所以程序崩溃。

所以我们现在在main上加上处理foo异常的代码:

int main()
{
    int val = getchar();
    try{
        foo(val);
    }catch (std::exception& ex)
    {
        printf("skip ex:%s.\n",ex.what());
    }

    return 0;
}

好了,我们再次输入a,则会出现:

skip ex:a.

foo在throw下正常的printf则不会执行,流程被改变。

所以我们可以简单理解为throw是一个“带有异常信息的”return,当然实际情况比这个复杂的多,我这样说只是为了让你有一种C语言的感觉。

还记得上面那个CFoo嘛,我一直没使用它,现在我们把foo函数改一下:

void foo(int exp)
{
    CFoo cfoo;
    if (exp == 'a')
        throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

可以看到我只加了一行代码,在堆栈上开了一个cfoo的实例,我们main不动,输入一个p试试:

Create CFoo.

foo ok 112.

~Destroy CFoo.

可以看到,其输出了CFoo的构造和析构,这个是正常的情况,因为我们看到printf执行了。

那我们输入a呢,我们来尝试:

Create CFoo.

~Destroy CFoo.

skip ex:a.

我们可以看到,虽然throw下面的printf没有被执行,但是CFoo被构造和析构了,这就是C++异常会遵循C++的栈上展开的特点,也就是即便发生异常了,throw前的栈上对象,都需要被析构,如果他们有“真正的”析构代码的话。

在执行析构的时候情况也是十分复杂,这里不扯那么多,因为这文章不是介绍C++异常处理的。。。

不过为了让你看得更清除点,我们再来把CFoo函数改一下,也是一行代码:

void foo(int exp)
{
    CFoo cfoo;
    if (exp == 'a')
        throw std::exception("a");

    CFoo cfoo2;
    printf("foo ok %d.\n",exp);
}

我们再次输入p:

Create CFoo.

Create CFoo.

foo ok 112.

~Destroy CFoo.

~Destroy CFoo.

可以看到这是输出,好,我们输入a:

Create CFoo.

~Destroy CFoo.

skip ex:a.

可以很明显的看到,因为cfoo2构造在throw下面,所以它在异常导致foo进行return的时候,并不需要被析构,因为它并没有生成一个真正的实例。

好了到这里你就算不懂C++异常处理可能也可以入门了(如果你有兴趣的话)。

【C语言中的模拟】

这里我们开始正式说一下setjmplongjmp

如果上面那个foo函数:

void foo(int exp)
{
    if (exp == 'a')
        throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

因为在C语言中没有C++异常,foo一般使用一个返回值来交出结果判断失败,然后调用者根据返回值进行流程控制,比如foo我们可以写成:

bool foo(int exp)
{
    if (exp == 'a')
        return false;

    printf("foo ok %d.\n",exp);
    return true;
}

我们用bool来给出返回值,当然更多是使用int,char。

如果我们有特殊的情怀,或者我们有一些批量的任务,希望用一个统一的例程处理他们的错误。。。

我们想在C语言中,使用C++类似的东西,在foo中抛出一个异常,在main中catch呢?

这里需要用到setjmp和longjmp,我先给你一些概念:

setjmp=try;

longjmp=throw。

可以看到try和throw都有了,那catch在哪里?

要知道C语言是流程式的语言,那catch在C语言中肯定得遵循某一个流程表达式,没错。。。就是if。。。

所以你可以看到:

setjmp=try,longjmp=throw,if=catch。

好像所有条件都具备了,到底怎么玩?来我们继续。

我们还是上面那个foo函数:

(首先我们使用setjmp和longjmp需要include setjmp.h)

void foo(int exp,jmp_buf& jb)
{
    if (exp == 'a')
        longjmp(jb,'a'); //throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

然后我们写main:

int main()
{
    jmp_buf jb;
    int jmp_ret = setjmp(jb);
    if (jmp_ret == 0) //try
    {
        int val = getchar();
        foo(val,jb);
    }else{ //catch
        printf("skip ex:%d.\n",jmp_ret);
    }

    return 0;
}

按照上面的路子来,我们输入b:

foo ok 98.

其输入也是一样的,那我们输入a呢:

skip ex:97.

这里97是a的ascii码,也就是其是跟上面的异常流程处理是一样的,是不是感觉很奇葩。

你肯定在想,为什么,按照理论上来说,setjmp后==0,foo才会执行,按照我们的传统流程,既然foo被执行了,那else应该永远得不到执行,那longjmp又是如何从foo里面跑回去了main?

我们来设想一下,else要如何才能被执行?

对了,肯定是jmp_ref != 0嘛,没错,longjmp做的就是这个工作。

我们先不要在意jmp_buf,我们先看下longjmp的第二个参数,他是一个值类型,这个参数我指定的是‘a‘,也就是97,你看到了,我在prntf里面打印了jmp_ret的值,也就是,我们在longjmp时指定某一个值后,longjmp会把当前函数的流程做一个大转弯,直接跳回到这里:

if (jmp_ret == 0) //try

而此时,jmp_ret已经是我们指定的值,就是97了,那if的==不会被成立,则去执行else了。

此时可能你想,如果我这样:

longjmp(jb,0);

那不是jmp_ret还是==0,还又去执行foo,又被longjmp,不是死循环了么?

这个情况在CRT已经考虑过了,如果你给longjmp使用0值,其会自动修改为1,也就是0值是永远不会被出现的。

好,我们来总结:

1、首先setjmp需要==0才执行foo。

2、foo发现错误,把setjmp的==0给改了。

3、if表达式的else被执行。

可能你现在头还有点晕,不过我们先说这个到这里,我们来看setjmp的第一个参数:jmp_buf。

这个jmp_buf是什么呢,首先我们来再写一个main:

int main()
{
    char sz[128] = "hello.\n";

    jmp_buf jb;
    int jmp_ret = setjmp(jb);
    if (jmp_ret == 0) //try
    {
        int val = getchar();
        foo(val,jb);
    }else{ //catch
        printf("skip ex:%d.\n",jmp_ret);
        printf(sz);
    }

    return 0;
}

输入a,则会输出:

skip ex:97.

hello.

你肯定想这是当然的,因为sz变量在main范围内嘛。

但是别忘了,我们访问sz可是在else里,也就是我们访问的时候,是被longjmp跳过去的。。。

要知道,执行foo的时候,可能整个堆栈环境已经变得离谱了,如果你知晓汇编,肯定知道,执行foo的时候,main使用堆栈指针EBP(当然也可以直接ESP,不过这里做一个比方)会被保存起来,要等foo进行return的时候,才会恢复EBP,然后main的局部变量才能通过EBP访问到,但是我们的foo可是直接longjmp的,我们没有任何代码用于恢复EBP的值,那如何保证飞过去else的时候,访问sz变量的地址是正确的?

对了,在setjmp的时候,CRT会把EBP等变量的值保存在jmp_buf里面,然后在longjmp里面,把EBP的值从jmp_buf里面取出来,进行恢复。

这样在执行longjmp的时候,EBP会被恢复到setjmp时的情况,也就保证了sz变量的地址在执行else的时候也是正确的。

如果你只会C语言,那看到这里,你应该大概理解了,如果你还了解过汇编,那可以继续看下去,我会为你揭示setjmp、longjmp背后的一些东西。

【深入探索】

我们把刚才那个exe进行动态反汇编,以便我们整体的了解setjmp和longjmp的所有情况。

首先在调试器里面,main是这样的:

可以看到,关键就是在TEST EAX,EAX这里有一个JNZ跳,如果不是0则跳到下面的catch。

我们来看setjmp的汇编:

可以看到其保存了几个windows关键的寄存器。

注意,在win32下,eax、edx、ecx被定义为易失寄存器,比如我们调用foo的时候,如果foo需要用到ebx,esi,它也需要保存,退出时恢复,但是使用edx则不需要保存。

setjmp也是遵循这个原则。

可以看到setjmp的返回是XOR EAX,EAX,就是返回0。

好我们来看longjmp的反汇编:

可以看到其检测了一下jmp_buf的正确性,然后就进行寄存器的恢复,最终把call自身的堆栈平衡了后,就使用JMP指令直接JMP到setjmp后的那个指令地址,而此时其把EAX改成了longjmp的第二个参数:

那接下来的TEST EAX,EAX肯定不会成功,就会跑去执行catch了。

【与C++的结合】

文章写到这里,应该快结束了,可还有一个点,可能你没注意到,我们还是回到我们第一个代码——CFoo这个类来。

在上面的C++异常里面,我们看到了这样的代码:

void foo(int exp)
{
    CFoo cfoo;
    if (exp == 'a')
        throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

按照C++的规范,异常发生的时候,cfoo也会被析构,如果我们使用longjmp呢,就像下面:

void foo(int exp,jmp_buf& jb)
{
    CFoo cfoo;
    if (exp == 'a')
        longjmp(jb,'a'); //throw std::exception("a");

    printf("foo ok %d.\n",exp);
}

你肯定会想,cfoo应该只会被构造,而不会被析构,因为longjmp可是CRT的函数。

其实原来我也是这样想的,但是我不懂是不是VC spec,我在跟踪longjmp的时候发现了堆栈展开的代码。。。

也就是,其实cfoo在longjmp的时候,也是会被析构的:

Create CFoo.

~Destroy CFoo.

skip ex:97.

hello.

这个要注意一下。

如果你想看汇编,在下面。

这个是foo函数的汇编:

SEH处理器在这里:

然后会展开到析构函数:

【完结】

为这2个狗血的东西写了那么多,也说的差不多了。

其实这2个东西,因为其反人类的特性,在项目开发中,不应该被使用上,在这里只是告诉大家,如果遇到有setjmp、longjmp的情况的时候,可以判断出来代码的执行流程。

时间: 2024-07-30 08:38:16

C语言之setjmp和longjmp详细剖析的相关文章

【C语言天天练(五)】setjmp和longjmp

setjmp和longjmp组合可以实现跳转,与goto语句有相似的地方.但有以下不同: 1.用longjmp只能跳回到曾经到过的地方.在执行setjmp的地方仍留有一个过程活动记录.从这个角度看,longjmp更像是"从何处来"而不是"往何处去".longjmp接收一个额外的整型参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一条语句执行后自然而然来到这里的. 2.goto语句不能跳出C语言当前的函数,而longjmp可以跳的更远,可以跳出函数,

C语言中利用setjmp和longjmp做异常处理

错误处理是任何语言都需要解决的问题,只有不能保证100%的正确运行,就需要有处理错误的机制.异常处理就是其中的一种错误处理方式. 1 过程活动记录(Active Record) C语言中每当有一个函数调用时,就会在堆栈(Stack)上准备一个被称为AR的结构,抛开具体编译器实现细节的不同,这个AR基本结构如下所示. 每当遇到一次函数调用的语句,C编译器都会产生出汇编代码来在堆栈上分配这个AR.例如下面的C代码: void a(int i) { if(i==0){ i = 1; } else {

【转】浅析C语言的非局部跳转:setjmp和longjmp

转自 http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html C语言中有一个goto语句,其可以结合标号实现函数内部的任意跳转(通常情况下,很多人都建议不要使用goto语句,因为采用goto语句后,代码维护工作量加大).另外,C语言标准中还提供一种非局部跳转“no-local goto",其通过标准库<setjmp.h>中的两个标准函数setjmp和longjmp来实现. C标准库<setjmp.h>

C语言再学习之 setjmp与longjmp

前不久在阅读Quake3源代码的时候,看到一个陌生的函数:setjmp,一番google和查询后,觉得有必要针对setjmp和longjmp这对函数写一篇blog,总结一下. setjmp和longjmp是C语言独有的,只有将它们结合起来使用,才能达到程序控制流有效转移的目的,按照程序员的预先设计的意图,去实现对程序中可能出现的异常进行集中处理. 先来看一下这两个函数的定义吧: setjmp和longjmp的函数原型在setjmp.h中 函数原型: int setjmp(jmp_buf envb

c语言setjmp与longjmp函数

我们都知道在一个函数内进行跳转,可以使用goto语句,但是如果要在函数之间跳转goto是不能完成的.要想完成函数之间的跳转我们需要借助setjmp和longjmp这两个函数连实现,这都包含在头文件setjmp.h中 函数原型: 1. int setjmp(jmp_buf env):此函数的功能是将函数在此处的上下文保存在jmp_buf结构体中,以供longjmp从此结构体中恢复.参数env即为保存上下文的jmp_buf结构体变量.如果直接调用该函数返回值为0,若该  函数从longjmp调用返回

C语言中setjmp与longjmp学习笔记

一.基础介绍 ?? ?头文件:#include<setjmp.h> ?? ?原型:??int?setjmp(jmp_buf envbuf) ?? ?宏函数setjmp()在缓冲区envbuf中保存系统堆栈里的内容,供longjmp()以后使用.首次调用setjmp()宏时,返回值为0,然而longjmp()把一个变原传递给setjmp(),该值(恒不为0)就是调用longjmp()后出现的setjmp()的值. void longjmp(jmp_buf envbuf,int status);

setjmp()和longjmp()

1.语言没有异常处理机制,可以使用setjmp和longjmp进行模拟,另外,goto语句不能在函数间跳转,可以使用setjmp和longjmp完成函数间的跳转. 使用setjmp()函数需要包含头文件<setjmp.h> setjmp()函数用于建立本地的jmp_buf缓冲区并初始化,用于将来跳转到这个地方. longjmp()函数恢复jmp_buf变量所保存的信息,longjmp()之后返回setjmp()处执行. 1.1使用setjmp处理异常的例子: # include <std

Mahout机器学习平台之聚类算法详细剖析(含实例分析)

第一部分: 学习Mahout必须要知道的资料查找技能: 学会查官方帮助文档: 解压用于安装文件(mahout-distribution-0.6.tar.gz),找到如下位置,我将该文件解压到win7的G盘mahout文件夹下,路径如下所示: G:\mahout\mahout-distribution-0.6\docs 学会查源代码的注释文档: 方案一:用maven创建一个mahout的开发环境(我用的是win7,eclipse作为集成开发环境,之后在Maven Dependencies中找到相应

非本地跳转之setjmp与longjmp

非本地跳转(unlocal jump)是与本地跳转相对应的一个概念. 本地跳转主要指的是类似于goto语句的一系列应用,当设置了标志之后,可以跳到所在函数内部的标号上.然而,本地跳转不能将控制权转移到所在程序的任意地点,不能跨越函数,因此也就有了非本地跳转. C语言里面提供了setjmp和longjmp函数来进行跨越函数之间的控制权的跳转,从而称之为非本地跳转. #include <setjmp.h> int setjmp(jmp_buf env); 该函数主要用来保存当前执行状态,作为后续跳