SEH结构化异常处理源码赏析(C篇)
                      关键字: C SEH 结构化异常处理 _try _finally _except _except_handler3 VC
                      工程使用工具: VC7.1.3088  IDA pro 4.9.0.863  cdb Windbg Editplus

1.起因
    C++程序员对try,catch,throw都应该很熟悉,能知道VC怎么实现它的人就不多了,不过网络世界使很多人知道了它与SEH(structured exception      handling)有密切关系,我也不例外,也是在若干年前从网络知道了SEH,并且大致也知道SEH的流程.但是和多数人一样在我的实践也很少直接使用SEH,对SEH也就仅限于网络上一些文章的介绍.曾经在用Windbg对某些软件作分析,我遇到了断点失效的情况,查找资料介绍是SEH中的Handler清除了调试寄存器,在分析SEH代码中由于VC没有SEH的源码,于是我产生了一种想法,详细完整地翻译VC的SEH的代码,一劳永逸的解决问题.C和C++的SEH有所不同,C++的要复杂些,我在此介绍的仅为C的SEH代码,也就是__try,__finally,__except,__leave所产生的SEH代码,C++篇有时间的话我再作.我以前看过的资料大都以比较专业的语言介绍,在此我仅以我自己感觉比较通俗的语言介绍,希望能有更多的人能认识,认清SEH.
2.SEH术语:SEH中术语虽然不多,但由于没有SDK的明确定义有时很难区别,各家说的表述也不太统一,因此为此文特定义如下术语,这些术语可能与其它文献有点冲突或细微差别,有的也是我自己的定义:
  A.SEH(structured exception handling): 在C语言中关键字是__try(_try),__finally(_finally),_except(__except),而C++使用的关键字是try,catch.在以下的表述中所有的try均不再特别声明为C关键字,一律默认为C关键字.
  B. EH3_List:_EH3_EXCEPTION_REGISTRATION链表,表头位于FS:[0],0xFFFFFFFF为链表结束标志.编译器在编译一个函数时只要检测到含有_try或__try则为此函数生成一个_EH3_EXCEPTION_REGISTRATION节点,并插入到表头.因为每个函数编译只生成一个节点,因此在一个函数中C和C++的SEH不能同时存在,如果代码中同时有catch和except则不能通过编译就是此原因.
  C. EH3_ScopeTable:是一个由编译器在data section生成的一张表(数组),实质可看作是二叉树结构(可能有多个二叉树顺序存放),节点为_SCOPETABLE_ENTRY类型,其中_SCOPETABLE_ENTRY.ParentLevel是父节点在数组中的位置,EH3_ScopeTable[0]是根节点,_SCOPETABLE_ENTRY.ParentLevel=0xFFFFFFFF.由此可见ParentLevel很重要,是SEH判断try嵌套层次的唯一依据.编译器从函数入口点开始遍历try,每遇到一个try生成一个节点_SCOPETABLE_ENTRY,并放在表最后,注意节点的先后与try的嵌套层次无关.
  D. filter handler:是异常发生后让用户决定是否认识此异常,通过修改异常语句的上下文环境(CONTEXT)可使应用程序能继续正常运行.其返回值有三.
  EXCEPTION_EXECUTE_HANDLER(1): 去执行exception handler,然后进程终止,且不显示出错提示框.
    EXCEPTION_CONTINUE_SEARCH(0): 不执行exception handler,显示出错提示框,进程终止或者进入调试器进行调试.
    EXCEPTION_CONTINUE_EXECUTION(-1): 不执行exception handler,系统用CONTEXT重新设置CPU环境,进程继续执行,如果修改了EIP则从新的EIP开始执行,否则从原异常点开始执行.

    E. exception handler:是异常发生后检测到该异常无法被处理,而进程终止前提醒应用程序执行的收尾工作
  F. termination handler:如果try语句被过早终止,不管是正常离开或者是非正常离开,包括goto,leave及异常时均执行此handler,具体执行过程可查MSDN.
  G. 展开(unwind):这个名词很让人费解,翻译也确实不好命名,我也沿用此名.展开的目的是执行finally对应的termination handler,对照在下面的源代码中我们就能很容易理解MSDN关于finally的解释了.展开分为本地局部展开(_local_unwind)和全局展开(_global_unwind);本地展开:展开此try所在函数try嵌套关系并分别执行其finally对应的termination handler;全局展开:当exception handler不在本try函数时,执行exception handler前需要先执行这之前的termination handler,全局展开就是查找这些termination handler并执行它.需要说明的是全局展开不含本地展开.
3.SEH数据结构
  typedef struct  // (sizeof=0xC)
  {
    DWORD ParentLevel ;     // 当前Handler父层TRY在EH3_ScopeTable中的位置,根没有上一层,故值=-1
                 // 形成TRY层次的二叉树结构,与_EH3_EXCEPTION_REGISTRATION.TryLevel物理意义一样
    DWORD FilterFunc;      // 非NULL则HandlerFunc是exception handler,否则termination handler
    DWORD HandlerFunc;     // exception handler or termination handler
  } _SCOPETABLE_ENTRY;
  typedef struct  // (sizeof=0x10)
  {
    _EH3_EXCEPTION_REGISTRATION* pPrev;// 栈上一级EH3_List节点,=0xFFFFFFFF则为最后一个节点
    EXCE_HANDLER ExceptionHandler;     // VC7.1中统一指向_except_handler3
    _SCOPETABLE_ENTRY* pScopeTable;     // 指向一个_SCOPETABLE_ENTRY数组,函数有n个TRY,则数组有n个元素
                       // p[0]->Try0为根,p[1]->Try1,p[2]->Try2...
    DWORD TryLevel;     // 指示当前指令在Try的层次级别,-1未进入TRY,进第一个TRY为0,第二个为1,...
              // 但它与嵌套层次无关,在编译时确定,从函数代码开始处开始计数
  } _EH3_EXCEPTION_REGISTRATION;
  typedef struct  // (sizeof=0x10)还有待进一步分析其用处
  {
    DWORD unKnown;    // 未知:被编译器赋值
    DWORD HandlerFunc;  // _SCOPETABLE_ENTRY.HandlerFunc
    DWORD firstPara;    // Try所在函数第一个参数:crtMain!EBP+8
    DWORD TryEBP;    // Try所在函数EBP
  }_NLGDestination;
  // 以下在MSDN中均有定义,不再作解释
  typedef struct _EXCEPTION_RECORD
  {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
  } EXCEPTION_RECORD,*PEXCEPTION_RECORD;
  typedef struct _CONTEXT
  {
    ...         // 依据CPU类型有不同定义,具体可见winnt.h
  } CONTEXT,*PCONTEXT;
  typedef struct _EXCEPTION_POINTERS 
  {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
  } EXCEPTION_POINTERS,*PEXCEPTION_POINTERS;
4.SEH汇编要点
  A. 函数中try的数据模型:VC将会为有try函数在栈上首先建立三个变量:
    EBP-18h:    SaveESP          // TRY前保存当时ESP,因此有:SaveESP<&Eh3Exception
    EBP-14h:    pExceInfo        // GetExceptionPointers(),在handler中调用filter前赋值
    EBP-10h:    Eh3Exception      // 一个_EH3_EXCEPTION_REGISTRATION,其大小刚好为10h哦
    EBP+00h:    ebp            // 上一frame的EBP
    EBP+04h:    EIP            // CALL返回地址
    在这里我们应该注意到这个公式是成立的:
          EBP=&Eh3Exception+10h
    而在_except_handler3中参数: pEh3Exce刚好为&Eh3Exception,所以每当在_except_handler3中要回调filter,exception,termination handler时,在这之前汇编中均有一句:
          lea     ebp, [ebx+10h]    // ebx=_except_handler3!arg_pEh3Exception
    明白了这点就不难明白如何在_except_handler3中访问pExceInfo及SaveESP了!
  B. try块的终止:异常发生,goto,return,leave,正常终止.goto和return可能跨过其它try,所以必须要展开到目的地所在的TryLevel,但是leave关键字不会跨过其它try,只是跳出自己这层try,如果编译器检测到这层try有termination handler,则用CALL xxx直接调用.当然也有例外,这也是VC聪明的地方,如果函数只有一个try,则根本不用展开而直接使用CALL了,这种情况有时可见.这样就不难理解如下代码:
    goto 004013fb 终止代码:
      // ---------------goto跳出try----------------------------------------
          goto try0;
      push    0              或者  (仅一个try时)    CALL 00401070  // 直接调termination handler
      lea     eax, [ebp+var_Eh3Exce]                jmp loc_4013FB
      push    eax
      call    __local_unwind2          // 展开到0(即第一个try内,goto目的地肯定在第一个try内)
      add     esp, 8
      jmp     loc_4013FB

    return       终止代码:(无返回值)
      // ---------------return跳出try--------------------------------------
      push        0FFFFFFFFh 
      lea         eax,[ebp-10h] 
      push        eax  
      call        __local_unwind2 (401786h)  // 展开到-1(即所有try外)
      add         esp,8 
          return ;
      jmp         $L19800 (401139h) 

    return var_i 终止代码:(有返回值)
      // ---------------return(带返回值)跳出try----------------------------
      mov         eax,dword ptr [i] 
      mov         dword ptr [ebp-100h],eax  // 返回值先被保存
      push        0FFFFFFFFh 
      lea         ecx,[ebp-10h] 
      push        ecx  
      call        __local_unwind2 (4017D6h)  // 再展开,即使finally中改变了i,也不会改变返回值!!!
      add         esp,8 
          return i;
      mov         eax,dword ptr [ebp-100h]  // 取保存的返回值来保存
      jmp         $L19800 (40117Bh)      // 跳到复原先前SEH链处

    leave和正常退出的代码:
      // ---------------leave跳出try---------------------------------------
          if (x > 18)
      004010FD  cmp         dword ptr [x],12h 
      00401101  jle         FunC+55h (401105h) 
            __leave;
      00401103  jmp         FunC+66h (401116h) // 直接调用termination handler
          printf("%s Try!\n",fun);
      00401105  mov         eax,dword ptr [fun] 
      00401108  push        eax  
      00401109  push        offset string "%s Try!\n" (410130h) 
      0040110E  call        printf (401650h) 
      00401113  add         esp,8 
      00401116  mov         dword ptr [ebp-4],0FFFFFFFFh // 退出try
      // ---------------正常退出try----------------------------------------
      0040111D  call        $L19798 (401124h) // 直接调用termination handler
      00401122  jmp         $L19801 (40113Fh) 

  C. _except_handler3执行两次原因,实际上理解了展开就能理解它,举例解释如下:
    EH3_List如:FS[0]->E1->E2->E3->E4,在E4中发生异常,依次搜索E1,E2,E3,最后在E4中找到能处理此异常的filter handler,这样E4,E3,E2,E1的_except_handler3均执行了一次,这是第一次.
    第二次:在执行E4的exception handler前要先调用全局展开(_global_unwind2)以运行E1,E2,E3的termination handler在全局展开(_global_unwind2)中将为EXCEPTION_RECORD.ExceptionFlags增加标志_EH_UNWINDING(2),再依次调用它们的_except_handler3,这就是E1,E2,E3的_except_handler3的第二次调用,但E4只有一次.
5. VC7.1下的SEH的C代码:
  // 源码基本以汇编为蓝本,没有进行优化,主要是为了方便大家与汇编对照阅读
  #define MAX_PAGES 0x10
  int ValidateEH3RN(_EH3_EXCEPTION_REGISTRATION* pEh3Exce)
  {
    // 1.验证_EH3_EXCEPTION_REGISTRATION.pScopeTable的合法性:4字节对齐,且不在栈空间内,因为它是编译器生成的全局变量
    _SCOPETABLE_ENTRY* pScopeTable=pEh3Exce->pScopeTable;
    if (((DWORD)pScopeTable & 0x3) == 0)
      return 0;
    NT_TIB *pTeb = (NT_TIB*)FS[0x18];
    DWORD nStackLimit = pTeb->StackLimit;
    if (pScopeTable >= pTeb->StackLimit && pScopeTable < pTeb->StackBase)
      return 0;// pScopeTable在栈内肯定不合法
    // 2.判断二叉树pScopeTable是否合法,并统计exception handler数量
    if (pEh3Exce->TryLevel == -1)
      return 1;// 表示语句不在try中
    DWORD nFilters,count;
    nFilters=count=0;
    for(;nFilters <= pEh3Exce->TryLevel; nFilters++)
    {// LOOP1:验证已进入的TRY的SCOPETABLE_ENTRY是否都合法
      if(pScopeTable[nFilters].ParentLevel!=-1 && pScopeTable[nFilters].ParentLevel>=nFilters)
        return 0;// EnclosingLevel在二叉树中不合法:不是根结点且ParentLevel>=nFilters
             // ParentLevel>=nFilters不合法原因:二叉树存贮不可能儿子先存
      if (pScopeTable[nFilters].FilterFunc == NULL)
        continue;// termination handler不统计
      count++;
    }
    if (count != 0)
    {// 有异常处理,验证saveESP
      PVOID saveESP =0;// 运行时真实值 = crtMain!saveESP=crtMain!(ebp-18h);
      if (saveESP < nStackLimit || saveESP >= pEh3Exce)
        return 0;// 不在栈内,或不在TRY函数内,saveESP为TRY函数的栈顶
    }

    // 3.找g_rgValidPages中是否已经有pScopeTable的页基址
    static int g_nValidPages=0;// g_rgValidPages数组有效元素个数
    static int g_rgValidPages[MAX_PAGES]={0}; // MaxPages=0x10
    DWORD nPageBase = (DWORD)pScopeTable & 0xfffff000;// 取pScopeTable所在页基址
    int nFind=0;
    if(g_nValidPages > 0)
    {
      do
      {// LOOP2
        if(nPageBase==g_rgValidPages[nFind])
          goto Finded;// 找到
        nFind++;
      }
      while(nFind<g_nValidPages);
    }

    // 4.没有过在g_rgValidPages找到nPageBase则判断pScopeTable是否在合法的EXE映像内
    MEMORY_BASIC_INFORMATION memInfo;
    if (VirtualQuery(pScopeTable,&memInfo,sizeof(memInfo)) == 0)
      return -1;
    if (memInfo.Type != 0x01000000)// MEM_IMAGE
      return -1;
    DWORD protect = 0xCC;// PAGE_READWRITE|PAGE_WRITECOPY|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY;
    if (memInfo.Protect & protect)
    {// 有指定属性
      IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)memInfo.AllocationBase;
      if (pDosHeader->e_magic != 'ZM')
        return -1;  // 非法DOS头,pScopeTable不为编译器分配的空间
      IMAGE_NT_HEADERS* pPeHeader = (IMAGE_NT_HEADERS*)((char*)pDosHeader+pDosHeader->e_lfanew);
      if (pPeHeader->Signature != 'ZM')
        return -1;  // 非法PE头,pScopeTable不为编译器分配的空间
      IMAGE_OPTIONAL_HEADER32* pOptHeader = &pPeHeader->OptionalHeader;
      if (pOptHeader->Magic != 0x10b)
        return -1; // 非WIN32 EXE
      DWORD rvaScope = (DWORD)pScopeTable-(DWORD)pDosHeader; // 计算pScopeTable的RAV
      if (pPeHeader->FileHeader.NumberOfSections <=0 )
        return -1;
      IMAGE_SECTION_HEADER* pSection = (IMAGE_SECTION_HEADER*)(pPeHeader+1);
      if (rvaScope >= pSection->VirtualAddress && rvaScope < pSection->VirtualAddress+pSection->Misc.VirtualSize)
      {// rvaScope在代码节内
        if (pSection->Characteristics & 0x80000000)// 0x80000000=IMG_SCN_MEM_WRITE
          return 0;
      }
    }

    // 5.对新验证的nPageBse插入到数组中
    static int g_nLock=0; // 1:上锁,0:解锁
    if(InterlockedExchange(&g_nLock,1)!=0)// 写线程锁
      return 1;// 其它线程已经进入,则不能再进入
    int k=g_nValidPages;
    if(k > 0)
    {
      do
      {// LOOP4:判断其它线程是否写入这个页基址
        if(nPageBase==g_rgValidPages[k-1])
          break;// 找到,k>0
        k--;
      }
      while(k>0);
    }
    if (k==0)
    {// 没有找到,nPageBase插入到g_rgValidPages头
      int pages = 0x0F;
      if(g_nValidPages <= pages)
        pages = g_nValidPages;
      k=0;
      if(pages >= 0)
      {
        do
        {// LOOP5
          int temp=g_rgValidPages[k];
          g_rgValidPages[k]=nPageBase;
          nPageBase=temp;
          k++;
        }
        while(k<=pages);
      }
      if (g_nValidPages<0x10h)
        g_nValidPages++;
    }
    InterlockedExchange(&g_nLock,0);// 解锁
    return 1;

    // 6.找到的nPageBase移到头
    //   但前面找到的结果也可能被其它线程改变位置甚至推出数组,但无论如何这个nPageBase合法可不再验证
  Finded:
    if (nFind <= 0)// 相当于if(nFind == 0)
      return 1;// nPageBase已经在头,可直接返回
    if(InterlockedExchange(&g_nLock,1)!=0)// 写线程锁
      return 1;// 其它线程已经进入,则不能再进入
    if(g_rgValidPages[nFind] != nPageBase)
    {// 再次对找到的pos进行比较,因为其它线程可能又修改了这个元素的值
      nFind = g_nValidPages-1;
      if (nFind>=0)
      {
        while(nFind>=0)
        {// LOOP3
          if (g_rgValidPages[nFind]==nPageBase)
            break;
          nFind--;
        }
        if (nFind>=0)
          goto End1;
      }
      // 没找到,新增加
      if(g_nValidPages < 0x10)
        g_nValidPages++;
      nFind = g_nValidPages-1;
      goto end2;
    }
    else
      goto end2;
  end1:
    if (nFind != 0)
    {
  end2:
      if (nFind >= 0)
      {
        for(int j = 0;j<=nFind;j++)
        {// LOOP6:g_rgValidPages中找到的nPageBase移到头或新nPageBase插入头,其余每个元素向后推
          int temp=g_rgValidPages[j];
          g_rgValidPages[j]=nPageBase;
          nPageBase=temp;
        }
      }
    }
    InterlockedExchange(&g_nLock,0);// 解线程锁
    return 1;
  }
  void _global_unwind2(_EH3_EXCEPTION_REGISTRATION*pEh3Exce)
  {// 对调用RtlUnwind的封装
    RtlUnwind(pEh3Exce,offset exit,0,0);
  exit:
    return;
  }
  int _UnwindHandler(EXCEPTION_RECORD*pExceRec,_EH3_EXCEPTION_REGISTRATION*pEh3Exce,
             void*,_EH3_EXCEPTION_REGISTRATION** ppEh3Exce)
  {
    if(pExceRec->ExceptionFlags && 6 == 0)
      return 1;// ExceptionContinueSearch
    *ppEh3Exce = pEh3Exce;
    return 3;// ExceptionCollidedUnwind
  }
  // NLG == "non-local-goto" 
  _NLGDestination g_NLGDestination;// 此全局变量我至始至终未见到任何其它EXE处或者DLL使用,通知是何意义暂无知!!!!!!!
  void _NLG_Notify1(int x)// --------------------------CallSettingFrame调用
  {// g_NLGDestination全局分配变量:0x19930520
    g_NLGDestination.dwInCode = EBP+8;// Try函数中第一个参数???
    g_NLGDestination.HandlerFunc = EAX;// 调用前通过EAX传入
    g_NLGDestination.TryEBP = EBP;// Try中的EBP???
  }
  void _NLG_Notify(int x)// --------------------------_except_handler3,_local_unwind2调用
  {// 传入参数未用
    // g_NLGDestination.unKnown全局分配变量:0x19930520
    g_NLGDestination.dwInCode = EBP+8;// Try函数中第一个参数,如:FunB(i,j)!i,crtMain!ebp+8
    g_NLGDestination.HandlerFunc = EAX;// 调用前通过EAX传入:pScopeTable.HandlerFunc
    g_NLGDestination.TryEBP = EBP;// Try中的EBP
  }
  // nToLevel:展开到此为止(不含nToLevel),举例:在goto中nToLevel由目标所在try决定,因为系统规定跳入TRY是不合法的,因此
  // goto不能跳入其它TRY(嵌套的上层TRY是合法的,很明显平级层是不合法的)层,这时编译是通不过的.
  void _local_unwind2(_EH3_EXCEPTION_REGISTRATION*pEh3Exce,int nToLevel)
  {// 用ESP,未用EBP,之前的EBP传入内调的_NLG_Notify,__try中的goto将用此函数展开得到其handler
    _EH3_EXCEPTION_REGISTRATION  eh3Unwind;
    eh3Unwind.TryLevel = (int)pEh3Exce;
    eh3Unwind.pScopeTable = (_SCOPETABLE_ENTRY*)-2;
    eh3Unwind.ExceptionHandler = (EXCE_HANDLER)_UnwindHandler;
    eh3Unwind.pPrev = (_EH3_EXCEPTION_REGISTRATION*)FS[0];
    FS[0]=ESP;
    int nLevel;// 
    while(1)
    {
      _SCOPETABLE_ENTRY *pScope=pEh3Exce->pScopeTable;
      ESI = pEh3Exce->TryLevel;
      if (ESI == -1)
        break;
      if (ESI == nToLevel)
        break;
      nLevel=pScope[ESI].ParentLevel;
      pEh3Exce->TryLevel=nLevel;  
      if (pScope[ESI].FilterFunc != NULL) // 有FilterFunc则是exception handler
        continue;
      EAX=pScope[ESI].HandlerFunc;    // 无FilterFunc则是termination handler
      _NLG_Notify(0x101);      // 无返回值
      CALL(EAX);          // 这就是展开所求得的Handler,可以是termination handler,exception Handler
    }
    FS[0]=(DWORD)eh3Unwind.pPrev;
  }

  // _except_handler3执行两次原因:
  // 举例:EH3_List如 FS[0]->E1->E2->E3->E4,在E4中发生异常,依次搜索E1,E2,E3,最后在E4中找到能处理此异常的filter handler,这样E4,E3,E2,E1的_except_handler3均执行了一次,这是第一次.
  //     第二次:在执行E4的exception handler前要先调用全局展开(_global_unwind2)以运行E1,E2,E3的termination handler
  // 在全局展开(_global_unwind2)中将为EXCEPTION_RECORD.ExceptionFlags增加标志_EH_UNWINDING(2),再依次调用它们的
  // _except_handler3,这就是E1,E2,E3的_except_handler3的第二次调用,但E4只有一次.
  int _except_handler3(EXCEPTION_RECORD*pExceRec,_EH3_EXCEPTION_REGISTRATION*pEh3Exce,
             CONTEXT* pContextRecord/*,void * DispatcherContext*/)
  {
    if ((pExceRec->ExceptionFlags & 6) == 0)
    {// 无_EH_UNWINDING and _EH_EXIT_UNWIND
      EXCEPTION_POINTERS ePointer;
      ePointer.ExceptionRecord = pExceRec;
      ePointer.ContextRecord = pContextRecord;
      // pEh3Exce=&EhRecord
      // lea     EAX, [EBP+var_ExcePointers]
      // mov     [ebx-4], EAX ;EBX=pEh3Exce
      *((DWORD*)pEh3Exce-1)=(DWORD)&ePointer;  // 给_XcptFilter的参数pExceptPointers赋值
      if (ValidateEH3RN(pEh3Exce))
      {
        _SCOPETABLE_ENTRY* pScopeTable=pEh3Exce->pScopeTable;
        int i = pEh3Exce->TryLevel;
        while(1)
        {
          if (i==-1)
            return 1;// handler拒绝处理这个异常
          EAX=pScopeTable[i].FilterFunc;
          if(EAX!=NULL)
          {// 属于exception handler
            EBP=(DWORD)pEh3Exce+0x10;// 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
            CALL(EAX);// 没有参数,ePointer已传过去
            if(EAX != 0)
            {
              if ((int)EAX < 0)
                return 0;        // 如果在filter中返回值<0,此返回后会引起代码为
                            // EXCEPTION_NONCONTINUABLE_EXCEPTION(0xC0000025)的异常,反复如此栈溢出
              _global_unwind2(pEh3Exce);  // 调用handler前,展开前级函数中的termination handler
              EBP=(DWORD)pEh3Exce+0x10;  // 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
              _local_unwind2(pEh3Exce,i);  // 此函数内不直接用EBP,但暗传EBP给可能执行的_NLG_Notify
                            // 并展开本函数中的termination handler
              EAX=pScopeTable[i].HandlerFunc;
              _NLG_Notify(1);// 此函数内要用EAX
              pEh3Exce->TryLevel=pScopeTable[i].ParentLevel;// 修改节点所在层次使EIP在TRY中位置表示正确
              EAX = pScopeTable[i].HandlerFunc;
              CALL(EAX);// 进入异常处理,不再返回此函数(内部可接受异常并修改环境,继续执行)
            }
          }
          i=pScopeTable[i].ParentLevel;
        }
        return 0;
      }
      else
      {
        pExceRec->ExceptionFlags |= 8;// 置pEh3Exce无效标志,它不属于此handler
        return 1;// handler拒绝处理这个异常,系统转调上一级
      }
    }
    else
    {// 有_EH_UNWINDING or _EH_EXIT_UNWIND
      EBP=(DWORD)pEh3Exce+0x10;// 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
      _local_unwind2(pEh3Exce,-1);
    }
    return 1;
  }
6. 结语
    SEH代码还有很多,它们有许多包含在NTDLL.DLL中,比如RtlUnwind我曾经非常想翻译它,但至今我也还未完成,因此我也就没有粘出来.实际上从我以上粘出的初步代码,我们应该能看出SEH并不复杂,翻译DLL中的相关代码也并不困难,我估计我们唯一无法看见的代码就是中断处理的起始部分,把它想像成一个黑匣子就成了.附件有我的IDA注释说明,注释由于是一个动态过程,特别是起初的注释可能有错,我没有精力再去一一阅读更正,希望大家理解,一并奉献上,希望对大伙有用.
      在这里,我只是作了SEH源码的一部分翻译工作,在翻译过程中我也在网上查看了很多关于SEH方面的文章,在此我不再一一列出,一并致谢这些无私奉献的网友和同行们!看了此文相信再看其它SEH专著就应该容易多了!
      今天就是我们的嫦娥飞天的日子,谨以此文预祝她能回宫成功!顺便也希望此文是我下一个工作的起始点!
          
                                        2007.10.24 于北京