[转]C标准库中的setjmp与longjmp

在C标准库中有一对非常有趣的函数setjmp()函数与longjmp()函数,用来实现代替goto实现一些非常重要的功能,如异常处理。C语言中,标准库函数setjmp和longjmp形成了结构化异常工具的基础。简单的说即setjmp实例化异常处理程序,而longjmp产生异常。

先介绍setjmp

int setjmp(jmp_buf envbuf)

宏函数setjmp()在缓冲区envbuf中保存系统堆栈里的内容,供longjmp()以后使用,setjmp()必须使用头文件setjmp.h。

调用setjmp()宏时,返回值为0,然而longjmp()把一个变原传递给setjmp(),该值(恒不为0)就是调用longjmp()后出现的setjmp()的值。

setjmp函数用于保存程序的运行时的堆栈环境,接下来的其它地方,你可以通过调用longjmp函数来恢复先前被保存的程序堆栈环境。当setjmp和longjmp组合一起使用时,它们能提供一种在程序中实现“非本地局部跳转”("non-local goto")的机制。并且这种机制常常被用于来实现,把程序的控制流传递到错误处理模块之中;或者程序中不采用正常的返回(return)语句,或函数的正常调用等方法,而使程序能被恢复到先前的一个调用例程(也即函数)中。

对setjmp函数的调用时,会保存程序当前的堆栈环境到env参数中;接下来调用longjmp时,会根据这个曾经保存的变量来恢复先前的环境, 并且当前的程序控制流,会因此而返回到先前调用setjmp时的程序执行点。此时,在接下来的控制流的例程中,所能访问的所有的变量(除寄存器类型的变量 以外),包含了longjmp函数调用时,所拥有的变量。

void longjmp(jmp_buf envbuf,int status);

函数longjmp()使程序在最近一次调用setjmp()出重新执行。setjmp()和longjmp()提供了一种在函数间调转的手段,必须使用头部文件setjmp.h。

函数longjmp()通过把堆栈复位成envbuf中描述的状态进行操作,envbuf的设置是由预先调用setjmp()生成的。这样使程序的执行在 setjmp()调用后的下一个语句从新开始,使计算机认为从未离开调用setjmp()的函数。从效果上看,longjmp()函数似乎“绕”过了时间 和空间(内存)回到程序的原点,不必执行正常的函数返回过程。

缓冲区envbuf具有中定义的buf_jmp类型,它必须调用longjmp()前通过调用setjmp()来设置好。

值status变成setjmp()的返回值,由此确定长调转的来处。不允许的唯一值是0,0是程序直接调用函数setjmp()时由该函数返回的,不是间接通过执行函数longjmp()返回的。

longjmp()函数最常用于在一个错误发生时,从一组深层嵌套的实用程序中返回。

longjmp函数用于恢复先前程序中调用的setjmp函数时所保存的堆栈环境。setjmp和longjmp组合一起使用时,它们能提供一种在程序中实现“非本地局部跳转”("non-local goto")的机制。并且这种机制常常被用于来实现,把程序的控制流传递到错误处理模块,或者不采用正常的返回(return)语句,或函数的正常调用等方法,使程序能被恢复到先前的一个调用例程(也即函数)中。

对setjmp函数的调用时,会保存程序当前的堆栈环境到env参数中;接下来调用longjmp时,会根据这个曾经保存的变量来恢复先前的环境,并且 因此当前的程序控制流,会返回到先前调用setjmp时的执行点。此时,value参数值会被setjmp函数所返回,程序继续得以执行。并且,在接下来 的控制流的例程中,它所能够访问到的所有的变量(除寄存器类型的变量以外),包含了longjmp函数调用时,所拥有的变量;而寄存器类型的变量将不可预 料。setjmp函数返回的值必须是非零值,如果longjmp传送的value参数值为0,那么实际上被setjmp返回的值是1。

在调用setjmp的函数返回之前,调用longjmp,否则结果不可预料。在使用longjmp时,请遵守以下规则或限制:

  • 不要假象寄存器类型的变量将总会保持不变。在调用longjmp之后,通过setjmp所返回的控制流中,例程中寄存器类型的变量将不会被恢复。
  • 不要使用longjmp函数,来实现把控制流,从一个中断处理例程中传出,除非被捕获的异常是一个浮点数异常。在后一种情况下,如果程序通过调用 _fpreset函数,来首先初始化浮点数包后,它是可以通过longjmp来实现从中断处理例程中返回。

如何实现异常处理

首先设置一个跳转点(setjmp() 函数可以实现这一功能),然后在其后的代码中任意地方调用 longjmp() 跳转回这个跳转点上,以此来实现当发生异常时,转到处理异常的程序上,在其后的介绍中将介绍如何实现。 setjmp() 为跳转返回保存现场并为异常提供处理程序,longjmp() 则进行跳转(抛出异常),setjmp() 与 longjmp() 可以在函数间进行跳转,这就像一个全局的 goto 语句,可以跨函数跳转。

jmp_buf 异常结构

使用 setjmp() 及 longjmp() 函数前,需要先认识一下 jmp_buf 异常结构。jmp_buf 将使用在 setjmp() 函数中,用于保存当前程序现场(保存当前需要用到的寄存器的值),jmp_buf 结构在 setjmp.h 文件内声明:

        typedef struct

        {

                unsigned j_sp;  // 堆栈指针寄存器

                unsigned j_ss;  // 堆栈段

                unsigned j_flag;  // 标志寄存器

                unsigned j_cs;  // 代码段

                unsigned j_ip;  // 指令指针寄存器

                unsigned j_bp; // 基址指针

                unsigned j_di;  // 目的指针

                unsigned j_es; // 附加段

                unsigned j_si;  // 源变址

                unsigned j_ds; // 数据段

        } jmp_buf;

        jmp_buf 结构存放了程序当前寄存器的值,以确保使用 longjmp() 后可以跳回到该执行点上继续执行。

 

setjmp() 与 longjmp() 函数都使用了 jmp_buf 结构作为形参,它们的调用关系是这样的:

首先调用 setjmp() 函数来初始化 jmp_buf 结构变量 jmpb,将当前CPU中的大部分影响到程序执行的积存器存入 jmpb,为 longjmp() 函数提供跳转,setjmp() 函数是一个有趣的函数,它能返回两次,它应该是所有库函数中唯一一个能返回两次的函数,第一次是初始化时,返回,第二次遇到 longjmp() 函数调用后,longjmp() 函数使 setjmp() 函数发生第二次返回,返回值由 longjmp() 的第二个参数给出(整型,这时不应该再返回零)。

在使用 setjmp() 初始化 jmpb 后,可以其后的程序中任意地方使用 longjmp() 函数跳转会 setjmp() 函数的位置,longjmp() 的第一个参数便是 setjmp() 初始化的 jmpb,若想跳转回刚才设置的 setjmp() 处,则 longjmp() 函数的第一个参数是 setjmp() 所初始化的 jmpb 这个异常,这也说明一件事,即 jmpb 这个异常,一般需要定义为全局变量,否则,若是局部变量,当跨函数调用时就几乎无法使用(除非每次遇到函数调用都将 jmpb 以参数传递,然而明显地,是不值得这样做的);longjmp() 函数的第二个参数是传给 setjmp() 的第二次返回值,这在介绍 setjmp() 函数时已经介绍过。

下面是 setjmp() 函数与 longjmp() 函数的一个示意图:

[转]C标准库中的setjmp与longjmp - Leox - X-修炼之道

通过示意图可以看出 setjmp() 函数与 longjmp() 函数的关系,如何跳转。

 [转]C标准库中的setjmp与longjmp - Leox - X-修炼之道

 呵呵!现在是否对程序的执行流程一目了然,其中最关键的就是setjjmp和longjmp函数的调用处理。我们分别来分析之。
  当程序运行到第②步时,调用setjmp函数,这个函数会保存程序当前运行的一些状态信息,主要是一些系统寄存器的值,如ss,cs,eip, eax,ebx,ecx,edx,eflags等寄存器,其中尤其重要的是eip的值,因为它相当于保存了一个程序运行的执行点。这些信息被保存到 mark变量中,这是一个C标准库中所定义的特殊结构体类型的变量。
  调用setjmp函数保存程序状态之后,该函数返回0值,于是接下来程序执行到第③步和第④步中。在第④步中语句执行时,如果变量n2为0值,于是便 引发了一个浮点数计算异常,,导致控制流转入fphandler函数中,也即进入到第⑤步。
  然后运行到第⑥步,调用longjmp函数,这个函数内部会从先前的setjmp所保存的程序状态,也即mark变量中,来恢复到以前的系统寄存器的 值。于是便进入到了第⑦步,注意,这非常有点意思,实际上,通过longjmp函数的调用后,程序控制流(尤其是eip的值)再次戏剧性地进入到 setjmp函数的处理内部中,但是这一次setjmp返回的值是longjmp函数调用时,所传入的第2个参数,也即-1,因此程序接下来进入到了第⑧ 步的执行之中。

因为 jmp_buf 全局只能存放一个 jmpb 结构,使得只有最后一组宏可以响应异常;

这是无法嵌套异常的原因,要实现多重嵌套可以建立一个全局堆栈来维护一组 jmpb 结构。

实现机理
setjmp()
保存其调用返回点的ebx, esi, edi, ebp, esp, eip,对于sigsetjmp(), 还保存当前的信号屏蔽字, longjmp恢复这些寄存器及信号屏蔽字,同时传递一个返回值。
longjmp只是使CPU的状态和setjmp时的状态一致,相同的CPU初始状态执行相同的代码并不意味着产生相同的结果,setjmp, longjmp并不关心内存变量的变化,而只关心CPU的状态,
内存变量的确定性应该根据上下文来灵活处理.

如果将函数之间的调用关系看成一种树型结构, 将可执行文件的入口函数看成它的最内层节点(根节点), 那么setjmp提供了标记节点的方法, longjmp提供了从外层节点直接返回到更内层节点的方法,
在同一层节点之间或者从内层节点到外层节点的longjmp是没有意义的, 因为目标节点在此时根本不存在.

 
此文来自  耶和华的大卫  的文件夹

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值