白话windows之四 异常处理机制(VEH、SEH、TopLevelEH...)

原文地址:http://bbs.pediy.com/showthread.php?t=173853

今天是我侄子的4岁生日,特此更新一篇,祝我小侄子在幼儿园多泡MM

我们都知道SEH异常处理机制,那VEH、TopLevelEH呢?他们执行的先后顺序是怎样的呢?当这些机制都不使用的情况下,会发生什么情况呢?异常处理器是怎么工作的?如果你对此感兴趣,那我们就一起来扒开异常处理机制的面纱吧

术语:
SEH: 结构化异常处理
VEH: 向量化异常处理
TopLevelEH:顶层异常处理

EXCEPTION_EXECUTE_HANDLER :该异常被处理。从异常处下一条指令继续执行
EXCEPTION_CONTINUE_SEARCH:不能处理该异常,让别人处理它吧
EXCEPTION_CONTINUE_EXECUTION:该异常被忽略。从异常处处继续执行

//调试器返回值:
DBG_CONTINUE : 等同于EXCEPTION_CONTINUE_EXECUTION
DBG_EXCEPTION_NOT_HANDLED :等同于EXCEPTION_CONTINUE_SEARCH


想想对我这等语文是体育老师教出来的童鞋来说,想把这个主题将透彻,还真是有点难度的~,我们还是按异常处理器执行顺序来吧,废话不多说,开始。。。

异常处理器其实包含 内核异常处理R3异常处理内核异常处理比较简单,我也对它没兴趣,所以这里就把它给忽略了。我们只讲R3程序产生异常时,异常处理器是怎么工作的。

异常处理器处理顺序流程
1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe


大致分上面几步把,下面咱就详细讨论一下各个步骤都干了哪些细活

1. 第一次交给调试器
如果该出现异常的程序正在被调试,则该异常首先交给调试器处理(通过DebugPort)。
调试器拿到这个异常后,需要判断是否要处理该异常,如果处理该异常返回DBG_CONTINUE,否则返回DBG_EXCEPTION_NOT_HANDLED

代码:
    while(!bExit) 
    {
        DWORD dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
        DEBUG_EVENT debugEvent;
        WaitForDebugEvent(&debugEvent, INFINITE);
        switch ( debugEvent.dwDebugEventCode )
        {
        case EXCEPTION_DEBUG_EVENT:
            {
                EXCEPTION_DEBUG_INFO* pExcpInfo = &debugEvent.u.Exception;
                if ( MessageBox(0,_T("处理该异常?"), _T("我是调试器"),MB_YESNO)==IDYES )
                {
                    dwContinueStatus = DBG_CONTINUE;
                    //...
                }
            }
            break;
            //...
        }
        ContinueDebugEvent(debugEvent.dwProcessId,  debugEvent.dwThreadId, dwContinueStatus); 
    }

2. 执行VEH
这里就不讲Veh的概念了,有兴趣的去Google一下。
如果没有被调试,或者调试器返回DBG_EXCEPTION_NOT_HANDLED,则就会检查是否存在VEH。如果存在VEH,则把异常交给他们处理。
VEH是个链表,可以存在多个Veh。每个VEH按顺序被调用
一个VEH可以返回连个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_CONTINUE_EXECUTION。返回EXCEPTION_EXECUTE_HANDLER无效的,等同于EXCEPTION_CONTINUE_SEARCH。
当一个Veh返回EXCEPTION_CONTINUE_SEARCH,则把异常交给下一个VEH处理。
如果返回EXCEPTION_CONTINUE_EXECUTION,认为已经被处理,退出异常处理器在异常指令处继续执行
从执行顺序来看,VEH是在SEH之前执行的,并且不依赖某一线程,本进程中任何线程出现异常都可以被VEH处理,所以在有些时候是很有用处的。
怎么添加一个VEH呢?

代码:
LONG NTAPI FirstVectExcepHandler( PEXCEPTION_POINTERS pExcepInfo )
{
    if( ... )
    {
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}
//参数1=1表示插入Veh链的头部,=0表示插入到VEH链的尾部
AddVectoredExceptionHandler( 1, &FirstVectExcepHandler );

3. 执行SEH
SEH应该是大家都比较熟悉的了。当所有的VEH都不处理该异常,该异常就会让SEH处理。
SEH是基于线程栈的异常处理机制,所以它只能处理自己线程的异常。
先看一个示例代码:

代码:
LONG FirstSEHer( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("第一个SEH处理器");
    _tprintf( _T("[EH.Exe] [SEH][1] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [SEH][1] out \n") );
    return nRet;
}
LONG SecondSEHer( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("第二个SEH处理器");
    _tprintf( _T("[EH.Exe] [SEH][2] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [SEH][2] out \n") );;
    return nRet;
}
void ExcepFunction()
{
  __try
  {
    __try
    {
      __try
      {

        _tprintf( _T("[EH.Exe] *[CALL] int 3\n") );
        __asm int 3;
      }
      __finally
      {
        printf( "[EH.Exe] *[SEH][0] finally call...\n" );
      }
    }
    __except( FirstSEHer(GetExceptionInformation()) )
    {
      _tprintf( _T("[EH.Exe] [SEH][1] 被俺处理了~(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
    }
  }
  __except( SecondSEHer(GetExceptionInformation()) )
  {
    _tprintf( _T("[EH.Exe] [SEH][2] 被俺处理了(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
  }
}

ExcepFunction函数有三个SEH,但是有两个Headler。当__asm int 3;被执行时就会被SEH捕获。捕获后,首先交给FirstSEHer处理,如果FirstSEHer返回EXCEPTION_CONTINUE_SEARCH则才会交给SecondSEHer处理。
FirstSEHer可以返回三个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTION
当返回EXCEPTION_CONTINUE_SEARCH,执行上一层SEH,这里执行SecondSEHer
返回EXCEPTION_EXECUTE_HANDLER时则表示异常被处理,会先把内部的__finally块执行完,再跳到自身的__except块中执行。
返回EXCEPTION_CONTINUE_EXECUTION时表示该异常被忽略,会再次执行__asm int 3处指令。如果该条汇编不被修正成其他指令(如nop),则会再次产生一个异常。

另外,如果想在 try catch的C++异常中捕获系统异常,必须让C++支持SEH异常处理。设置方法: Vc-〉项目属性-->配置属性-->c/C++-->代码生成-->启用C++异常,选中"是,但有SEH异常(/EHa)"。

4. TopLevelEH
顶层异常处理,这个其实是利用SEH实现的。在最顶层的SEH中,可以注册一个顶层异常处理器。虽然他是基于SEH实现的,但是它可以处理所有线程抛出的异常。
当SEH都处理不了该异常,在最顶层的SEH中就会检查是否注册了顶层异常处理,如果注册了,则执行顶层异常处理。
注意:如果该进程正在调试状态,顶层异常处理会被忽略,不会被执行
顶层异常处理函数也可以返回三个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTION
返回EXCEPTION_CONTINUE_EXECUTION时,和SEH一样。
返回EXCEPTION_EXECUTE_HANDLER时,则直接杀死该进程
返回EXCEPTION_CONTINUE_SEARCH时,会查注册表,检查是否存在实时调试器。注册表路径:KLM\software\microsoft\windows nt\currentvsrsion\aedebug。如果Auto==1,Debugger!=NULL则根据Debugger中指示的参数启动实时调试器,让调试器处理该异常(如果不存在顶层异常且进程没被调试,也会检查并启动实时调试器)
注册方法:

代码:
LONG NTAPI TopLevelExcepFilter( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("*顶级* 异常处理器");
    _tprintf( _T("[EH.Exe] [TOP] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [TOP] out \n") );;
    return nRet;
}
//注册
SetUnhandledExceptionFilter( &TopLevelExcepFilter );

顶层异常处理通常用来生成程序Dump文件。供开发人员分析。

5. 再次交给调试器
如果上述的异常处理机制都没有处理该异常,则调试器会再次接收该异常。
调试器这个时候返回DBG_CONTINUE,则和第一次相同。
返回DBG_EXCEPTION_NOT_HANDLED,则直接杀死该进程

6. 调用异常端口通知csrss.exe
当上面提到的都没有处理该异常,则调用ExceptionPort通知csrss.exe。csrss.exe的做法是会弹出一个对话框:
名称:  sd.jpg
查看次数: 0
文件大小:  33.7 KB
这个时候还有一次修复异常并让程序继续运行的机会,就是点击“调试”按钮。其他按钮都很导致异常进程被终止。

终于写完了,为了完成该文,特意写了个小软件。里面包含一个异常产生程序(ExcepHandler)和一个简单调试器(MyDbg)源码在此:
TestException.rar.
(调试器参考了超然兄的代码,特此感谢)

posted @ 2014-08-26 09:35  灬后知后觉  阅读(4812)  评论(0编辑  收藏  举报