异常处理与MiniDump详解(3) SEH(Structured Exception Handling)

一、    综述

SEH-- Structured Exception Handling ,是 Windows 操作系统使用的异常处理方式。

对于 SEH ,有点需要说明的是, SEH 是属于操作系统的特性,不为特定语言设计,但是实际上,作为操作系统的特性,几乎就等同与面向 C 语言设计,这点很好理解,就像 Win32 API,Linux 下的系统调用,都是操作系统的特性吧,实际还是为 C 做的。但是,作为为 C 语言设计的东西,实际上可调用的方式又多了,汇编, C++ 对于调用 C 语言的接口都是比较方便的。

二、    基础篇

还是简单介绍一下 SEH 的使用,但是不准备太详细的介绍了,具体的详细介绍见参考中提及的书目。 关于 SEH 的基本应用, 《 Windows 核心编程》绝对是最佳读物(其实个人一直认为《 Windows 核心编程》是 Windows 编程领域必看的第二本书,第一本是《 Programming Windows 》。关于 SEH 更深入的一点的知识可能就要参考一些能用汇编讲解的书籍了,《 Windows用户态程序高效排错》算是其中讲的不错的一本。

首先, SEH 也有像 C++ 异常一样的语法,及类 try-catch 语法,在 SEH 中为 __try-except 语法,抛出异常从 thro w 改为 RaiseException , 在 MSDN 中的语法描述为:

__try 

{

   // guarded code

}

__except ( expression )

{

   // exception handler code

}

见一 个实际使用的例子:

例 1 :

#include <iostream>

#include <windows.h>

using namespace std ;

int main ()

{

    __try

    {

       RaiseException (0, 0, 0, NULL );

    }

    __except (EXCEPTION_EXECUTE_HANDLER )

    {

       cout <<"Exception Raised." <<endl ;

    }

    cout <<"Continue running" <<endl ;

}

这可能是最简单的 SEH 的例子了,输出如下:

Exception Raised.

Continue running

这个例子和普通 C++ 异常的 try-catch 类似,也很好理解。只不过 catch 换成了 except 。

因为 C 语言没有智能指针,那么就不能缺少 finally 的异常语法,与 JAVA,Python 等语言中的也类似,(这是 C++ 中没有的) finally 语法的含义就是无论如何(不管是正常还是异常),此句总是会执行,常用于资源释放。

例 2 :

#include <iostream>

#include <windows.h>

using namespace std ;

int main ()

{

    __try

    {

       __try

       {

           RaiseException (0, 0, 0, NULL );

       }

       __finally

       {

           cout <<"finally here." <<endl ;

       }

    }

    __except (1)

    {

    }

    __try

    {

       __try

       {

           int i ;

       }

       __finally

       {

           cout <<"finally here." <<endl ;

       }

    }

    __except (1)

    {

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

这个实例看起来过于奇怪,因为没有将各个 try-finally 放入独立的模块之中,但是说明了问题:

1.      finally 的语句总是会执行,无论是否异常 finally here 总是会输出。

2.      finally 仅仅是一条保证 finally 语句执行的块,并不是异常处理的 handle 语句(与 except 不同),所以,假如光是有 finally语句块的话,实际效果就是异常会继续向上抛出。(异常处理过程也还是继续)

3.      finally 执行后还可以用 except 继续处理异常,但是 SEH 奇怪的语法在于 finally 与 except 无法同时使用,不然会报编译错误。

如下例:

    __try

    {

       RaiseException (0, 0, 0, NULL );

    }

    __except (1)

    {

    }

    __finally

    {

       cout <<"finally here." <<endl ;

    }

VS2005 会报告

error C3274: __finally 没有匹配的 try

这点其实很奇怪,难道因为 SEH 设计过于老了? -_-! 因为在现在的语言中 finally 都是允许与 except (或类似的块,比如 catch)同时使用的。 C# , JAVA,Python 都是如此,甚至在 MS 为 C++ 做的托管扩展中都是允许的。如下例: ( 来自 MSDN 中对 finally keyword [C++] 的描述 )

using namespace System ;

ref class MyException : public System ::Exception {};

void ThrowMyException () {

    throw gcnew MyException ;

}

int main () {

    try {

       ThrowMyException ();

    }

    catch ( MyException ^ e ) {

       Console ::WriteLine (  "in catch" );

       Console ::WriteLine ( e ->GetType () );

    }

    finally {

       Console ::WriteLine (  "in finally" );

    }

}

当你不习惯使用智能指针的时候常常会觉得这样会很好用。关于 finally 异常语法和智能指针的使用可以说是各有长短,这里提供刘未鹏的一种解释,(见参考 5 的 RAII 部分,文中比较的虽然是 JAVA , C#, 但是实际 SEH 也是类似 JAVA 的)大家参考参考。

SEH 中还提供了一个比较特别的关键字, __leave,MSDN 中解释如下

Allows for immediate termination of the __try block without causing abnormal termination and its performance penalty.

简而言之就是类似 goto 语句的抛出异常方式,所谓的没有性能损失是什么意思呢?看看下面的例子:

#include <iostream>

#include <windows.h>

using namespace std ;

int main ()

{

    int i = 0;

    __try

    {

       __leave ;

       i = 1;

    }

    __finally

    {

       cout <<"i: " <<i <<" finally here." <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

输出:

i: 0 finally here.

Continue running

实际就是类似 Goto 语句,没有性能损失指什么?一般的异常抛出也是没有性能损失的。

MSDN 解释如下:

The __leave keyword

The __leave keyword is valid within a try-finally statement block. The effect of __leave is to jump to the end of the try-finally block. The termination handler is immediately executed. Although a goto statement can be used to accomplish the same result, a goto statement causes stack unwinding. The __leave statement is more efficient because it does not involve stack unwinding.

意思就是没有 stack unwinding ,问题是。。。。。。如下例,实际会导致编译错误,所以实在不清楚到 __leave 到底干啥的,我实际中也从来没有用过此关键字。

#include <iostream>

#include <windows.h>

using namespace std ;

void fun ()

{

    __leave ;

}

int main ()

{

    __try

    {

       fun ();

    }

    __finally

    {

       cout <<" finally here." <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

三、    提高篇

1.       SEH 的优点

1)     一个很大的优点就是其对异常进程的完全控制,这一点是 C++ 异常所没有的,因为其遵循的是所谓的终止设定。

这一点是通过 except 中的表达式来控制的(在前面的例子中我都是用 1 表示,实际也就是使用了 EXCEPTION_EXECUTE_HANDLER 方式。

EXCEPTION_CONTINUE_EXECUTION (–1)     表示在异常发生的地方继续执行,表示处理过后,程序可以继续执行下去。 C++ 中没有此语义。

EXCEPTION_CONTINUE_SEARCH (0)     异常没有处理,继续向上抛出。类似 C++ 的 throw;

EXCEPTION_EXECUTE_HANDLER (1)    异常被处理,从异常处理这一层开始继续执行。 类似 C++ 处理异常后不再抛出。

2)     操作系统特性,不仅仅意味着你可以在更多场合使用SEH(甚至在汇编语言中使用),实际对异常处理的功能也更加强大,甚至是程序的严重错误也能恢复(不仅仅是一般的异常),比如,除 0 错误,访问非法地址(包括空指针的使用)等。这里可以用一个例子来说明:

#include <iostream>

#include <windows.h>

using namespace std ;

int main ()

{

    __try

    {

       int *p = NULL ;

       *p = 0;

    }

    __except (1)

    {

       cout <<"catch that" <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

输出:

catch that

Continue running

在 C++ 中这样的情况会导致程序直接崩溃的,这一点好好利用,可以使得你的程序稳定性大增,以弥补 C++ 中很多的不足。但是,问题又来了,假如异常都被这样处理了,甚至没有声息,非常不符合发生错误时死的壮烈的错误处理原则。。。。。。。很可能导致程序一堆错误,你甚至不知道为什么,这样不利于发现错误。

但是, SEH 与 MS 提供的另外的特性 MiniDump 可以完美的配合在一起,使得错误得到控制,但是错误情况也能捕获到,稍微的缓解了这种难处(其实也说不上完美解决)。

这一点需要使用者自己权衡,看看到底开发进入了哪个阶段,哪个更加重要,假如是服务器程序,那么在正式跑着的时候,每崩溃一次就是实际的损失。。。所以在后期可以考虑用这种方式。

关于这方面的信息,在下一次在详细讲解。

2.       SEH 的缺点

其实还是有的,因为是为操作系统设计的,实际类似为 C 设计,那么,根本就不知道 C++ 中类 / 对象的概念,所以,实际上不能识别并且正确的与 C++ 类 / 对象共存,这一点使用 C++ 的需要特别注意,比如下例的程序根本不能通过编译。

例一:

int main ()

{

    CMyClass o ;

    __try

    {

    }

    __except (1)

    {

       cout <<"catch that" <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

例二:

int main ()

{

    __try

    {

       CMyClass o ;

    }

    __except (1)

    {

       cout <<"catch that" <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

错误信息都为:

warning C4509: 使用了非标准扩展 :“main” 使用 SEH ,并且 “o” 有析构函数

error C2712: 无法在要求对象展开的函数中使用 __try

这点比较遗憾,但是我们还是有折衷的办法的,那就是利用函数的特性,这样可以避开 SEH 的不足。

比如,希望使用类的使用可以这样:

这个类利用了上节的 CResourceObserver 类,

class CMyClass : public CResourceObserver <CMyClass >

{

};

void fun ()

{

    CMyClass o ;

}

#include <iostream>

#include <windows.h>

using namespace std ;

int main ()

{

    __try

    {

       fun ();

    }

    __except (1)

    {

       cout <<"catch that" <<endl ;

    }

    cout <<"Continue running" <<endl ;

    getchar ();

}

输出:

class CMyClass Construct.

class CMyClass Deconstruct.

Continue running

可以看到正常的析构,简而言之就是将实际类 / 对象的使用全部放进函数中,利用函数对对象生命周期的控制,来避开 SEH 的不足。

四、    参考资料

1.      Windows 核心编程( Programming Applications for Microsoft Windows ) , 第 4 版, Jeffrey Richter 著,黄陇,李虎译,机械工业出版社

2.      MSDN—Visual Studio 2005 附带版 ,Microsoft

3.      加密与解密,段钢编著,电子工业出版社

4.      Windows 用户态程序高效排错,熊力著,电子工业出版社

5. 错误处理 (Error-Handling) :为何、何时、如何 (rev#2) ,刘未鹏 (pongba) 著

摘自:http://blog.csdn.net/vagrxie/archive/2009/07/27/4382591.aspx

原文地址:https://www.cnblogs.com/lzjsky/p/1886728.html