前段时间给组内做一个栈方面内容培训的时候,很多人都讨论起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)
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>
![](http://dl.iteye.com/upload/attachment/195829/be2e5ba5-ab56-3024-a27c-6a2527467259.png)
#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;
}
[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]#