不会开机的男孩

SGI STL 学习笔记一 Iterator

| Comments

之前一直希望能够看STL源代码,因为他一直存放在我的硬盘深处。但是由于复杂性,我一直再绕。而且纠结的是,我一直推荐我的学弟去研读STL。由于最近的工作需要,使我不得不一看STL的究竟。当然,STL对我来说依然是太庞大了,有相当多的相关的基础知识的缺乏导致整个过程实在是太艰难了,直到我看到了《SGI STL 源码剖析》。之后我的很多例子其实就是这本书的源代码。真的,这又是一本经典的著作。这本书贯穿了我整个STL的学习。当然,如果你之前研读过《inside c++ object model》等经典C++教材。你会发现。整个知识开始网罗了。

  按照道理来讲,学习STL,实在是不能不从总图开始。但是STL太庞大了。这个总图只是一个残缺的部分。按照常规的思路,的确是需要从总纲学,但是,真的,我不能在这里胡扯。为了更方便自己理解。我从Iterator开始。

  从这个图中可以看出,Algorithm 通过 Iterator 访问 Container,而我们很多面向container的操作同样是有Iterator出发的。所以,我也准备从这里入手。

alt text

制作一个Iterator,我们首先遇到的一个问题,就是如何找到这个Iterator 指向的类型。

1、根据参数推导,我们可以找出参数类型,但是,如果是返回值,我们则无能为力。

#include <iostream>
using namespace std;

template <class I, class T>
void func_impl(I iter, T t)
{
    T tmp = t;
    cout<<tmp<<endl;
}
template <class I>
inline
void func(I iter)
{
    func_impl(iter, *iter);
}

int main()
{
    int i = 9;
    func(&i);
}

2、通过声明内嵌类型。我们可以找到这个Iterator的类型

#include <iostream>
using namespace std;

template <class T>
struct MyIter
{
    typedef T value_type;
    T* ptr;
    MyIter(T* p = 0):ptr(p){}
    T& operator*()const {return *ptr;}
};
template <class I>
typename I::value_type
func(I ite){return *ite;}

int main()
{
    MyIter<int> iter(new int(8));
    cout<<func(iter)<<endl;
    delete iter.ptr;
    iter.ptr = NULL;
}

但是,我们并没有解决问题,如果这个Iterator 指向的地方,不是一个class type。原生指针不是,所以,我们必须找到一个方式这个就是 template partial specialization。

#include <iostream>
using namespace std;

template <class I>
struct iterator_traits
{
    typedef typename I::value_type value_type;
};

template <class T>
struct iterator_traits<T*>
{
    typedef T value_type;
};
template <class T>
struct iterator_traits<const T*>
{
    typedef T value_type;
};

template <class I>
typename iterator_traits<I>::value_type
func(I iter){return *iter;}

int main()
{
    int i = 50;
    cout<<func(&i)<<endl;
}

构造一个Iterator,我们必须有的部分。

struct iterator {

typedef _Category iterator_category;//Iterator种类

typedef _Tp value_type; //iterator 所指对象类型

typedef _Distance difference_type; //2个Iterator之间的距离

typedef _Pointer pointer; //iterator 所指对象的地址

typedef _Reference reference; //Iterator 所指对象引用类型

};

template <class _Iterator>

struct iterator_traits {

typedef typename _Iterator::iterator_category iterator_category;

typedef typename _Iterator::value_type value_type;

typedef typename _Iterator::difference_type difference_type;

typedef typename _Iterator::pointer pointer;

typedef typename _Iterator::reference reference;

};

对于原生指针,需要适应特化版本。这里从略。

alt text

SGI STL 增加元素之一 __type_traits

从字面上看,这里是类型萃取。的确。这里是对 trivial default constructor 和 none trivial defaultconstructor 的区别。 以及 none-trivial assignment operator 。 non-trivial-dtor。相关的知识可以在《inside c++ object model》中找到。在面对拥有”无用”的构造,拷贝,复制等类时,通过萃取机制,可以在编译时完成函数绑定。会直接采用最有效的策略。采用更为高效的memcpy等。 为了构造能够在编译时完成函数绑定,我们只能利用重载机制,那么,我们也就必须构造类型,作为函数参数

struct __true_type {
};

struct __false_type {
};


template <class _Tp>
struct __type_traits { 
   typedef __true_type     this_dummy_member_must_be_first;
                   /* Do not remove this member. It informs a compiler which
                      automatically specializes __type_traits that this
                      __type_traits template is special. It just makes sure that
                      things work if an implementation is using a template
                      called __type_traits for something unrelated. */

   /* The following restrictions should be observed for the sake of
      compilers which automatically produce type specific specializations 
      of this class:
          - You may reorder the members below if you wish
          - You may remove any of the members below if you wish
          - You must not rename members without making the corresponding
            name change in the compiler
          - Members you add will be treated like regular members unless
            you add the appropriate support in the compiler. */


   typedef __false_type    has_trivial_default_constructor;
   typedef __false_type    has_trivial_copy_constructor;
   typedef __false_type    has_trivial_assignment_operator;
   typedef __false_type    has_trivial_destructor;
   typedef __false_type    is_POD_type;

};

SGI 为每一个内嵌类型都定义为默认__false_type。这样来保证最底线的正确。因为如果判断错误则会有致命的错误。

然后为每一个标准类型设计特化版本。从而里引用偏特化机制来保证整个机制运行。比如

__STL_TEMPLATE_NULL struct __type_traits<char> {
   typedef __true_type    has_trivial_default_constructor;
   typedef __true_type    has_trivial_copy_constructor;
   typedef __true_type    has_trivial_assignment_operator;
   typedef __true_type    has_trivial_destructor;
   typedef __true_type    is_POD_type; // plain old data
};

template <class _ForwardIter, class _Size, class _Tp>
inline _ForwardIter 
uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x)
{
  return __uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first));
}


template <class _ForwardIter, class _Size, class _Tp, class _Tp1>
inline _ForwardIter 
__uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x, _Tp1*)
{
    //这里根据传入的类型_Tp1,得到了is_POD_type 类型。并定义了一个类型_Is_POD
    //通过_Is_POD()构造一个临时对象,并传入函数参数。
  typedef typename __type_traits<_Tp1>::is_POD_type _Is_POD;
  return __uninitialized_fill_n_aux(__first, __n, __x, _Is_POD());
}

//_Is_POD 类型 == __true_type 执行
//这里其实,并没有调用构造函数。
template <class _ForwardIter, class _Size, class _Tp>
inline _ForwardIter
__uninitialized_fill_n_aux(_ForwardIter __first, _Size __n,
                           const _Tp& __x, __true_type)
{
  return fill_n(__first, __n, __x);
}

//_Is_POD 类型 == __false_type 执行
//这里,我们可以看出,在构造多个函数的时候,这里采用了c++的异常处理,保证如果有异常出现,
//构造过的对象能够被析构掉。当然,那个被构造了一半的对象是不会被析构的,也可能会造成memory leak,所以,切忌不要
//在构造函数中抛出异常。
template <class _ForwardIter, class _Size, class _Tp>
_ForwardIter
__uninitialized_fill_n_aux(_ForwardIter __first, _Size __n,
                           const _Tp& __x, __false_type)
{
  _ForwardIter __cur = __first;
  __STL_TRY {
    for ( ; __n > 0; --__n, ++__cur)
      construct(&*__cur, __x);
    return __cur;
  }
  __STL_UNWIND(destroy(__first, __cur));
}
//类似的这样的,还有许多特化后的版本。这里从略。

类似这样的设计,充斥在SGI STL中,在这里,任何一个小小的开销都被认为是无法接受的。的确。这里给人一种真实的理想的世界。如果你对code 有洁癖,SGI STL,是不能错过的。

又是一年过去了

| Comments

又是一年过去了。转眼已经大四。活了这么多年,终于要开始走向社会了。也不得不面对人生n个第一次。 第一次一个人做火车。第一次一个人出远门。第一次和别人合租。第一次正经工作……

  看了一眼,发现自己的第一篇博客是2010-01-10 11:54。完成的。不知不觉在园子里面也混了将近一年了。看这这些文章,不禁又想总结这一年来的得与失。

  我一直以来有一个很坏的习惯。不喜欢去那笔记录一下自己。但是当我看到这篇文章之后,永远改变了我的看法。http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now。的确。写这一年的博客,真的让我收获颇丰,是的,如果没有写博客,那么就从现在开始吧。

第一篇博客,WPF设计简单游戏初探。   是我第一次看到深蓝大哥的博客之后,有感而写的。是WPF下的一个简易《弹弹堂》的实现。当第一次看到深蓝的博客之后,一股做游戏的冲动就无法遏制。的确。WPF/SL的强大的动画能力。给了想我这样的小白一个非常好的机会。一个月。从0到这个小游戏。真的。除了WPF/SL,世上真的没有其他能够比这个再快了,而且界面效率也在可接受范围之内。当然。这些的背后,必然是游戏的粗糙。但这不是最重要的。最重要的则是,自己完全对程序执行流程没有把握。我在完全不知道的情况下,都能写一个demo,说明了MS的强大和自己的无知,愚昧。.net的机制的完全陌生。使得我在这一个月看了大量的有关.net的资料。其中收获最大的就是http://www.cnblogs.com/anytao/。《你必须知道的.net》。可以说是深刻的点醒了我对.net的兴趣。后面的一个月也沉浸在这里学习。但是越学越迷茫。越来越多的东西,使我把c# 和c++彻底混乱了(当时也不懂c++)。最终,由于个人兴趣爱好,我放弃的C#(或是C# 放弃了我)。但是.net所给我带来的冲击直到现在也没有退去。真的,我从没有感觉到一个完全无知的coder。可以将一个程序跑成那样。

  虽然结果不很让人满意。但是再深蓝大哥的鼓励下。我发布了个人第一篇博客。这里万分感谢,万事开头难。

每一个
从小在游戏中长大的孩子
都有一个梦


希望
有天能玩到自己的游戏
但是
面对OpenGL、D3D
面对动辄上万的代码
有的只是无尽的
遗憾!


当第一次遇到WPF/SL时
梦想的大门似乎再次打开
一个个不眠之夜
体会到了理想与现实是如此的
接近!


理想与现实总是和残酷对等
想到丑陋的代码
想到运行性能的低下
想到GC,IL,CLR
。。。


面对自己
笑!



谨以此勉励自己最近一个月的努力

第二篇博客,第三篇博客,缩略图设计初探, 缩略图设计初探二   这个可以说是我程序人生的第一次转折吧。这里感谢王克伟,Jake Li等大牛。能够加入itoday。应该是我真正开始接触编程吧。从一开始的写一个简单的写日志程序,到最后研究暴雪的MPQ文件格式,.net framework Dictionary。开始真正的体会到编程的乐趣。整个程序,现在看来也没有什么亮点。只是又重新复习了一遍hash table的相关知识。只是现在看来终于明白了为什么暴雪处理冲突的时候没有采用分离链表法,而是仅仅是很简单的线性再散列。分离链表法带来的空间的节省。但是带来了硬盘的多次seek。在面对600M的文件来说。真是不得不考虑的问题。同样,也是为什么像SQLite的文件型数据库,删除数据,文件也不会变小的原因之一。没有考虑seek,是我当时设计的最大缺憾。只是我的文件数据量小。问题不突出。

2010-04-16 多线程程序设计笔记一 ,多线程程序设计笔记二   开始学习几本windows 开发的核心书籍,《windows 核心编程》,《win32多线程程序设计》,《windows 程序设计》。写的一篇总结。同大部分初学者一样。对windows 的消息事件模型很不明白,在加上当时的对.net更糊涂。随之也冒出了各种各样的疑惑,现在看来真是小儿科的问题。但是多线程问题,可以说是又复杂又简单的问题。需要深入的理解判断。《win32多线程程序设计》当时就没有看明白。我还是需要从最基Critical Section开始理解,当时没有能力深入的理解Critical Section的机制。这次不能再错过了。

2010-06-07 C++虚继承初探, 再谈C++虚继承   2个月,第一次看到这本书《inside c++ object model》。这本书可以说是最最经典的c++书籍。其内容之深,内容之广,对我来说真是有如一个板砖拍在自己头上。读完这本书后,我的第一个感受就是,我恨死我在学校的c++老师了。她虽然交的东西很少,但是她连构造函数都教错我了,无语。也让我意识到了,学校学的那点c++,基本就是负的。还不如啥也没学,知道错的还不如不知道。了解C++虚函数,才能了解二进制复用,而这又是理解COM的基础基础,理解COM同样是理解.net的基础。想了解虚函数,必须通过了解类的构成,以及为什么这样构成。真的。在没有写过这门语言的编译器之前,真的没有资格对这门语言评头论足。天啊,我还是想说,C++,你太难了,太复杂了。

  有趣的是,我现在对c++的理解也仅仅在那个程度上。 在我眼里,c++ 就是一个加了函数指针的c(这个是一个很不恰当的比喻,但是请原谅我实在没有想出其他恰当比喻)。模板,构造函数,析构函数。以及泛型一无所知。

2010-10-06 PE文件初探一, PE文件初探二   之前的学习,让我完全迷茫了。真是学啥啥不会,问啥啥不会。这会终于清醒了。因为我终于找到了,我到底该怎么学。MSJ,绝对是最适合我的资料。under the hood 这一系列绝对是经典中的经典。如何了解编程。从了解程序是如何保存在计算机开始是最好不过的。《csapp》同样也是和《inside c++ object model》,给人一种板砖拍的感觉,同样也是这样的顺序。从机器的角度理解程序的编译,链接,以及中间的符号等等问题。将是学习编程最好的开始。PE文件,同样是一个非常古老的东东,虽然至少也有20年了。但是依然值得学,即使在.net平台开发。真的了解.net的程序是如何执行的么?先搞定win32程序的启动过程吧。这里面的遗憾,是没有总结资源类型数据。的确。一下次都搞定,真的需要毅力和强大的知识储备。

  我相信有很多和我一样的同学同样会遇到这样的问题。知道要学习的知识非常多,但是却无法下手。真的,有时候真想从95甚至dos开始理解OS,理解从无到有,才能理解nt存在的原因。但是这个真是不是一般人可以做到的,强大的知识储备是在是太必要了。而对像我这样的小白,真的无法想象。但是《csapp》+ under the hood 绝对能够给你指出学习的路径。一开始的确十分枯燥,就像练功的基本功,无聊,重复,看不到希望,需要放下浮躁、急功近利的心态,慢慢积累。好的基础,越学到最后,学习效率越快。就像动态规划,这些基础就是那张表,用来降低日后运算的复杂度。同样的智商,你才能学的更深,学的更快。当然,你也会遇到风险。谁能保证那张表的所有内容都会在日后的计算中用到?谁能保证你还没把表算完,便已经程序溢出了?而《csapp》+ under the hood就是那张表,在我的角度看。

2010-12-15 SEH学习笔记一,SEH 学习笔记二   SEH 给我们提供了一个如此出色的异常处理,但我们却对这个知之甚少。这2篇理解了SEH的编译器级的很基础的实现。我觉得真的非常值得一看。同样SEH背后的,安全漏洞,包括MS提供的各种安全机制保证OS安全。以及背后的有关编译器OS之前的“互动”。的确是非常的有意思,里面的水也不浅。如果你也对代码安全感兴趣,SEH的基本知识,不能只停留在几个key word上。

  回头看来,发现,自己学的太杂了。而且真是觉得太快了,很多东西都没有经过时间的沉淀变已经成为了一种习惯。如果一开始就是错的,那么该怎么办?

  最近由于项目实践,不得不看一些STL代码。挑战自己对模板的恐惧。但是,我真的后悔了。在STL面前,我之前的所有认识,都是shit。我连一个简单的快排,甚至一个简单的资源分配,释放都不会。真的,我还没有关闭盒子的情况下又打开了另一个盒子,我怕我的好奇心会驱使我陷入茫茫的细节而忽视了对知识的整体把握。真的。也许我离再次迷茫,已经不远了。

SEH 学习笔记二

| Comments

之前我们知道了异常是什么样的,以及我们写好了出现异常时执行的代码。那么windows是如何调用我们的代码呢?在了解这个之后,global unwind,异常嵌套也就容易了解了。不得不说,如果之前没有了解SEH,这的确是一件非常有挑战的事情。当然,如果从根上,也就是从硬件到os kernel再到user mode这个就不是挑战,而是不可能一下子完成的任务了。我们还是一步步来,从异常跑到user mode开始。准备好了么?

  首先想一个问题,上一篇我们的第一个例子,我们保护了一段代码,但是同时,为了修正这段代码,我们又引入了一段代码。事实上,我们的确没有解决问题,谁能保证我们又引入的代码本身不会再产生异常呢?而且,这次是在发生异常的“拯救”过程中又产生异常。让我们先看一个简单的例子,我对上一篇的第一个例子,加了一点点修饰。

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    // Indicate that we made it to our exception handler
    printf( "Hello from an exception handler\n" );

    if (ExceptionRecord->ExceptionFlags & 0x10)
    {
        printf( "bad except\n" );
    }
    else
    {
        //  bad happen
        __asm
        {
            mov     eax,   0
            mov     [eax], 1
        }
    }

    //
    // Change EAX in the context record so that it points to someplace
    // where we can successfully write
    ContextRecord->Eax = (DWORD)&scratch;

    // Tell the OS to restart the faulting instruction
    return ExceptionContinueExecution;
}

结果

Hello from an exception handler
Hello from an exception handler
bad except
After writing!

  如果我们把ExceptionRecord->ExceptionFlags & 0x10 判断去掉,无条件的执行 bad ,那么我们好像陷入了死循环中,不停的输出Hello from an exception handler,而整个线程也死在了栈溢出,栈溢出是一个非常严重的异常,他会导致我们的finally block 无法执行,我们获得的同步变量没有被释放掉,即使我们填入了finally block。 我们一切的梦似乎还没有开始就结束了,而原因仅仅是因为我们在异常中又产生了一个异常。异常本身已经很让人头痛了,现在又来了一个。为了彻底了解,我们必须从了解OS是如何调用我们的代码,如何分配异常开始。

异常user mode 从KiUserExceptionDispatcher 开始。

VOID KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
 {
     DWORD retValue;

     // Note: If the exception is handled, RtlDispatchException() never returns
     if ( RtlDispatchException( pExceptRec, pContext ) )
         retValue = NtContinue( pContext, 0 );
     else
         retValue = NtRaiseException( pExceptRec, pContext, 0 );

     EXCEPTION_RECORD excptRec2;

     excptRec2.ExceptionCode = retValue;
     excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
     excptRec2.ExceptionRecord = pExcptRec;
     excptRec2.NumberParameters = 0;

     RtlRaiseException( &excptRec2 );
 }


BOOLEAN
NTAPI
RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
                     IN PCONTEXT Context)
{
    PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, NestedFrame = NULL;
    DISPATCHER_CONTEXT DispatcherContext;
    EXCEPTION_RECORD ExceptionRecord2;
    EXCEPTION_DISPOSITION Disposition;
    ULONG_PTR StackLow, StackHigh;
    ULONG_PTR RegistrationFrameEnd;

    /* Perform vectored exception handling if we are in user mode */
    if (RtlpGetMode() != KernelMode)
    {
        /* Call any registered vectored handlers */
        if (RtlCallVectoredExceptionHandlers(ExceptionRecord, Context))
        {
            /* Exception handled, continue execution */
            return TRUE;
        }
    }

    /* Get the current stack limits and registration frame */
    RtlpGetStackLimits(&StackLow, &StackHigh);
    RegistrationFrame = RtlpGetExceptionList();

    /* Now loop every frame */
    while (RegistrationFrame != EXCEPTION_CHAIN_END)//#define EXCEPTION_CHAIN_END  -1
    {
        /* Find out where it ends */
        RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame +
                                sizeof(EXCEPTION_REGISTRATION_RECORD);

        /* Make sure the registration frame is located within the stack */
        if ((RegistrationFrameEnd > StackHigh) ||
            ((ULONG_PTR)RegistrationFrame < StackLow) ||
            ((ULONG_PTR)RegistrationFrame & 0x3))
        {
            /* Check if this happened in the DPC Stack */
            if (RtlpHandleDpcStackException(RegistrationFrame,
                                            RegistrationFrameEnd,
                                            &StackLow,
                                            &StackHigh))
            {
                /* Use DPC Stack Limits and restart */
                continue;
            }

            /* Set invalid stack and return false */
            ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
            return FALSE;
        }

        /* Check if logging is enabled */
        RtlpCheckLogException(ExceptionRecord,
                              Context,
                              RegistrationFrame,
                              sizeof(*RegistrationFrame));

        //这里应该有判断SEH是否有效,reactos这里并没有检查。


/* Call the handler */
        Disposition = RtlpExecuteHandlerForException(ExceptionRecord,
                                                     RegistrationFrame,
                                                     Context,
                                                     &DispatcherContext,
                                                     RegistrationFrame->
                                                     Handler);

        /* Check if this is a nested frame */
        if (RegistrationFrame == NestedFrame)
        {
            /* Mask out the flag and the nested frame */
            ExceptionRecord->ExceptionFlags &= ~EXCEPTION_NESTED_CALL;
            NestedFrame = NULL;
        }

        /* Handle the dispositions */
        switch (Disposition)
        {
            /* Continue searching */
            case ExceptionContinueExecution:

                /* Check if it was non-continuable */
                if (ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
                {
                    /* Set up the exception record */
                    ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                    ExceptionRecord2.ExceptionCode =
                        STATUS_NONCONTINUABLE_EXCEPTION;
                    ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                    ExceptionRecord2.NumberParameters = 0;

                    /* Raise the exception */
                    RtlRaiseException(&ExceptionRecord2);
                }
                else
                {
                    /* Return to caller */
                    return TRUE;
                }

            /* Continue searching */
            case ExceptionContinueSearch:
                break;

            /* Nested exception */
            case ExceptionNestedException:

                /* Turn the nested flag on */
                ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;

                /* Update the current nested frame */
                if (DispatcherContext.RegistrationPointer > NestedFrame)
                {
                    /* Get the frame from the dispatcher context */
                    NestedFrame = DispatcherContext.RegistrationPointer;
                }
                break;

            /* Anything else */
            default:

                /* Set up the exception record */
                ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
                ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                ExceptionRecord2.NumberParameters = 0;

                /* Raise the exception */
                RtlRaiseException(&ExceptionRecord2);
                break;
        }

        /* Go to the next frame */
        RegistrationFrame = RegistrationFrame->Next;
    }

    /* Unhandled, return false */
    return FALSE;
}

  上面的代码来自ReactOS,和我们xp2上的代码已经很接近了(除了SEH的安全机制),RtlDispatchException将处理异常的部分交给了RtlpExecuteHandlerForException。

  看一下RtlUnwind,同样来自ReactOS。同样把脏活给了RtlpExecuteHandlerForUnwind来做。

VOID
NTAPI
RtlUnwind(IN PVOID TargetFrame OPTIONAL,
          IN PVOID TargetIp OPTIONAL,
          IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL,
          IN PVOID ReturnValue)
{
    PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, OldFrame;
    DISPATCHER_CONTEXT DispatcherContext;
    EXCEPTION_RECORD ExceptionRecord2, ExceptionRecord3;
    EXCEPTION_DISPOSITION Disposition;
    ULONG_PTR StackLow, StackHigh;
    ULONG_PTR RegistrationFrameEnd;
    CONTEXT LocalContext;
    PCONTEXT Context;

    /* Get the current stack limits */
    RtlpGetStackLimits(&StackLow, &StackHigh);

    /* Check if we don't have an exception record */
    if (!ExceptionRecord)
    {
        /* Overwrite the argument */
        ExceptionRecord = &ExceptionRecord3;

        /* Setup a local one */
        ExceptionRecord3.ExceptionFlags = 0;
        ExceptionRecord3.ExceptionCode = STATUS_UNWIND;
        ExceptionRecord3.ExceptionRecord = NULL;
        ExceptionRecord3.ExceptionAddress = _ReturnAddress();
        ExceptionRecord3.NumberParameters = 0;
    }

    /* Check if we have a frame */
    if (TargetFrame)
    {
        /* Set it as unwinding */
        ExceptionRecord->ExceptionFlags |= EXCEPTION_UNWINDING;
    }
    else
    {
        /* Set the Exit Unwind flag as well */
        ExceptionRecord->ExceptionFlags |= (EXCEPTION_UNWINDING |
                                            EXCEPTION_EXIT_UNWIND);
    }

    /* Now capture the context */
    Context = &LocalContext;
    LocalContext.ContextFlags = CONTEXT_INTEGER |
                                CONTEXT_CONTROL |
                                CONTEXT_SEGMENTS;
    RtlpCaptureContext(Context);

    /* Pop the current arguments off */
    Context->Esp += sizeof(TargetFrame) +
                    sizeof(TargetIp) +
                    sizeof(ExceptionRecord) +
                    sizeof(ReturnValue);

    /* Set the new value for EAX */
    Context->Eax = (ULONG)ReturnValue;

    /* Get the current frame */
    RegistrationFrame = RtlpGetExceptionList();

    /* Now loop every frame */
    while (RegistrationFrame != EXCEPTION_CHAIN_END)
    {
        /* If this is the target */
        if (RegistrationFrame == TargetFrame) ZwContinue(Context, FALSE);

        /* Check if the frame is too low */
        if ((TargetFrame) &&
            ((ULONG_PTR)TargetFrame < (ULONG_PTR)RegistrationFrame))
        {
            /* Create an invalid unwind exception */
            ExceptionRecord2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
            ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
            ExceptionRecord2.ExceptionRecord = ExceptionRecord;
            ExceptionRecord2.NumberParameters = 0;

            /* Raise the exception */
            RtlRaiseException(&ExceptionRecord2);
        }

        /* Find out where it ends */
        RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame +
                               sizeof(EXCEPTION_REGISTRATION_RECORD);

        /* Make sure the registration frame is located within the stack */
        if ((RegistrationFrameEnd > StackHigh) ||
            ((ULONG_PTR)RegistrationFrame < StackLow) ||
            ((ULONG_PTR)RegistrationFrame & 0x3))
        {
            /* Check if this happened in the DPC Stack */
            if (RtlpHandleDpcStackException(RegistrationFrame,
                                            RegistrationFrameEnd,
                                            &StackLow,
                                            &StackHigh))
            {
                /* Use DPC Stack Limits and restart */
                continue;
            }

            /* Create an invalid stack exception */
            ExceptionRecord2.ExceptionCode = STATUS_BAD_STACK;
            ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
            ExceptionRecord2.ExceptionRecord = ExceptionRecord;
            ExceptionRecord2.NumberParameters = 0;

            /* Raise the exception */
            RtlRaiseException(&ExceptionRecord2);
        }
        else
        {
            /* Call the handler */
            Disposition = RtlpExecuteHandlerForUnwind(ExceptionRecord,
                                                      RegistrationFrame,
                                                      Context,
                                                      &DispatcherContext,
                                                      RegistrationFrame->
                                                      Handler);
            switch(Disposition)
            {
                /* Continue searching */
                case ExceptionContinueSearch:
                    break;

                /* Collission */
                case ExceptionCollidedUnwind :

                    /* Get the original frame */
                    RegistrationFrame = DispatcherContext.RegistrationPointer;
                    break;

                /* Anything else */
                default:

                    /* Set up the exception record */
                    ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                    ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
                    ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                    ExceptionRecord2.NumberParameters = 0;

                    /* Raise the exception */
                    RtlRaiseException(&ExceptionRecord2);
                    break;
            }

            /* Go to the next frame */
            OldFrame = RegistrationFrame;
            RegistrationFrame = RegistrationFrame->Next;

            /* Remove this handler */
            RtlpSetExceptionList(OldFrame);
        }
    }

    /* Check if we reached the end */
    if (TargetFrame == EXCEPTION_CHAIN_END)
    {
        /* Unwind completed, so we don't exit */
        ZwContinue(Context, FALSE);
    }
    else
    {
        /* This is an exit_unwind or the frame wasn't present in the list */
        ZwRaiseException(ExceptionRecord, Context, FALSE);
    }
}

RtlpExecuteHandlerForUnwind 和RtlpExecuteHandlerForException 是汇编写的代码,这个函数的尾部会跳转到ExecuteHandler。

PUBLIC _RtlpExecuteHandlerForException@20
_RtlpExecuteHandlerForException@20:

    /* Copy the routine in EDX */
    mov edx, offset _RtlpExceptionProtector

    /* Jump to common routine */
    jmp _RtlpExecuteHandler@20


PUBLIC _RtlpExecuteHandlerForUnwind@20
_RtlpExecuteHandlerForUnwind@20:
    /* Copy the routine in EDX */
    mov edx, offset _RtlpUnwindProtector


_RtlpExecuteHandler@20:

    /* Save non-volatile */
    push ebx
    push esi
    push edi

    /* Clear registers */
    xor eax, eax
    xor ebx, ebx
    xor esi, esi
    xor edi, edi

    /* Call the 2nd-stage executer */
    push [esp+32]
    push [esp+32]
    push [esp+32]
    push [esp+32]
    push [esp+32]
    call _RtlpExecuteHandler2@20

    /* Restore non-volatile */
    pop edi
    pop esi
    pop ebx
    ret 20


PUBLIC _RtlpExecuteHandler2@20
_RtlpExecuteHandler2@20:

    /* Set up stack frame */
    push ebp
    mov ebp, esp

    /* Save the Frame */
    push [ebp+12]

    /* Push handler address */
    push edx

    /* Push the exception list */
    push [fs:TEB_EXCEPTION_LIST]

    /* Link us to it */
    mov [fs:TEB_EXCEPTION_LIST], esp //这里我们构造了一个nt_EXCEPTION_REGISTRATION

    /* Call the handler */
    push [ebp+20]
    push [ebp+16]
    push [ebp+12]
    push [ebp+8]
    mov ecx, [ebp+24]
    call ecx

    /* Unlink us */
    mov esp, [fs:TEB_EXCEPTION_LIST]

    /* Restore it */
    pop [fs:TEB_EXCEPTION_LIST]

    /* Undo stack frame and return */
    mov esp, ebp
    pop ebp
    ret 20

  我们看到了,其实,当我们真正执行filter(其实是vc的运行时库函数except_handler)之前,windows 已经为我们提前构造了一个nt_EXCEPTION_REGISTRATION, 和我们之前的vc_EXCEPTION_REGISTRATION,不同的是,在基本的EXCEPTION_REGISTRATION结构体之后,只是加了一个成员PEXCEPTION_REGISTRATION_RECORD RegistrationFrame,这个成员的意义则就是判断异常嵌套。当然,这依然没有解决问题(异常再产生异常),我们需要知道RtlpExceptionProtector,_RtlpUnwindProtector。不过,我们这个担心有点多余,因为这个是os本身的代码,如果他自己还不能保证正确,那么后面还有什么意义呢?

_RtlpExceptionProtector:

    /* Assume we'll continue */
    mov eax, ExceptionContinueSearch

    /* Put the exception record in ECX and check the Flags */
    mov ecx, [esp+4]
    test dword ptr [ecx+EXCEPTION_RECORD_EXCEPTION_FLAGS], EXCEPTION_UNWINDING + EXCEPTION_EXIT_UNWIND
    jnz return

    /* Save the frame in ECX and Context in EDX */
    mov ecx, [esp+8]
    mov edx, [esp+16]

    /* Get the nested frame */
    mov eax, [ecx+8]

    /* Set it as the dispatcher context */
    mov [edx], eax

    /* Return nested exception */
    mov eax, ExceptionNestedException

return:
    ret 16


_RtlpUnwindProtector:

    /* Assume we'll continue */
    mov eax, ExceptionContinueSearch

    /* Put the exception record in ECX and check the Flags */
    mov ecx, [esp+4]
    test dword ptr [ecx+EXCEPTION_RECORD_EXCEPTION_FLAGS], EXCEPTION_UNWINDING + EXCEPTION_EXIT_UNWIND
    jz .return

    /* Save the frame in ECX and Context in EDX */
    mov ecx, [esp+8]
    mov edx, [esp+16]

    /* Get the nested frame */
    mov eax, [ecx+8]

    /* Set it as the dispatcher context */
    mov [edx], eax

    /* Return collided unwind */
    mov eax, ExceptionCollidedUnwind

.return:
    ret 16

  我们看到了,当异常嵌套发生时,windows和处理之前的异常一样,依然会走这个流程。来自Matt Pietrek,之前介绍的文章。

KiUserExceptionDispatcher()
     RtlDispatchException()
         RtlpExecuteHandlerForException()
             ExecuteHandler() // Normally goes to __except_handler3
 ---------
 __except_handler3()
     scopetable filter-expression()
     __global_unwind2() 
         RtlUnwind()
             RtlpExecuteHandlerForUnwind() 
     scopetable __except block()

  只是不同的是,嵌套发生时,fs:[0]上的frame,已经不是我们的代码,而是nt_frame,回调函数的事情也很简单,判断异常时候是在unwind或是unwind_exit,如果不是,那么我们知道了这个是异常传递的第一次,而这个是在正常情况下,不可能发生的(正常情况指的是异常没有嵌套,执行nt_frame的只可能是第二次,也就是unwind 或是 exit_unwind,nt_frame返回ExceptionContinueSearch,让异常继续传递给我们的代码)。那么很显然,现在遇到了异常嵌套,nt_frame返回了 ExceptionNestedException,并且将frame 保存在了edx中,也就是修改了DispatcherContext,RtlpExecuteHandlerForException的第四个参数。那么当返回时,windows 就可以知道是那个frame 在处理异常的时候,干了坏事(又产生了异常)。好吧。流程又恢复一样,继续的去遍历 fs:[0],直到我们发现了这个干坏事的frame,然后我们把异常嵌套标志位去掉,ExceptionRecord->ExceptionFlags &= ~EXCEPTION_NESTED_CALL; NestedFrame = NULL。

让我们看一个简单的例子。对上一篇的Matt Pietrek的例子做了些修改。

void WalkSEHFrames( void )
{
    VC_EXCEPTION_REGISTRATION * pVCExcRec;
    printf( "\n" );

    // Get a pointer to the head of the chain at FS:[0]
    __asm   mov eax, FS:[0]
    __asm   mov [pVCExcRec], EAX

    // Walk the linked list of frames.  0xFFFFFFFF indicates the end of list
    while (  0xFFFFFFFF != (unsigned)pVCExcRec )
    {
        ShowSEHFrame( pVCExcRec );
        pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
    }       
}

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    // Indicate that we made it to our exception handler
    printf( "Hello from an exception handler\n" );

    WalkSEHFrames();

     if (ExceptionRecord->ExceptionFlags & 0x10)
     {
         printf( "bad except\n" );
     }
     else
     {
         //  bad happen
         __asm
         {
             mov     eax,   0
             mov     [eax], 1
         }
     }
     //
     // Change EAX in the context record so that it points to someplace
     // where we can successfully write
     ContextRecord->Eax = (DWORD)&scratch;

     // Tell the OS to restart the faulting instruction
     return ExceptionContinueExecution;
}

int _tmain(int argc, _TCHAR* argv[])
{
    DWORD handler = (DWORD)_except_handler;

    __try
    {
        __asm
        {                           // Build EXCEPTION_REGISTRATION record:
            push    handler         // Address of handler function
            push    FS:[0]          // Address of previous handler
            mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
        }

        WalkSEHFrames();

        __asm
        {
            mov     eax,0           // Zero out EAX
            mov     [eax], 1        // Write to EAX to deliberately cause a fault
        }

        printf( "After writing!\n" );

        __asm
        {                           // Remove our EXECEPTION_REGISTRATION record
            mov     eax,[ESP]       // Get pointer to previous record
            mov     FS:[0], EAX     // Install previous record
            add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
        }

        WalkSEHFrames();

    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        printf("never happen\n");
    }


    return 0;
}

产生的结果

Frame: 0022FDD8  Handler: 0118110E  Prev: 0022FEC0  Scopetable: 00000000

Frame: 0022FEC0  Handler: 01181091  Prev: 0022FF10  Scopetable: 01186C80

Frame: 0022FF10  Handler: 01181096  Prev: 0022FF64  Scopetable: 81F63D92

Frame: 0022FF64  Handler: 7712D74D  Prev: FFFFFFFF  Scopetable: 021DFE40

Hello from an exception handler

Frame: 0022FA04  Handler: 7715660D  Prev: 0022FDD8  Scopetable: 0022FDD8

Frame: 0022FDD8  Handler: 0118110E  Prev: 0022FEC0  Scopetable: 00000000

Frame: 0022FEC0  Handler: 01181091  Prev: 0022FF10  Scopetable: 01186C80

Frame: 0022FF10  Handler: 01181096  Prev: 0022FF64  Scopetable: 81F63D92

Frame: 0022FF64  Handler: 7712D74D  Prev: FFFFFFFF  Scopetable: 021DFE40

Hello from an exception handler

Frame: 0022F540  Handler: 7715660D  Prev: 0022FA04  Scopetable: 0022FDD8

Frame: 0022FA04  Handler: 7715660D  Prev: 0022FDD8  Scopetable: 0022FDD8

Frame: 0022FDD8  Handler: 0118110E  Prev: 0022FEC0  Scopetable: 00000000

Frame: 0022FEC0  Handler: 01181091  Prev: 0022FF10  Scopetable: 01186C80

Frame: 0022FF10  Handler: 01181096  Prev: 0022FF64  Scopetable: 81F63D92

Frame: 0022FF64  Handler: 7712D74D  Prev: FFFFFFFF  Scopetable: 021DFE40

bad except
After writing!

Frame: 0022FEC0  Handler: 01181091  Prev: 0022FF10  Scopetable: 01186C80

Frame: 0022FF10  Handler: 01181096  Prev: 0022FF64  Scopetable: 81F63D92

Frame: 0022FF64  Handler: 7712D74D  Prev: FFFFFFFF  Scopetable: 021DFE40

  0x0118110E这个是我们自己的handler地址,也就是handler。0x01181091则是vc_handler的地址, 0x7715660D, 这个地址,就是我们的nt_frame的地址。0x01181096是CRT main函数时加的, 0x7712D74D 地址是KERNEL32.DLL 的 BaseProcessStart加的。

  在发生异常之后,windows为了保证vc_exception_hander抛出异常可以处理,加了nt_frame,但是运行时,再次引发异常,那么则会继续走KiUserExceptionDispatcher…那么则会再加入一个nt_frame在stack上。这时异常没有继续抛出,最后,windows 会逐个卸载掉那些frame。

再搞明白这些之后,就很容易理解一开始的例子了,为什么死在栈溢出,如果我们在异常嵌套的时候,继续产生异常,那么windows会不断的去走KiUserExceptionDispatcher…而SEH的frame是建立在stack上的,那么stack overflow 实在是不可避免的事情了。所以,我们最好按照msdn上的建议,filter的代码一定要简洁(我们在遍历和unwind的时候,执行2次),而且一定不能产生任何异常,否则,后果十分严重(可能死在stack overflow)。

  但是,事实上,我们却很难写出不再产生异常的代码,即使代码很简洁,而且逻辑上看上去并没有问题。那时因为我们的惯性思维停留在了像,c,c++这些高级语言上了(相对汇编)。比如下面的例子。来自《windows 核心编程》。

char g_szBuffer[100];

void FunclinRoosevelt1() 
{
   int x = 0;
   char *pchBuffer = NULL;

   __try
   {
      *pchBuffer = 'J';
      x = 5 / x;
   }
   __except(OilFilter1(&pchBuffer)) 
   {
      MessageBox(NULL, "An exception occurred", NULL, MB_OK);
   }
   MessageBox(NULL, "Function completed", NULL, MB_OK);
}

LONG OilFilter1(char **ppchBuffer)
{
   if(*ppchBuffer == NULL) 
   {
      *ppchBuffer = g_szBuffer;
      return(EXCEPTION_CONTINUE_EXECUTION);
   }
   return(EXCEPTION_EXECUTE_HANDLER);
}

  一段看似,没有问题的代码。但是这个确实是一个问题很隐晦的代码。我们看似修改了pchBuffer,使得pchBuffer 指向一个合法的地址,但是继续执行依然会有可能产生异常。原因在于,编译器有可能给我们产生如下代码,对*pchBuffer = ‘J’; 来说。

MOV EAX, [pchBuffer]  // Move the address into a register
MOV [EAX], 'J'        // Move 'J' into the address

  我们只是修改了pchBuffer,并没有修改eax的值,程序并不能真正的继续执行。所以,如果想使用EXCEPTION_CONTINUE_EXECUTION,Jeffrey Richter告诉我们一定要小心,小心。但是我相信,即使这个功能很cool,没有人会愿意每次编译之后,查看下汇编代码,看看是否生成了我们想要的代码。所以,我大胆的说,想使用EXCEPTION_CONTINUE_EXECUTION,最简单的方法就是在汇编下跑,c,c++下,就不用想了。而Jeffrey Richter 告诉我们系统在处理访问违规的时候,有类似的使用,那么系统那部分的代码,也很有可能是汇编直接写的。

所以,在c++下,MS自己都劝开发者使用c++自己的异常语法,而不是直接使用SEH。这个不仅能使代码有强的移植性,而且也能避免EXCEPTION_CONTINUE_EXECUTION。

SEH就像ReactOS上写的一样,“SEH is a game which is played between OS and Compiler (Keywords: try, except, __finally)”。vc通过这些关键字,使得开发者只需要了解一点点知识,便可以体验到SEH的强大。当然,强大的封装之后,必然会给我们理解带来了不少困难。如果你也对这些感兴趣,那么真的可以继续下去。因为我现在所知道的有关SEH的部分仅仅是最最基础的部分,这些部分早在10几年前就已经存在。

  下一篇将开始真正的接触SEH。

  最后写给自己。

本来应该更详细的阐述一些细节,特别是local unwind, 他在执行我们的代码之前也构造了一个自己的frame,有兴趣的同学可以自己研究下。这个和AbnormalTermination()的实现息息相关。只是我发现vs2008 和vc6 在这上面似乎有些不同,vs2008似乎很强大的把这个完全优化掉了(也许不是因为这个原因,或是其他原因,了解一点vs的应该都知道vs这方面很强大)。在和他纠结了半个多小时后,我也实在是没有兴趣去和他比下去了。

  对于像我这样长期处在user mode的开发者来说。了解到这一地步,在实现上已经是足够了。但是即使已经了解大部分的SEH核心行为后(除去安全机制,这个同样对大多数开发者是透明的),依然很难说清楚什么时候改抛出异常,什么时候该使用返回值。(唯一可以肯定的是,不能有时候返回值,有时候又抛异常 :P)

  这里先记录一下自己的想法吧。当然,这里的异常主要还是SEH,c++概念不在考虑之内(即使在windows 底层实现可能会很相像和SEH)。

首先看SEH的finally,这个的确看上去是一个很美好的东东,Jeffrey Richter给了我们几点使用finally的理由。

They simplify error processing because all cleanup is in one location and is guaranteed to execute. They improve program readability. They make code easier to maintain. They have minimal speed and size overhead if used correctly.   在我看来首先第一条就有问题,finally中的代码能够肯定保证执行么?显然不行,至少现在不可以。在一些严重的异常下,如stack overflow 或是进程,线程直接被Terminate。都不能直接执行。

  2和3条,这个的确是完美,但是并不是非常完美,因为能够做到这一点的不仅仅是finally,使用良好的编程规范,如合理的goto语句,等等。我们依然能够做到在一个地方释放空间。来增强程序的可读性。比如pthread中的一段。

result = pthread_mutex_init (&rwl->mtxExclusiveAccess, NULL);
if (result != 0)
{
  goto FAIL0;
}

result = pthread_mutex_init (&rwl->mtxSharedAccessCompleted, NULL);
if (result != 0)
{
  goto FAIL1;
}

result = pthread_cond_init (&rwl->cndSharedAccessCompleted, NULL);
if (result != 0)
{
  goto FAIL2;
}

  最后一条,前半句非常对,的确SEH的机制非常迅速(相对,没有绝对),在目前看来在不发生异常的时候,我们的确很享受这个过程,但是当你知道SEH背后的安全机制之后,你可能就不会这么认为了,那可不是点点CPU周期可以搞定的,而且还有后半句 if used correctly。使用异常,那么我们需要理解更多的有关异常本身的问题,包括异常是什么?异常如何调度?什么时候效率影响大?等等问题,也会带来更多的对程序员的心智上的负担。

  其次 except。同之前说的一样,由于使用异常,的确造成了非常大的知识的负担和程序运行上的负担。但是,当你去编写一个需要长期运行,而且要保证高效稳定性的程序之下。没有异常机制,实在是一件不可能的事情。当整运算一个大数据量的时候(已经算了几个小时了),若是来一点意外,总不能就推到重来计算等。而避免这些的最好的方法就是处理异常。

  但是在一些情景下,我们却不能使用异常,比如在一些硬件不够高的地方,嵌入式平台等。抛出异常是被禁止的。在一些运算密集性场景,如游戏引擎上,异常依然是禁区。

  但是在看到一些.net 的源代码上,比如Dictionary,我记得是抛出异常的。55,扯的实在是太远了。

之前描述的不清楚。 如果没有发生异常,SEH的机制比较迅速。只是修改了stack 上的临时变量和线程的 exceptionList(或没有修改)。

只是当抛出异常的时候,整个运行的效率才会降下来。

SEH学习笔记一

| Comments

SEH(structured exception handling),结构化异常处理。在windows本身开发中运用的非常广泛,而且MS并没有独享,并且通过vs为开发者提供了方便几个关键字来支持。try, exception,__finally。但是讲解的却非常少。本文希望能够给大家抛砖引玉一下。

http://www.microsoft.com/msj/0197/exception/exception.aspx,这篇是理解SEH必须的文章,虽然他的时间悠久,但是却真正的解释了SEH的编译器级实现,下面的一些示例代码也来自这里。

相关的不错的SEH文章,http://www.woodmann.com/crackz/Tutorials/Seh.htm。

http://blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx 这里讲了一些.net 异常机制,之前讲一些SEH也很不错。

SEH中,在《windows 核心编程》中有一些讲解,但是我相信绝大多数,想我这样的初学者,并不能理解Jeffrey Richter的意思。其中最富有争议的就是“栈展开”(stack unwind),这个可以说是非常有想象力的一个词,伴随这个还有全局展开(global unwind),和局部展开(local unwind)。以下内容,主要围绕《windows 核心编程》中比较容易让我这样的初学者困惑的地方展开(unwind? :P)。

首先我们需要对SEH有一个大体的认识,

当异常出现的时候,我们可以有选择性的处理异常,将相同的异常处理函数集中一起,大大减少了代码的维护工作,这意味着处理异常的时候,将有类似非局部跳转的能力。 异常和返回值判断的最根本的不同是,异常真正的做到了健壮性,甚至连栈溢出的问题都可以恢复运行(当然,这个恢复没有任何意义,主要是能够保存错误信息)。所以异常是和操作系统结合的,所以必然导致了复杂性的大大提高,效率上的降低。 程序的执行,需要一些最基本的运行环境,而在windows 中则是contex,(上下文),其中保存了大量的寄存器的值,而通过这些可以保证程序的执行环境正确,而这是在进行非局部跳转必须做到的事情。所以,在遇到try block的时候,编译器会在栈空间上保存一些信息,做为一个结点并将这些信息用链表联系起来,这样,当异常发生的时候,操作系统找到链表的头结点,然后遍历list,执行我们的代码,并找到相对应的处理异常的代码。而这个头结点,就保存在FS:[0]。当windows 遍历list,并找到相对应的代码时,由于程序控制流程的改变,在发生异常,到找到执行代码的这部分之间的一些临时变量都没有被释放掉(这里面不仅有我们的,还有一些是编译器默默为我们做的,比如之前提到的try所加入的节点必须从之前的list删掉)。而这个做的释放过程就是unwind。处理多个try的为global unwind,处理当前的try 上的__try则是local unwind(这里不是很准确,后面会详细解释)。

结束处理程序(Termination Handlers),看起来简单也十分让人疑惑,为什么 return, goto,longjump,异常,控制流离开try block的时候,可以去执行finally block呢? 同样,为什么ExitProcess, ExitThread, TerminateProcess, or TerminateThread则不能被执行呢?为什么可以使用goto到try外面,而不能跳入一个try block?等等。

异常处理程序(Exception Handlers),则更让新手疑惑,特别是在结合了结束处理程序情况下,在程序的执行流程则变的诡异起来,而我们看到在vc中的SEH并不能够支持finally 和except结合一起使用,这又是为什么?使用SEH是否为我们程序增加了相当的负担?SEH是否安全?

为了清楚的认识这些问题,我们必须更进一步的去探究SEH的具体实现过程,由于不同厂商不同编译器的实现方式不同,所以以下的部分来自MS自己的vc。而其由于SEH涉及到了一些安全问题和硬件的部分,所以在不同的vc 版本,不同的操作系统不同的计算机下的情况也不同。当然,为了简单,我们先看最简单的vc6。在我们正式进入细节的时候,让我们先暂时忘记那些__try关键字。

  异常是操作系统传给我们写的程序,我们写好处理异常的代码,那么操作系统是如何调用我们写的函数呢?当然是通过回调函数做的,那么这个回调函数是什么样子的呢?

EXCEPTION_DISPOSITION
__cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );

在EXCPT.H中,我们可以找到这个定义。

typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
}  EXCEPTION_RECORD;

EXCEPTION_RECORD 定义异常,更多的可以参考msdn,http://msdn.microsoft.com/en-us/library/aa363082(VS.85).aspx

contex的定义则根据不同的硬件有不同的定义,这里面定义了线程运行的环境,上下文。找到了回调函数,和异常的样子,那么操作系统是如何调用呢?还记得之前提到的list么?fs:[0],那里,有我们需要的,我们需要知道另一个结构体。这是一个汇编上的定义。

_EXCEPTION_REGISTRATION struc
        prev    dd              ?
        handler dd            ?
_EXCEPTION_REGISTRATION ends

prev记录了上一个_EXCEPTION_REGISTRATION结构体的地址,而handler则是我们回调函数的地址,操作系统通过fs:[0],找到了一系列的我们写的回调函数。

让我们先试一下。

//==================================================
// MYSEH - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH.CPP
// To compile: CL MYSEH.CPP
//==================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD  scratch;

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    // Indicate that we made it to our exception handler
    printf( "Hello from an exception handler\n" );

    // Change EAX in the context record so that it points to someplace
    // where we can successfully write
    ContextRecord->Eax = (DWORD)&scratch;

    // Tell the OS to restart the faulting instruction
    return ExceptionContinueExecution;
}

int main(int argc, char* argv[])
{
    DWORD handler = (DWORD)_except_handler;

    __asm
    {                           // Build EXCEPTION_REGISTRATION record:
        push    handler         // Address of handler function
        push    FS:[0]          // Address of previous handler
        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
    }

    __asm
    {
        mov     eax,0           // Zero out EAX
        mov     [eax], 1        // Write to EAX to deliberately cause a fault
    }

    printf( "After writing!\n" );

    __asm
    {                           // Remove our EXECEPTION_REGISTRATION record
        mov     eax,[ESP]       // Get pointer to previous record
        mov     FS:[0], EAX     // Install previous record
        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
    }

    return 0;
}

typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

vc通过类似的代码生成,在我们的这段代码,

mov eax,0  mov [eax], 1,

在栈空间上分配了一个EXCEPTION_REGISTRATION结构体,并插入了fs:[0]链表的表头。 当然,在最后跳出这个代码块的时候,这个栈空间的EXCEPTION_REGISTRATION结构体也必须从fs:[0]中卸载掉。而在_except_handler返回的ExceptionContinueExecution,则意味着告诉OS,需要从发生异常的那个语句重新执行,一切都是那么的简单和自然。为了简单,我们在首节点就处理了这个异常,让我们再进一步,看一下异常是如何传递的。

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
        ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );

    if ( ExceptionRecord->ExceptionFlags & 1 )
        printf( " EH_NONCONTINUABLE" );
    if ( ExceptionRecord->ExceptionFlags & 2 )
        printf( " EH_UNWINDING" );
    if ( ExceptionRecord->ExceptionFlags & 4 )
        printf( " EH_EXIT_UNWIND" );
    if ( ExceptionRecord->ExceptionFlags & 8 )
        printf( " EH_STACK_INVALID" );
    if ( ExceptionRecord->ExceptionFlags & 0x10 )
        printf( " EH_NESTED_CALL" );

    printf( "\n" );

    // Punt... We don't want to handle this... Let somebody else handle it
    return ExceptionContinueSearch;
}

void HomeGrownFrame( void )
{
    DWORD handler = (DWORD)_except_handler;

    __asm
    {                           // Build EXCEPTION_REGISTRATION record:
        push    handler         // Address of handler function
        push    FS:[0]          // Address of previous handler
        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
    }

    *(PDWORD)0 = 0;             // Write to address 0 to cause a fault

    printf( "I should never get here!\n" );

    __asm
    {                           // Remove our EXECEPTION_REGISTRATION record
        mov     eax,[ESP]       // Get pointer to previous record
        mov     FS:[0], EAX     // Install previous record
        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    _try
    {
        HomeGrownFrame(); 
    }
    _except( EXCEPTION_EXECUTE_HANDLER )
    {
        printf( "Caught the exception in main()\n" );
    }
    return 0;
}

我们在_except_handler中返回了ExceptionContinueSearch,这会告诉windows,我们这个回调函数不处理这个异常,你找其他去吧。我们看到了这个输出结果。

Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the exception in main()

  第一个我们很好理解,但是第二次是什么情况呢?这个就是之前提到的unwind过程。windows依次调用fs:[0]上的exceptionlist的回调函数,并根据返回值判断该如何执行,如果是ExceptionContinueSearch,则通过EXCEPTION_REGISTRATION 的prev寻找下一个,直到找到处理异常的函数(windows在创建线程的时候,已经为我们准备好了处理异常的程序)。在找到处理异常的代码后,windows会再一次遍历list,直到处理异常的地方。这一次和第一次不同的是Exception Flags | = EH_UNWINDING,这一次,正是给那些拒绝处理这个异常的代码块一次清理自己的机会,包括一些编译器默默为我们生成的一些临时东东的移除,c++一些临时对象的析构函数调用,从fs:[0],list上删除EXCEPTION_REGISTRATION 等等,当然,我们的finally block也正好趁着这个机会把自己执行了一次。但是,在我们开心的找到回调函数地址的时候,我们却不能直接执行这个地址的代码,因为在之前,很可能运行的环境已经变化了,许多寄存器的数值已经变化了,而且更重要的是ebp esp,很可能根本和我们的这个程序不符合,程序根本不能正确执行(之前做了很多的非局部跳转),所以,必须也把函数运行的状态保存起来,这样我们才能真正的执行我们的回调函数。那么这些状态保存在哪里呢?EXCEPTION_REGISTRATION结构体的地址,在windows fs:[0]可以找到, 那么我们只需要在原有的EXCEPTION_REGISTRATION成员下增加数据就可以找到这些状态。从而正确的恢复执行。

在进一步了解之前,让我们先回顾一下文法。

__try 
{
   //Guarded body
}
__except(exception filter) 
{
   // Exception handler
}

void FuncOStimpy1()
{
   //1. Do any processing here.

   ...

   __try
   {
      //2. Call another function.
      FuncORen1();

      // Code here never executes.
   }

   __except( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) 
   {
      //8. After the unwind, the exception handler executes.
      MessageBox(…);
   }

   //9. Exception handled--continue execution.



}
void FuncORen1() 
{
   DWORD dwTemp = 0;

   //3. Do any processing here.



   __try
   {
      //4. Request permission to access protected data.
      WaitForSingleObject(g_hSem, INFINITE);

      //5. Modify the data.
      //    An exception is generated here.
      g_dwProtectedData = 5 / dwTemp;
   }
   __finally
   {
      //7. Global unwind occurs because filter evaluated
      //    to EXCEPTION_EXECUTE_HANDLER.

      // Allow others to use protected data.
      ReleaseSemaphore(g_hSem, 1, NULL);
   }
   // Continue processing--never executes.

   ...  

}

  有了现在的基础,在看上面的代码,在执行代码顺序上,已经没有疑惑了。我们所指的回调函数,其实就是exception filter,当异常在5处发生的时候,系统首先要遍历fs:[0],找到处理这个异常的代码,执行流程跑到了6,返回了EXCEPTION_EXECUTE_HANDLER,这告诉系统我认出了这个异常,然后,系统再次遍历fs:[0],这个就是unwind,然后,我们在7处的finally代码才执行,最后执行Exception handler的代码,然后程序从9处恢复执行。Jeffrey Richter中描述的global unwind,local unwind,又是什么意思呢?书写什么样的代码可以最大的提高效率?以及异常处理的效率为什么要慢呢?这背后还有许许多多的小问题,比如为什么goto 只能跳出try block,而不能跳入try block?GetExceptionCode为什么能够在filter expression 和exception-handler block,为什么不能在filter function中调用?而如果想弄清楚这一系列问题,我们需要更深入的了解SEH。当然,这才是学习的重点。由于这部分和系统相关,在异常的转发过程中,需要编译器和操作系统的支持,所以,我们需要找一个稍微简单一点的编译器和os,如果是第一次接触这个,那么最好是 vc6 + xp sp1或2000。如果对vc6有极大的抵触情绪(比如本人),使用08的时候需要在编译器中加入/GS-,否则编译器会在栈中生成其他代码(检测是否有溢出)越高的系统还可能会加入safeSEH,SEHOP,而且,具体的实现可能也会稍有不同,一上来全部接触,可能难度稍微有些大(对本人来说),所以,我们从最简单的开始。

让我们看下vc(vc6 vs2008),下的结构体。

struct _EXCEPTION_REGISTRATION {
     struct _EXCEPTION_REGISTRATION *prev;          //上一个结构体
     void (*handler)(PEXCEPTION_RECORD,             //我们的回调函数 
                     PEXCEPTION_REGISTRATION,
                     PCONTEXT,
                     PEXCEPTION_RECORD);
     struct scopetable_entry *scopetable;             
     int trylevel;
};

typedef struct _SCOPETABLE
{
    DWORD       previousTryLevel;
    DWORD       lpfnFilter        //我们的filter code address
    DWORD       lpfnHandler       //我们的exception handler block 或是 finally handler bloack address
} SCOPETABLE, *PSCOPETABLE;

这个trylevel有是什么呢?为什么要有SCOPETABLE?

我们考虑这样的一个问题。

{
...
    __try
    {
        __try { } __except() { }
    }
    __except()
    {

    }
...
    __try
    {

    }
    __except()
    {

    } 
...
}

当一个函数中,有非常多的try block时,如果我们每遇到一个try,就生成一个EXCEPTION_REGISTRATION ,加入fs:[0]然后离开之后,在从fs:[0]中卸载掉,这个的确是一个浪费时间,浪费空间的做法。vc 在做的时候,每个函数只是生成一个EXCEPTION_REGISTRATION 结构体,而在一个函数内,可能有嵌套的try block,也可能又并列的try block(以下把try 简写成try,这个的确不是一个好的书写,但是这个_是在是太麻烦了,try block 是c++的异常,和SEH很像,但也是有些不同的),那么如何才能分辨出到底是哪一个try block?trylevel 和SCOPETABLE,则是为了满足这个要求而实现的。在进入函数的时候,vc会把trylevel初始化为-1,这个表示目前的代码在当前的EXCEPTION_REGISTRATION 下,不属于try block保护下,遇到第一个try block的时候,vc把trylevel改为0,进入下一个并列的try block则为1….。struct scopetable_entry *则,保存了一个数组,previousTryLevel,告诉我们这个嵌套try block 的上一层block的index….。

可见,vc通过这些手段,在我们的代码之中,维护了一个树的结构,来标示每一个try block,并提供从内层到外层的遍历方法。handler,按理来所,应该跑我们的lpfnFilter ,这里会不会重复? 当然不会,vc实现_EXCEPTION_REGISTRATION 中,handler指向了同一个代码,vc 的运行时库函数 __except_handler ,根据vc版本后面3啊4啊什么的。原因也很简单,整个东东都有了嵌套,必然需要遍历,为了减少重复代码,和代码的安全,当然会都从一个函数入口开始,然后再去调用我们的代码。所以代码的地址,也需要保存。lpfnFilter 我们的except filter代码入口,lpfnHandler,则是我们的except block 入口。 那么,我们的finally在那里呢?由于,finally 并没有filter的概念,所以,当lpfnFilter == null的时候,vc会认为我们跑的是finally block,那么lpfnHandler则是我们的finally 的terminal handle。这也就告诉我们,为什么SEH中,不能同时存在finally 和except block了。

整个事情越来越有趣了,但是一大堆的论述,的确没有任何意思。还是让我们看看代码。我在原有的代码上加上了查看trylevel的代码。

#ifndef _MSC_VER
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif

//----------------------------------------------------------------------------
// Structure Definitions
//----------------------------------------------------------------------------

// The basic, OS defined exception frame

struct EXCEPTION_REGISTRATION
{
    EXCEPTION_REGISTRATION* prev;
    FARPROC                 handler;
};


// Data structure(s) pointed to by Visual C++ extended exception frame

struct scopetable_entry
{
    DWORD       previousTryLevel;
    FARPROC     lpfnFilter;
    FARPROC     lpfnHandler;
};

// The extended exception frame used by Visual C++

struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
    scopetable_entry *  scopetable;
    int                 trylevel;
    int                 _ebp;
};

//----------------------------------------------------------------------------
// Prototypes
//----------------------------------------------------------------------------

// __except_handler3 is a Visual C++ RTL function.  We want to refer to
// it in order to print it's address.  However, we need to prototype it since
// it doesn't appear in any header file.

extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *,
                                PCONTEXT, PEXCEPTION_RECORD);


//----------------------------------------------------------------------------
// Code
//----------------------------------------------------------------------------

//
// Display the information in one exception frame, along with its scopetable
//

void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec )
{
    printf( "Frame: %08X  Handler: %08X  Prev: %08X  Scopetable: %08X\n",
            pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
            pVCExcRec->scopetable );

    scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;

    for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ )
    {
        printf( "    scopetable[%u] PrevTryLevel: %08X  "
                "filter: %08X  __except: %08X\n", i,
                pScopeTableEntry->previousTryLevel,
                pScopeTableEntry->lpfnFilter,
                pScopeTableEntry->lpfnHandler );

        pScopeTableEntry++;
    }

    printf( "\n" );
}   

//
// Walk the linked list of frames, displaying each in turn
//

void WalkSEHFrames( void )
{
    VC_EXCEPTION_REGISTRATION * pVCExcRec;

    // Print out the location of the __except_handler3 function
    printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
    printf( "\n" );

    // Get a pointer to the head of the chain at FS:[0]
    __asm   mov eax, FS:[0]
    __asm   mov [pVCExcRec], EAX

    // Walk the linked list of frames.  0xFFFFFFFF indicates the end of list
    while (  0xFFFFFFFF != (unsigned)pVCExcRec )
    {
        ShowSEHFrame( pVCExcRec );
        pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
    }       
}

void Function1( void )
{
    int tl=0;
    __try
    {
        __asm mov eax, [ebp-4]
        __asm mov tl, eax
        printf("try leval = %d\n", tl);
    }
    __except(EXCEPTION_CONTINUE_SEARCH)
    {

    }

    // Set up 3 nested _try levels (thereby forcing 3 scopetable entries)
    __try
    {
        __asm mov eax, [ebp-4]
        __asm mov tl, eax
        printf("try leval = %d\n", tl);
        __try
        {
            __asm mov eax, [ebp-4]
            __asm mov tl, eax
            printf("try leval = %d\n", tl);
            __try
            {
    __asm mov eax, [ebp-4]
    __asm mov tl, eax
    printf("try leval = %d\n", tl);
                WalkSEHFrames();    // Now show all the exception frames
            }
            __except( EXCEPTION_CONTINUE_SEARCH )
            {
            }
        }
        __except( EXCEPTION_CONTINUE_SEARCH )
        {
        }
    }
    __except( EXCEPTION_CONTINUE_SEARCH )
    {
    }
}

int main(int argc, char* argv[])
{
    int i;
    int tl=0;
    __asm mov eax, [ebp-4]
    __asm mov tl, eax
    printf("try leval = %d\n", tl);
    __try
    {
        __asm mov eax, [ebp-4]
  __asm mov tl, eax
  printf("try leval = %d\n", tl);

        Function1();    // Call a function that sets up more exception frames
    }
    __except( EXCEPTION_EXECUTE_HANDLER )
    {
        i = 0x4321;     // Do nothing (in reverse)
    }

    __try
    {
  __asm mov eax, [ebp-4]
  __asm mov tl, eax
  printf("try leval = %d\n", tl);

        Function1();    // Call a function that sets up more exception frames   
    }
    __except( EXCEPTION_EXECUTE_HANDLER )
    {
        // Should never get here, since we aren't expecting an exception
        printf( "Caught Exception in main\n" );
    }
    return 0; 
}

这里我们可以看到如下情况,当然,这个是在2003下的,win7,会有不同的结果。最好还是先不用win7。win7的问题,我也不清楚。这个只能先放下了。

try leval = -1

try leval = 0
try leval = 0
try leval = 1
try leval = 2
try leval = 3
_except_handler3 is at address: 004014C0

Frame: 0012FEFC  Handler: 004014C0  Prev: 0012FF70  Scopetable: 004210B8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401203  __except: 00401206
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004012A4  __except: 004012A7
    scopetable[2] PrevTryLevel: 00000001  filter: 0040128E  __except: 00401291
    scopetable[3] PrevTryLevel: 00000002  filter: 00401278  __except: 0040127B

Frame: 0012FF70  Handler: 004014C0  Prev: 0012FFB0  Scopetable: 00420150
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 0040135F  __except: 00401365

Frame: 0012FFB0  Handler: 004014C0  Prev: 0012FFE0  Scopetable: 00420278
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401788  __except: 004017A3

Frame: 0012FFE0  Handler: 7C82B798  Prev: FFFFFFFF  Scopetable: 7C8123D8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 7C8571C8  __except: 7C8571DE

try leval = 1
try leval = 0
try leval = 1
try leval = 2
try leval = 3
_except_handler3 is at address: 004014C0

Frame: 0012FEFC  Handler: 004014C0  Prev: 0012FF70  Scopetable: 004210B8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401203  __except: 00401206
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004012A4  __except: 004012A7
    scopetable[2] PrevTryLevel: 00000001  filter: 0040128E  __except: 00401291
    scopetable[3] PrevTryLevel: 00000002  filter: 00401278  __except: 0040127B

Frame: 0012FF70  Handler: 004014C0  Prev: 0012FFB0  Scopetable: 00420150
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 0040135F  __except: 00401365
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004013A2  __except: 004013A8

Frame: 0012FFB0  Handler: 004014C0  Prev: 0012FFE0  Scopetable: 00420278
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401788  __except: 004017A3

Frame: 0012FFE0  Handler: 7C82B798  Prev: FFFFFFFF  Scopetable: 7C8123D8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 7C8571C8  __except: 7C8571DE

有了实践,这部分比较好懂了。明白了vc如何维护try block 之后,想要更清楚一点,只能从汇编的角度来看了。

EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable pointer
EBP-0C handler function address
EBP-10 previous EXCEPTION_REGISTRATION
EBP-14 LPEXCEPTION_POINTERS
EBP-18 Standard ESP in frame

这是try except block生成时的堆栈。[ebp –10],这里保存了vc 的EXCEPTION_REGISTRATION结构体,就和之前一样,对windows来说,他只是知道最基本的EXCEPTION_REGISTRATION,也就是只是关注prev 和handler,而其他的则是vc 编译器为了生成高效代码为我们加上去的。对windows当然是透明的。从一开始的例子也可以看出,我们只是使用最基本的EXCEPTION_REGISTRATION,依然能够执行SEH。

同样,EBP-14 GetExceptionPointers, EBP-18 Standard ESP in frame也是vc帮我们加入的。[EBP-14 ]这个就是函数当调用GetExceptionInformation会返回[EBP-14], 所以,这个函数其实是一个vc相关的函数。同样的还有GetExceptioncode这个地方还有一点不同的是,vc通过on the flay的方式处理这个数据,也就是说,当异常真的发生的时候,这个数据才会添入数据(这个真是一个废话,没有发生异常,那里来的异常信息?)EBP-18 Standard ESP in frame就不用说了,想要非局部跳转,光搞定ebp是不行的,没有esp的修正,并不能将控制流转到那里。

为了正确理解整个过程,我们需要理解except_handler 的代码,可惜,Matt Pietrek的有一些细节问题,可能会给我们这样的初学者疑惑,所以可以先看下http://bbs.pediy.com/showthread.php?t=53778,也是一位大牛的文章中,有vc6的except_handler code。当然,他多了一个ValidateEH3RN,这个和SEH的安全机制有关,我们目前先跳过去。__except_handler 的代码去了ValidateEH3RN,比较容易理解,当然,细扣细节的话,可能不同。在下一篇文章中,我们会着重关注这些细节。

知道了这么多后,我们在看看我们现在可以解决什么样的问题了。Jeffrey Richter 告诉了我们很多有关于展开的,并且告诉了我们很多可能导致额外负担的代码,那么下面我们就看看,为什么会有额外代码。

DWORD Funcenstein1()

{
    DWORD dwTemp;

    //1. Do any processing here.

        __try
    {
        //2. Request permission to access
        //    protected data, and then use it.
        WaitForSingleObject(g_hSem, INFINITE);
        g_dwProtectedData = 5;
        dwTemp = g_dwProtectedData;

        // Return the new value.
        return(dwTemp);


    }
    __finally
    {
        //3. Allow others to use protected data.
        ReleaseSemaphore(g_hSem, 1, NULL);
    }

    //4. Continue processing.
    return(dwTemp);
}


.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 push    0FFFFFFFFh
.text:00401005                 push    offset stru_4021F8
.text:0040100A                 push    offset __except_handler3
.text:0040100F                 mov     eax, large fs:0
.text:00401015                 push    eax
.text:00401016                 mov     large fs:0, esp
.text:0040101D                 sub     esp, 0Ch
.text:00401020                 push    ebx
.text:00401021                 push    esi
.text:00401022                 push    edi
.text:00401023                 mov     [ebp+var_4], 0
.text:0040102A                 push    0FFFFFFFFh      ; dwMilliseconds
.text:0040102C                 mov     eax, ?g_hSem@@3PAXA ; void * g_hSem
.text:00401031                 push    eax             ; hHandle
.text:00401032                 call    ds:__imp__WaitForSingleObject@8 ; WaitForSingleObject(x,x)
.text:00401038                 mov     esi, 5
.text:0040103D                 mov     ?g_dwProtectedData@@3KA, esi ; ulong g_dwProtectedData
.text:00401043                 mov     [ebp+dwTemp], esi
.text:00401046                 push    0FFFFFFFFh
.text:00401048                 lea     ecx, [ebp+var_10]
.text:0040104B                 push    ecx
.text:0040104C                 call    __local_unwind2 ;这里应该就是Jeffrey Richter 告诉我们的局部展开。
.text:00401051                 add     esp, 8
.text:00401054                 mov     eax, esi
.text:00401056                 mov     ecx, [ebp+var_10]
.text:00401059                 mov     large fs:0, ecx
.text:00401060                 pop     edi
.text:00401061                 pop     esi
.text:00401062                 pop     ebx
.text:00401063                 mov     esp, ebp
.text:00401065                 pop     ebp
.text:00401066                 retn

那local unwind到底做了什么呢?当然是将本EXCEPTION_REGISTRATION内嵌套的那些try block遍历,并展开了。这里贴出local unwind伪代码。这个和我们想象的一样。当然,我这里掩去了一个很重要很重要的部分,是有关于异常嵌套的问题。这个问题会在下一篇中在描述。

void _local_unwind2(EXCEPTION_REGISTRATION*pEh3Exce, int targetLevel)
{
    scopetable_entry *scopetable = peh3Exce->scopetable;
    int trylevel = peh3Exce->trylevel;

    while (trylevel != -1)
    {
        if (targetLevel == -1 || trylevel > targetLevel)
            break;

        if (scopetable[trylevel]->lpfnFilter == NULL)//__finally block
        {
            eax = scopetable[trylevel]->lpfnHandler;
            _NLG_Notify(101);
            eax = scopetable[trylevel]->lpfnHandler;
            __NLG_Call();// call eax
        }
        trylevel = scopetable[targetLevel].previousTryLevel;
        peh3Exce->trylevel = trylevel;
    }
    return;
}

那当我们把return 换成leave时,又是什么样子呢?leave我们并没有看到local unwind,我们需要明白return 和leave的区别。从return发生local unwind,我们可以看出多少端倪,local unwind 的作用在于遍历本地的except frame,那么return和leave的区别就在于,leave不会跳出多个try block 而 return 是有可能的。 所以return 必须要产生额外的负担去执行local unwind,leave则相当于,goto到try block 的结束并正常跳出try block。所以,如果我们只是想跳出本次try要注意不要直接return。

写给自己。

这一篇其实没有写完,虽然历时1个多月,最近实在是太忙了。这篇文章有2点遗憾。

1、最后应该写上global unwind,但是的确是不想去重复大牛们的内容了,global unwind 其实是系统RtlUnwind的封装,上边的链接中有讲这个的,也很详细。只是由于时间悠久和我们现在的编译器和操作系统距离很远了。如果对这些感兴趣,可以看看wince的代码,http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/EXDSPTCH.C.html,

http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/X86/MDX86.C.html。这个和我们的xp2比较像。

2、本来想尽可能的在这一篇中没有或是少有汇编,但是这个的确对我来说,是一个比较复杂的问题,而且越到最后,其实汇编也是不可避免的,因为真实的代码也很有可能就是汇编写的,我们实在是没有必要去把他翻译成c。

这篇文章里面的问题还是很多的,也很有可能会给第一次接触这些的同学一些误解,下一篇将更深入的理解SEH机制,将尽可能的减少这些误解(也包括自己理解错误),内容包括global unwind,异常嵌套和一些很基础很基础的SEH安全机制的总结。

未来一年的小计划

| Comments

虽然目标,任务给自己下了不知道多少次,但是总没有坚持下来过。这次破例,再给自己下一次。这也算是自己未来1年的计划吧。

第一年,我的事情主要有2个。第一个是参与创业,第二个是加强自己的基础知识。

  参与创业这个事情,对现在的我来说,说容易也容易,说难也难。创业到底是什么,其实我也不知道,对他来说,我不知道他的过去,也无法预知他的将来。也不知道我能为我们这个创业团队带来什么,但是好在我现在能知道他能给我带来什么。自由,这可以说是我加入这个团队最大的原因。在这里,我可以自己去支配自己的时间,能够有一个宽松的技术氛围,去做我感兴趣的事情。报酬么,现在还不是考虑他的时候,如何最大限度的提高自己的能力,则是最重要的。在这里可以我可以真实的体会一个项目的启动,完善以及最后的维护。虽然不可能像那些外企那样一上来就可以做哪些拥有大客户量的产品,但是我相信大道至简,没有最基本的体验,是不可能体会到真实的软件。

  对于我这种没有任何经验的开发者来说,我的确很难有真正的对产品的认识,在目前的创业团队中也并没有找到自己的位置,这个事情,我目前完全是踩着石头过河。虽然其中可能会走一些弯路,但是只要其中的每一步都是真实的,在我们这个多维空间下,那么也就没有弯路这回事了。

  如果说,这2件事情谁更重要,说实话,当然是第二个重要,当然也可以说,第二个和第一个并不矛盾。的确,但是这里分出来的主要原因,是这个在技术上的关联有些距离,创业的范围是移动互联,而这里的基础则是操作系统和编译原理。

  很难讲我是为什么去喜欢上了这2个非常枯燥的东东,我只知道未来的3年里,我将会用自己三分之一以上的精力专注在这2个部分,我相信,这2部分的能力将构成我整个知识体系金字塔的基石。由于平台的相关性和个人兴趣。我准备专注在windows 和vc这2块。希望我通过对windows 和vc底层实现的理解,加深自己对程序的认识。当然,想要真正的理解这2部分,没有10年20年是不可能有结果的。在第一年里,我将专注在OS user mode下,初步了解windows运行机制以及相对应的vc在os 和 code之前做桥梁的部分,这部分将包括编程语言(主要是c\c++)实现的底层部分,特别是在windows平台下,vc和os之间相辅相成的部分,包括windows是如何保证代码安全,漏洞,以及vc代码效率等方面。我希望通过对这些的了解,加深自己对计算机的认知。

  最后,再一次很无耻的给自己一个硬性的目标。

每天用半个小时到一个小时的时间听英文的webcast,pdc,或是其他的资料。听不懂也要坚持。至少要达到听1个小时英语不恶心的程度。 每天一道算法题,之前只是坚持了2个月。这次一定要坚持下来。每天不多,也不少。 每2个月研究一篇有价值,感兴趣的大牛写的技术文章以及相应的周边知识,并写1-2篇博客记录这个学习过程。   先这么多把,其实想想。这个对我来说已经是很满很满的计划了。