`
jibin
  • 浏览: 1752 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
最近访客 更多访客>>
社区版块
存档分类
最新评论

C/C++ 异常处理之 01:setjmp 和 longjmp

阅读更多

前段时间给组内做一个栈方面内容培训的时候,很多人都讨论起C++ 里面 try catch 在捕获异常的时候栈是如何工作。因为我对try catch的异常处理时,栈的回退也不是很清楚,更何况Windows下还有SEH,VEH之流的处理机制。因此只好找时间慢慢做些功课,顺便记录下来。

 

提到C和C++的异常处理的,大家可能首先第一个想到的自然是try catch。但是在C语言里面是没有try-catch-finally 这样的异常处理方式,至少C标准没有定义,至于VC里面可以使用__try 和 __except 那是属于SEH 的范畴。其实最最原始的处理方式goto应该算一种,不过据大部分地方都有不建议使用goto的说法,大家感兴趣的完全可以使用索搜引擎搜一下关于goto的讨论。至少在我所在的公司不少部门在编程规范中都有关于“使用goto需要申请的”这么一条,哈哈。其实在做一些清理工作的时候goto是非常有用的。当然goto只能处理自身函数域内的一些问题,总不至于整个程序只有一个函数吧。

 

在C语言里还有一种处理机制就是使用setjmp 和longjmp。我们先来看一下这两个函数的原型:

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

 这两个函数在glibc 里面可以找到实现。简单的说setjmp 会保存当前的栈信息,而longjmp则会恢复当前的栈信息。而堆栈信息就保存在参数env里面了。

 

我们先看一个简单的例子来分析一下,参见如下代码:

#include <setjmp.h>
#include <iostream>

using namespace std;

static jmp_buf g_jmpbuf;

void exception_jmp()
{
    cout << "throw_exception_jmp start." << endl;
    longjmp(g_jmpbuf, 1);
    cout << "throw_exception_jmp end." << endl;
}


void call_jmp()
{
    exception_jmp();
}


int main(int argc, char *argv[])
{
    /* using setjmp and longjmp */
    if (setjmp(g_jmpbuf) == 0)
    {
        call_jmp();
    }
    else
    {
        cout << "catch exception via setimp-longjmp." << endl;
    }

    return 0;
}

 main 函数调用 call_jmp,call_jmp 里再调用exception_jmp,先看执行结果:

 

 

[root@centbox cjmp]# ./test01 
throw_exception_jmp start.
catch exception via setimp-longjmp.
[root@centbox cjmp]# 
[root@centbox cjmp]#

 结果和使用try-catch很类似。但是按照C 语言的思维来思考的话,可能不太好理解,因为两句打印实际上位于一个If的两个分支里面的。我们需要看一下setjmp 和 longjmp 的实现。

 

前面已经说过了setjmp 具体的作用就是保存寄存器。setjmp/longjmp 是分别在 glibc (GNU)和 CRT (MSVC) 里面实现的。我暂且先看GNU glibc里的实现。在X86下,实现在sysdeps/i386/setjmp.S,有如下实现:

 

ENTRY (BP_SYM (__sigsetjmp))
	ENTER

	movl JMPBUF(%esp), %eax
	CHECK_BOUNDS_BOTH_WIDE (%eax, JMPBUF(%esp), $JB_SIZE)

     	/* Save registers.  */
	movl %ebx, (JB_BX*4)(%eax)
	movl %esi, (JB_SI*4)(%eax)
	movl %edi, (JB_DI*4)(%eax)
	leal JMPBUF(%esp), %ecx	/* Save SP as it will be after we return.  */
#ifdef PTR_MANGLE
	PTR_MANGLE (%ecx)
#endif
     	movl %ecx, (JB_SP*4)(%eax)
	movl PCOFF(%esp), %ecx	/* Save PC we are returning to now.  */
#ifdef PTR_MANGLE
	PTR_MANGLE (%ecx)
#endif
     	movl %ecx, (JB_PC*4)(%eax)
	LEAVE /* pop frame pointer to prepare for tail-call.  */
	movl %ebp, (JB_BP*4)(%eax) /* Save caller's frame pointer.  */

#if defined NOT_IN_libc && defined IS_IN_rtld
	/* In ld.so we never save the signal mask.  */
	xorl %eax, %eax
	ret
#else
	/* Make a tail call to __sigjmp_save; it takes the same args.  */
	jmp __sigjmp_save
#endif
END (BP_SYM (__sigsetjmp))

 

 其实简单的说就是存储相应的寄存器的值,在Jmpbuf-offsets.h 里有如下定义:

 

#define JB_BX	0
#define JB_SI	1
#define JB_DI	2
#define JB_BP	3
#define JB_SP	4
#define JB_PC	5
#define JB_SIZE 24

 

 现在很清楚了,被保存起来的寄存器依次是:EBX, ESI, EDI, EBP, ESP, 还有就是返回地址。EBP和ESP是用来恢复栈帧的。EBX, ESI, EDI 被约定函数调用的时候需要恢复。注意返回值,也就是EAX里面的值,是0。所以setjmp 执行的时候实际返回的是0。

 

我们接着看 longjmp 的实现,在sysdeps/i386/__longjmp.S 中,注意在longjmp 和 setjmp都有出现一组宏 PTR_DEMANGLE 和 PTR_MANGLE,这是glibc 为了解决安全问题引入的,为了方便理解,我们暂时只看没有这组宏的地方的代码。__longjmp 如下:

 

#else
	movl 4(%esp), %ecx	/* User's jmp_buf in %ecx.  */
	movl 8(%esp), %eax	/* Second argument is return value.  */
	/* Save the return address now.  */
	movl (JB_PC*4)(%ecx), %edx
     	/* Restore registers.  */
	movl (JB_BX*4)(%ecx), %ebx
	movl (JB_SI*4)(%ecx), %esi
	movl (JB_DI*4)(%ecx), %edi
	movl (JB_BP*4)(%ecx), %ebp
	movl (JB_SP*4)(%ecx), %esp
#endif
	/* Jump to saved PC.  */
     	jmp *%edx
END (__longjmp)
首先将返回地址传给 edx, 然后恢复剩余的5个寄存器后,最后jmp 到 edx 所指向的地址上去。这样就完成了栈的恢复。回过头来我考察一下main 函数里面调用setjmp 的代码的汇编逻辑。

0x0804878a <main+24>:   call   0x804858c <_setjmp@plt>
0x0804878f <main+29>:   test   %eax,%eax
0x08048791 <main+31>:   sete   %al
0x08048794 <main+34>:   test   %al,%al
0x08048796 <main+36>:   je     0x804879f <main+45>
0x08048798 <main+38>:   call   0x8048764 <_Z8call_jmpv>
调用_setjmp@plt后,我们之前分析过了_setjmp里面会保存当前寄存器的值,同时会把返回地址:0x0804878f 也保存下来。由于_setjmp执行返回0, 因此接下来的跳转不会执行,而在接下来的执行的代码的时候如果调用longjmp 那么代码就会跳转回0x0804878f,由于longjmp 设置了新的返回值,此时eax 不为0,那么接下来的跳转就被执行了。至此在两个If 分支里的代码都被执行也就解释清楚了。
汇编的跳转如下:

 
还有一点值得说的。由于longjmp的时候是直接跳到原来的栈帧上去的,所以和C++ 的 try catch 是不太一样的。由于我们尚没有看C++ 的try catch是如何实现的。因此只好用一段测试代码的行为来说明。
#include <setjmp.h>
#include <iostream>

using namespace std;

static jmp_buf g_jmpbuf;

class TestClass
{
public:
    ~TestClass()
    {
        cout << "Call ~TestClass." << endl;
    }
};

void exception_jmp()
{
    cout << "throw_exception_jmp start." << endl;
    longjmp(g_jmpbuf, 1);
    cout << "throw_exception_jmp end." << endl;
}

void exception_throw()
{
    cout << "throw_exception_throw start." << endl;
    throw(0);
    cout << "throw_exception_throw end." << endl;
}

void call_jmp()
{
    TestClass oTest;
    exception_jmp();
}

void call_throw()
{
    TestClass oTest;
    exception_throw();
}

int main(int argc, char *argv[])
{
    /* using setjmp and longjmp */
    if (setjmp(g_jmpbuf) == 0)
    {
        call_jmp();
    }
    else
    {
        cout << "catch exception via setimp-longjmp." << endl;
    }

    /* using try and catch */
    try
    {
        call_throw();
    }
    catch (...)
    {
        cout << "catch exception via try-catch." << endl;
    }

    return 0;
}
 上面的代码分别使用longjmp 和 throw来抛出异常。
调用关系分别有:
main -> call_throw -> exception_throw
main -> call_jmp -> exception_jmp 
在函数call_throw 和 call_jmp 里面都定义了局部的C++对象,执行结果表明在使用longjmp 方式的,析构函数没有被调用到。原因:longjmp 是直接跳回去的,try catch 的方式,我们后续会再讨论。所以:在C++里面可以使用try catch了,就不要用setjmp/longjmp来处理了。
上面代码的执行结果:
[root@centbox cjmp]# ./test
throw_exception_jmp start.
catch exception via setimp-longjmp.
throw_exception_throw start.
Call ~TestClass.
catch exception via try-catch.
[root@centbox cjmp]# 
 
  • 大小: 12.9 KB
0
0
分享到:
评论

相关推荐

    c标准异常处理-全面了解setjmp与longjmp的使用[参照].pdf

    在C语言中,setjmp和longjmp是一对用于异常处理和非局部跳转的函数,它们提供了在程序中实现类似于异常处理的机制。虽然C++有自己的异常处理框架,但setjmp和longjmp在某些特定场景下依然有其独特价值。 首先,...

    用 setjmp 和 longjmp 实现多线程(1)_多线程_

    在C语言中,setjmp和longjmp是两个与异常处理和非局部跳转相关的函数,它们可以被用来实现一种特殊的多线程效果。虽然这两个函数并非设计为创建和管理线程的标准方法,但在某些特定场景下,它们可以模拟多线程的行为...

    setjmp和longjmp详细介绍

    `setjmp`和`longjmp`提供了C语言中强大的流程控制能力,尤其是对于错误处理和异常管理而言。然而,由于它们的强大特性也可能导致代码难以理解和维护,因此建议仅在必要时使用,并且要特别注意资源管理和变量的有效性...

    c/vc++/MFC异常处理/结构化异常处理 浅析

    SEH使用__try、__except和__finally关键字,能够捕获和处理系统级异常,如硬件故障、访问违规等。相比于C++异常,SEH更加底层,性能更好,但在代码的可读性和可移植性上略逊一筹。在MFC中,可以通过CATCH_ALL、END_...

    C/C++ 语言参考手册 CHM

    - **错误处理**:讨论错误检测和处理机制,如errno全局变量和setjmp/longjmp。 2. **C++语言部分**: - **面向对象编程**:详细解释类、对象、继承、多态、封装等核心概念。 - **模板**:涵盖函数模板和类模板,...

    C语言异常处理setjmp.pdf

    C标准库中提供了两个非常有技巧的库函数setjmp和longjmp,通过它们的结合使用,可以实现对程序的异常处理机制。 setjmp函数是一个保存当前程序状态的函数,其原型为:int setjmp(jmp_buf env);当调用setjmp函数时...

    C++异常处理总结

    2. C++异常处理机制比C语言的setjmp、longjmp机制更为优秀,可以处理任意类型的异常。用户可以人为地抛出任何类型的对象作为异常。 3. 异常处理机制需要一定的开销,因此在频繁执行的关键代码段中避免使用这种机制...

    用C语言实现的异常处理库

    总结起来,这个C语言实现的异常处理库通过自定义函数和`setjmp/longjmp`机制,为C语言提供了异常处理的功能,尽管与高级语言的异常处理有所不同,但在某些情况下能提供更加清晰的错误处理流程。

    使用纯C语言实现异常处理

    然而,C语言本身并不直接支持异常处理机制,它依赖于错误返回码和自定义错误处理函数来处理异常情况。但在某些情况下,开发者可能需要在C语言中实现类似异常处理的功能,以增强代码的健壮性和可维护性。本篇文章将...

    C++异常处理

    C++异常处理还包括了setjmp和longjmp函数,这些函数是从C语言中继承来的。setjmp用于设置一个跳转点,而longjmp用于从当前函数跳出,跳转到由setjmp设置的跳转点,这可以用来实现非局部的跳转。然而,setjmp和...

    可嵌套的C语言异常处理机制

    总的来说,C语言中的可嵌套异常处理机制主要依赖于setjmp和longjmp的组合使用,并通过自定义的框架来实现类似于高阶语言的异常处理行为。这需要程序员对程序控制流有深入理解,并且在编写代码时要格外注意错误处理和...

    C++异常处理的编程方法

    C++与C语言在异常处理上存在差异,C语言本身不支持异常处理机制,但可以使用setjmp和longjmp函数来模拟异常处理。setjmp函数用于保存当前的环境,而longjmp用于恢复环境,这两个函数可以被用来实现一种跳转机制,...

    C/C++语言编程规范

    - C++中,使用`try-catch`处理异常,避免使用`setjmp()`/`longjmp()`。 - 在可能抛出异常的地方进行捕获,或在函数声明中明确指出可能抛出的异常。 10. **面向对象编程**(C++特有的): - 尽量使用类和对象,而...

    c与c++中的异常处理

    C语言本身并不直接支持异常处理,但可以通过一些技巧实现类似功能,比如设置和检查错误标志,或者使用`setjmp()`和`longjmp()`函数进行非局部跳转。然而,这种方式不如C++的异常处理优雅且难以维护。 ### C++中的...

    C与C++中的异常处理

    C标准库提供了一些基本工具来处理异常,主要是通过`setjmp`和`longjmp`这两个函数实现。`setjmp`函数用来保存当前程序状态,而`longjmp`则用于在发生异常时跳回到之前保存的状态。 ```c #include &lt;setjmp.h&gt; jmp_...

    C++中的异常处理机制详解

    在C语言中,传统的错误处理方法有三种:在函数中返回错误、使用信号来做信号处理系统、使用标准C库中的非局部跳转函数setjmp和longjmp。这些方法都存在一些问题,如耦合度高、错误处理代码与正常代码混杂、对象不能...

Global site tag (gtag.js) - Google Analytics