首页
社区
课程
招聘
[转帖]SEH 暗桩 一
发表于: 2012-8-5 17:06 19671

[转帖]SEH 暗桩 一

2012-8-5 17:06
19671
最近看罗云彬老师的 《琢石成器》--- 异常处理。转两个帖子,补充下知识。慢慢看

【 以下文字转载自 Virus 讨论区 】
发信人: RonLiang (俩零), 信区: Virus
标  题: SEH in ASM 研究 By Hume
发信站: BBS 水木清华站 (Thu Jun 19 15:24:28 2003), 转信
  
-=-=-=-=-=>
SEH  in ASM 研究
By Hume/冷雨飘心
为什么老调重弹:
SEH出现已绝非一日,但很多人可能还不彻底了解Seh的运行机制;有关seh的知识资
料不是很多,asm级的详细资料就更少!seh不仅可以简化程序错误处理,使你的程序
更加健壮,还被广泛应用于反跟踪以及加解密中,因此,了解seh非常必要,但遗憾的
是关于seh详细介绍的中文资料非常少,在实践的基础上,把自己学习的一点笔记奉
献给大家,希望对喜欢ASM的朋友有所帮助.如有错误,请高手不吝指正.
  
第一部分 基础篇
PART I   简单接触  
一、SEH背景知识
SEH("Structured Exception Handling"),即结构化异常处理.是(windows)操作系
统提供给程序设计者的强有力的处理程序错误或异常的武器.在VISUAL C++中你或
许已经熟悉了_try{} _finally{} 和_try{} _except {} 结构,这些并不是编译程
序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装,不
用这些高级语言编译器所提供的包装 ,照样可以利用系统提供的强大seh处理功能
,在后面你将可以看到,用系统本身提供seh结构和规则以及ASM语言,我们将对SEH的
机制以及实现进行一番(深入?)探究.
使用windows的人对microsoft设计的非法操作对话框一定不会陌生,尤其是在9X下
.这表示发生了一个错误,如果是应用程序的错误,那么windows可能要关闭应用程序
,如果是系统错误,你很可能不得不reset以重新启动计算机.从程序编写的角度来看
,这种异常产生的原因很多,诸如堆栈溢出,非法指令,对windows保护内存的读写权
限不够等等.幸运的是windows通过seh机制给了应用程序一个机会来修补错误,事实
上windows内部也广泛采用seh来除错.让我们先来看看如果一个应用程序发生错误
后windows是怎么处理的.
程序发生异常时系统的处理顺序(most by Jeremy Gordon):
1.因为有很多种异常,系统首先判断异常是否应发送给目标程序的异常处理例程,如
果决定应该发送,并且目标程序正处于被调试状态,则系统挂起程序并向调试器发送
EXCEPTION
_DEBUG_EVENT消息.剩下的事情就由调试器全权负责.如果系统级调试器存在,对于
int 1,int 3这样的异常在faults on时一般是会选择处理的,因而如果你的异常处
理程序由他们来进入,则不会得到执行,呵呵,这不是正好可以用来探测调试器的存
在吗?
2.如果你的程序没有被调试或者调试器未能处理异常(/错误),系统就会继续查找你
是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统
就把异常发送给你程序的线程相关的seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且
安装了多个线程相关的异常处理例程,可交由链起来的其他例程处理.
4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂
起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用
SetUnhandledExceptionFilter安装了final型异常处理例程的话,系统转向对它的
调用.
6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的
系统处理程序,通常显示一个对话框,你可以选择关闭或者最后将其附加到调试器上
的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用
ExitProcess终结程序.
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程
异常处理例程最后清理的机会.以上大致描述了异常/错误发生时系统的逻辑处理顺
序,如果你看了一头雾水的话,别着急,化点时间慢慢理解或者进入下一部分实例操
作.作几个例子后你也许就会慢慢理解了.
    你要有一个最基本的观念就是She只不过是系统在终结你应用程序之前给你的一
个最后处理错误的机会,从程序设计的角度来说就是给你自己设计的一个回调函数
执行的机会.
  
二.初步实战演习:
你在自己程序里可以设计两种异常处理例程,一种是通过
SetUnhandledExceptionFilter API设置的,姑且称之为final型的,他是进程相关的
,也就是说在线程相关的异常处理部分不能处理的异常才会到达final处理例程.另
外一种是线程相关的,他一般用来监视处理进程中某个线程的异常情况.他比较灵活
,可选择监视线程中的某一小段代码.姑且称之为thread型的.
下面看看如何设计一个最简单的异常处理程序.
  挂接异常处理例程:
   I. final型的异常处理   
对于final型的,在你的异常未能得到调试器以及线程相关处理例程处理操作系统在
即将关闭程序之前会回调的例程,这个例程是进程相关的而不是线程相关的,因此无
论是哪个线程发生异常未能被处理,都会调用这个例程.
    见下面的例子1:
;==========================================
; ex. 1,by Hume,2001 演示final型异常处理
;==========================================
.586p
.model flat, stdcall
option casemap :none   ; case sensitive
include c:\hd\hd.h      ;头部包含文件
include c:\hd\mac.h    ;常用宏,自己维护一个吧
;;--------------
.data
szCapdb "By Hume[AfO],2001...",0
szMsgOK db "OK,the exceptoin was handled by final handler!",0
szMsgERR1 db "It would never Get here!",0
        .code
_start:
pushoffset Final_Handler
callSetUnhandledExceptionFilter      
;调用SetUnhandledExceptionFilter来安装final SEH
                                   ;原型很简单SetUnhandledExceptionFilter
  proto
                                   ;pTopLevelExceptionFilter:DWORD
        xor     ecx,ecx
        mov     eax,200
        cdq
divecx         ;除0错误
;以下永远不会被执行
        invokeMessageBox,0,addr szMsgERR1,addr szCap,30h+1000h   
        invokeExitProcess,0                    ;30h=MB_ICONEXCLAMATION
   ;1000h=MB_SYSTEMMODAL  
        ;-----------------------------------------
Final_Handler:
invokeMessageBox,0,addr szMsgOK,addr szCap,30h
moveax,EXCEPTION_EXECUTE_HANDLER     ;==1 这时不出现非法操作的讨厌对话框
  
;moveax,EXCEPTION_CONTINUE_SEARCH     ;==0 出现,这时是调用系统默认的异常
处理过程,程序被终结了
        ;mov    eax,EXCEPTION_CONTINUE_EXECUTION  ;==-1 不断出现对话框,你
将陷入死循环,可别怪我  
ret     4
end _start
  简单来解释几句,windows根据你的异常处理程序的返回值来决定如何进一步处理  
   EXCEPTION_EXECUTE_HANDLER  equ 1    表示我已经处理了异常,可以优雅地结
束了   EXCEPTION_CONTINUE_SEARCH  equ 0    表示我不处理,其他人来吧,于是
windows调用默认的处理程序,显示一个除错对话框,并结束
EXCEPTION_CONTINUE_EXECUTION         equ -1   表示错误已经被修复,请从异
常发生处继续执行   你可以试着让程序返回0和-1然后编译程序,就会理解我所有
苍白无力的语言...
  
II.线程相关的异常处理.
通常每个线程初始化准备好运行时fs指向一个TIB结构(THREAD INformATION  
BLOCK),这个结构的第一个元素fs:[0]指向一个_EXCEPTION_REGISTRATION结构,具
体结构见下,后面_EXCEPTION_REGISTRATION为了简化,用ERR来代替这个结构...不
要说没见过啊...
    fs:[0]->
     _EXCEPTION_REGISTRATION struc  
     prev dd ?                    ;前一个_EXCEPTION_REGISTRATION结构
     handler dd ?                 ;异常处理例程入口....呵呵,现在明白该怎
么作了吧
     _EXCEPTION_REGISTRATION ends  
我们可以建立一个ERR结构然后将fs:[0]换成指向他的指针,当然最常用的是堆栈,
如果你非要用静态内存区也可以, 把handler域换成你的程序入口,就可以在发生异
常时调用你的代码了,好马上实践一下,见例子2
;===========================================
; ex. 2,by Hume,2001  线程相关的异常处理
;===========================================
.386
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h            
;============================
.data
szCapdb "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsgERR1 db "It would never Get here!",0
.code
_start:
   ASSUME FS:NOTHING        ;否则Masm编译报错  
        push    offset perThread_Handler
push    fs:[0]        
        mov     fs:[0],esp       ;建立SEH的基本ERR结构,如果不明白,就仔细
研究一下前面所述
        xor     ecx,ecx                           
        mov     eax,200
        cdq
divecx
;以下永远不会被执行
        invokeMessageBox,0,addr szMsgERR1,addr szCap,10h+1000h
        pop     fs:[0]            ;清除seh链表
        add     esp,4
        invokeExitProcess,0         
;============================
perThread_Handler:
        invokeMessageBox,NULL,addr szMsgOK,addr szCap, 40h+1000h
        mov     eax,1         ;ExceptionContinueSearch,不处理,由其他例程
或系统处理
        ;mov    eax,0         ;ExceptionContinueExecution,表示已经修复
CONTEXT,可从异常发生处继续执行
  
ret                    ;这里如果返回0,你会陷入死循环,不断跳出对话框....
  
end _start
    嘿嘿,这个简单吧,我们由于没有足够的资料,暂时还不能修复ecx的值使之从异
常发生处继续执行,只是简单显示一个MSG,然后让系统处理,自然跳出讨厌的对话框
了....
    注意化5分钟研究和final返回值的含义不同...windows也是根据返回值来决定
下一步的动作的.好像到此为止,我们并没有从异常处理中得到任何好处,除了在异
常发生后可以执行一点我们微不足道的代码,事实上SEH可以修复这些异常或者干我
们想干的任何事情然后从希望的地方继续执行,嘿嘿,很爽吧,可惜我们没有足够的
信息,那里找到我们所需要的信息?
                       
PART II   继续深入
三、传递给异常处理例程的参数
    I、传递给final型的参数,只有一个即指向EXCEPTION_POINTERS结构的指针,  
EXCEPTION_POINTERS定义如下:
       EXCEPTION_POINTERS STRUCT
   pExceptionRecord  DWORD      ?               
   ContextRecord     DWORD      ?
       EXCEPTION_POINTERS ENDS
执行时堆栈结构如下:
           esp    -> ptEXCEPTION_POINTERS  
然后执行call _Final_Handler
注意堆栈中的参数是指向EXCEPTION_POINTERS 的指针,而不是指向
pExceptionRecord的指针
以下是EXCEPTION_POINTERS两个成员的详细结构
EXCEPTION_RECORD STRUCT
  ExceptionCode         DWORD      ?       ;异常码
  ExceptionFlags        DWORD      ?   ;异常标志
  PExceptionRecord      DWORD      ?  ;指向另外一个EXCEPTION_RECORD的指针
  
  ExceptionAddress      DWORD      ?       ;异常发生的地址
  NumberParameters      DWORD      ? ;下面ExceptionInformation所含有的
dword数目
  ExceptionInformation  DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORD ENDS               
      ;EXCEPTION_MAXIMUM_PARAMETERS ==15
;================具体参数解释========================================
ExceptionCode 异常类型,SDK里面有很多类型,但你最可能遇到的几种类型如下:
    C0000005h----读写内存冲突
    C0000094h----非法除0
    C00000FDh----堆栈溢出或者说越界
    80000001h----由Virtual Alloc建立起来的属性页冲突
    C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异

    C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇
妙的返回,则出现此代码,例如调用
  
RtlUnwind时没有Exception Record参数时产生的异常填入的就是这个代码
    80000003h----调试时因代码中int3中断
    80000004h----处于被单步调试状态
注:也可以自己定义异常代码,遵循如下规则:
               
_____________________________________________________________________+
      位:       31~30            29~28           27~16          15~0
               
_____________________________________________________________________+
    含义:     严重程度          29位            功能代码        异常代码
  
              0==成功         0==Mcrosoft     MICROSOFT定义   用户定义
              1==通知         1==客户
              2==警告          28位
              3==错误         被保留必须为0
ExceptionFlags 异常标志
              0----可修复异常
              1----不可修复异常
              2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为
C0000005h即内存异常时
  
(ExceptionCode=C0000005h) 的含义如下,其他情况下一般没有意义
              第一个dword 0==读冲突 1==写冲突
              第二个dword 读写冲突地址
;==========CONTEXT具体结构含义================================
CONTEXT STRUCT                     ; _                  
  ContextFlags  DWORD      ?       ;  |---------------  +00
  iDr0          DWORD      ?       ;  |                 +04
  iDr1          DWORD      ?       ;  |                 +08
  iDr2          DWORD      ?       ;   >调试寄存器       +0C
  iDr3          DWORD      ?       ;  |                 +10
  iDr6          DWORD      ?       ;  |                 +14
  iDr7          DWORD      ?       ; _|                 +18
  FloatSave     FLOATING_SAVE_AREA <>  ;浮点寄存器区      +1C~~+88
  regGs         DWORD      ?       ;--|                 +8C
  regFs         DWORD      ?       ;  |\段寄存器         +90         
  regEs         DWORD      ?       ;  |/                +94
  regDs         DWORD      ?       ;--|                 +98
  regEdi        DWORD      ?       ;____________        +9C  
  regEsi        DWORD      ?       ;       |  通用      +A0   
  regEbx        DWORD      ?       ;       |   寄       +A4
  regEdx        DWORD      ?       ;       |   存       +A8
  regEcx        DWORD      ?       ;       |   器       +AC
  regEax        DWORD      ?       ;_______|___组_      +B0     
  regEbp        DWORD      ?       ;++++++++++++++++    +B4
  regEip        DWORD      ?       ;    |控制           +B8
  regCs         DWORD      ?       ;    |寄存           +BC
  regFlag       DWORD      ?       ;    |器组            +C0
  regEsp        DWORD      ?       ;    |               +C4   
  regSs         DWORD      ?       ;+++++++++++++++++   +C8
  ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
以上是两个成员的详细结构,下面给出一个final型的例子,这也是本文所讨论的最
后一个final型的例子,以后的例子集中在thread类型上.
;--------------------------------------------
; Ex3,演示final处理句柄的参数获取,加深前面
; 参数传递的介绍理解如果难于理解请先看partIII
; 再回来看这个例子
;--------------------------------------------
.586
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h
include mac.h
;;--------------
.data
sztit   db "exceptION MeSs,by hume[AfO]",0
fmt     db "Context eip--> %8X   ebx--> %8X ",0dh,0ah
        db "Flags   Ex.c-> %8x   ***--> %8X",0
szbuf   db 200 dup(0)
;;-----------------------------------------
.CODE
_Start:
        assume  fs:nothing
        push    offset _final_xHandler0
        call    SetUnhandledExceptionFilter
        xor     ebx,ebx
        mov     eax,200
        cdq
        div     ebx
        invoke  MessageBox,0,ddd("Good,divide overflow was solved!"),addr
  sztit,40h
        xor     eax,eax
        mov     [eax],ebx  
    invoke   ExitProcess,0     
;-----------------------------------------
_final_xHandler0:
        push    ebp
        mov     ebp,esp
         
        mov     eax,[ebp+8]      ;the pointer to EXCEPTION_POINTERS
        mov     esi,[eax]        ;pointer to _EXCEPTION_RECORD
        mov     edi,[eax+4]      ;pointer to _CONTEXT
        test    dword ptr[esi+4],1   
  
        jnz     @_final_cnotdo
        test    dword ptr[esi+4],6   
        jnz     @_final_unwind
        ;call    dispMsg  
        cmp     dword ptr[esi],0c0000094h
        jnz     @_final_cnotdo
        mov     dword ptr [edi+0a4h],10
        call    dispMsg
        
        mov     eax,EXCEPTION_CONTINUE_EXECUTION       ;GO ON
        jmp     @f
@_final_unwind:
        invokeMessageBox,0,CTEXT("state:In final unwind..."),addr sztit,
0
                             ;好像不论处理不处理异常,都不会被调用,
right?
@_final_cnotdo:                             
        mov     eax,EXCEPTION_CONTINUE_SEARCH  
        jmp     @f         
@@:        
        mov     esp,ebp
        pop     ebp
        ret
;-----------------------------------------
dispMsgproc                      ;My  lame proc to display some  
message
        pushad
        mov     eax,[esi]
        mov     ebx,[esi+4]
        mov     ecx,[edi+0b8h]
        mov     edx,[edi+0a4h]
        invoke  wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx
        invoke  MessageBox,0,addr szbuf,CTEXT("related Mess of  
context"),0
        popad
ret
dispMsgendp
END_Start
;;------------------------------------------------
   
II、 传递给per_thread型异常处理程序的参数,如下:
     在堆栈中形成如下结构  
             esp    -> *EXCEPTION_RECORD
         esp+4  -> *ERR                     ;注意这也就是fs:[0]的指向
         esp    -> *CONTEXT record           ;point to registers
         esp    -> *Param                    ;呵呵,没有啥意义
           然后执行 call _Per_Thread_xHandler
操作系统调用handler的MASM原型是这样  
invoke xHANDLER,*EXCEPTION_RECORD,*_EXCEPTION_REGISTRATION,*CONTEXT,
*Param
  
即编译后代码如下:
PUSH *Param                     ;通常不重要,没有什么意义
push *CONTEXT record            ;上面的结构
push *ERR                       ;the struc above
push *EXCEPTION_RECORD          ;see above
CALL HANDLER  
ADD ESP,10h  
    下一部分给出thread类型的具体实例.
PART III 不是终结  
我们的目标是分三步走,学会SEH,现在让我们接触最有趣的部分:SEH的应用.seh设
计的最初目的就是为了使应用程
  
序运行得更健壮,因此SEH用于除错,避免应用程序和系统的崩溃是最常见的用途.例
如:
1.比如你的程序里出现了除0错,那你就可以在你的seh处理程序中将除数改为非零
值,per_Thread seh返回0(ExceptionContinueExecution)、final返回-1  
(EXCEPTION_CONTINUE_EXECUTION),系统就会根据你的意图用改变过的context加载
程序在异常处继续执行,由于被除数已经改变为非零值,你的程序就可以正常仿佛什
么也没有发生的继续执行了.
2.seh还可以处理内存读写异常,如果你分配的堆栈空间不够,产生溢出,这时你就可
以处理这个异常,再多分配一些空间,然后结果是你的程序照常运行了,就好像什么
也没有发生过,这在提高内存运用效率方面很值得借鉴,虽然会降低一些程序的执行
效率.另外,在很多加壳或反跟踪软件中,利用vitualAlloc和VitualProtect制造异
常来进入异常程序,或仅仅是用,mov [0],XXX来进入异常程序,要比用int3或者
int1或
pushf
and [esp],100h
popf
进入要隐蔽得多,如果可以随机引起这些异常的话,效果会更好...当然应用很多了
,感兴趣自己去找.话题似乎有点远了,让我们回到最基础的地方.   
前面的例子中你可能已经注意到,假如我们改变了Context的内容,(注意啊,
context包含了系统运行时各个重要的寄存器),并且返回
0(ExceptionContinueExecution-->perThread SEH),或者
-1(EXCEPTION_CONTINUE_EXECUTION,final SEH),就表示要系统已现有的context继
续执行程序,当然我们的改变被重载了,就像周星驰的月光宝盒改变了历史一样奇妙
,程序就会以改变的context内容去执行程序,通过这种手段,我们可以修复程序,使
其继续执行.
  
看下面的例子4.
读之前,先再罗嗦几句,由于前面介绍了seh例程被调用的时候,系统把相关信息已经
压入堆栈,所以我们只要在程序里寻址调用就行了,怎么寻址呢???唉....回顾一下
call指令执行的基本知识,一般对于近调用,通过[esp+4]即刻找到
*EXCEPTION_RECORD,其余的不用说了吧,如果执行了push ebp;mov ebp,esp的话,就
是[ebp+8]指向*EXCEPTION_RECORD,这也是大多数程序用的和我们最常见到的,明白
了吗?不明白?我--去--跳--楼.
;
________________________________________________________________________
  
;|EX.4 By hume,2001,to show the basic simple  seh function  
;
|_______________________________________________________________________
_
.386
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h           ;//相关的头文件,你自己维护一个吧
.data
szCapdb "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsg1 db "In normal,It would never Get here!",0
fmt     db "%s ",0dh,0ah,"  除法的商是:%d",0
buff    db 200 dup(0)
.code
_start:
  Assume FS:NOTHING
        push    offset perThread_Handler
push    fs:[0]        
        mov     fs:[0],esp                       ;//建立SEH的基本ERR结构
,如果不明白,就仔细研究一下吧
        xor     ecx,ecx                           
        mov     eax,200
        cdq
divecx
WouldBeOmit:                               ;//正常情况以下永远不会被执行
  
        add     eax,100                     ;//这里不会执行,因为我们改变
了eip的值  
         
ExecuteHere:
        div     ecx                             ;//从这里开始执行,从结果
可以看到
        invokewsprintf,addr buff,addr fmt,addr szMsg1,eax         
        invokeMessageBox,NULL,addr buff,addr szCap,40h+1000h
        pop     fs:[0]                          ;//修复后显示20,因为我们
让ecx=10
        add     esp,4  
        invokeExitProcess,NULL         
perThread_Handler proc \
uses ebx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD      
   
        mov eax,pContext  
  Assume eax:ptr CONTEXT
        mov     [eax].regEcx,20  ;//Ecx改变
        lea     ebx, ExecuteHere
        mov     [eax].regEip,ebx ;//从我们想要的地方开始执行,嘿嘿,这就是
很多
;//反跟踪软件把你引向的黑暗之域
        mov     eax,0         ;//ExceptionContinueExecution,表示已经修复
  
;//CONTEXT,可从异常发生处
                              ;//reload并继续执行
ret                     
perThread_Handler endp
end _start
;//====================================================
哈哈,从这个例子里我门可以真正看到seh结构化处理的威力,他不仅恢复了ecx的内
容而且使程序按照你想要的顺序
  
执行了,哈哈,如果你对反跟踪很感兴趣的话,你还可以在例程中加入
        xor     ebx,ebx
        mov     [eax].iDr0,ebx
        mov     [eax].iDr2,ebx
        mov     [eax].iDr3,ebx
        mov     [eax].iDr4,ebx
清除断点,跟踪者....嘿嘿,不说你也体验过,当然也可以通过检验drx的值来判断是
否被跟踪,更复杂地,你可以设置dr6,和dr7产生一些有趣的结果,我就不罗嗦了.
上面的例子理解了吧,因为我用的是MASM提供的优势来简化程序,老Tasm Fans可能
会不以为然,你可以试一下下面的代码代替,是TASM,MASM compatibale的
perThread_Handler:
        push    ebp
        mov     ebp,esp
        mov     eax,[ebp+10h]     ;取context的指针
        mov     [eax+0ach],20     ;将ecx=0,可以对照前面的例程和context结

        lea     ebx, ExecuteHere
        mov     [eax+0b8h],ebx    ;eip== offset ExecuteHere,呵呵
        xor     eax,eax
        mov     esp,ebp
        pop     ebp
        ret
这是raw asm的,不过masm既然给我们设计了这么多好东西,我们为什么不好好利用
呢?
好,到现在为止,基本知识已经结束了,我们应该可理解seh的相关文章和写简单的
seh处理程序了,但关于seh还只是刚刚开始,很多内容和应用还没有涉及到,请继续
看提高篇.
  
第二部分  提高篇
PART IV  关于异常处理的嵌套和堆栈展开
在实际程序设计过程中,不可能只有一个异常处理例程,这就产生了异常处理程序嵌
套的问题,可能很多处理例程分别监视若干子程序并处理其中某种异常,另外一个监
视所有子程序可能产生的共性异常,这作起来实际很容易,也方便调试.你只要依次
建立异常处理框架就可以了.关于VC++异常处理可以嵌套很多人可能比较熟悉,用起
来更容易不过实现比这里也就复杂得多,在VC++中一个程序所有异常只指向一个相
同的处理句例程,然后在这个处理例程里再实现对各个子异常处理例程的调用,他的
大致方法是建立一个子异常处理例程入口的数组表,然后根据指针来调用子处理例
程,过程比较烦琐,原来打算大致写一点,现在发现自己对C/C++了解实在太少,各位
有兴趣还是自己参考MSDN  
Matt Pietrek 1996年写的一篇文章<<A Crash Course on the Depths of  
Win32? Structured Exception Handling>>,里面有非常详细的说明,对于系统的实
现细节也有所讨论,不过相信很多人都没有兴趣.hmmm...:)实际上Kernel的异常处
理过程和VC++的很相似.我们的程序中当然也可以采用这种方法,不过一般应用中不
必牛刀砍蚂蚁.
有异常嵌套就涉及到我们可能以前经常看到并且被一些污七八糟的不负责任的”翻
译家”搞得头晕脑涨不知为何的”异常展开”的问题,也许你注意到了如果按照我
前面的例子包括final都不处理异常的话,最后系统在终结程序之前会来一次展开,
在试验之后发现,展开不会调用final只是对per_thread例程展开(right?).什么是
堆栈展开?为什么要进行堆栈展开?如何进行堆栈展开?
     我曾经为堆栈展开迷惑过,原因是各种资料的描述很不一致,Matt Pietrek说展
开后前面的ERR结构被释放,并且好像seh链上后面的处理例程如果决定处理异常必
须对前面的例程来一次展开,很多C/C++讲述异常处理的书也如斯说这使人很迷惑,
我们再来看看Jeremy Gordon的描述,堆栈展开是处理异常的例程自愿进行的.呵呵
,究竟事实如何?
在迷惑好久之后我终于找到了答案:Matt Pietrek讲的没有错,那是VC++以及系统
kernel的处理方法,Jeremy Gordon说的也是正确的,那是我门asm Fans的自由!
好了,现在来说堆栈展开,堆栈展开这个词似乎会使人有所误解,堆栈怎么展开呢?事
实上,堆栈展开是异常处理例程在决定处理某个异常的时候给前面不处理这个异常
的处理例程的一个清洗的机会,前面拒绝处理这个异常的例程可以释放必要的句柄
对象或者释放堆栈或者干点别的工作...那完全是你的自由,叫stack unwind似乎有
点牵强.堆栈展开有一个重要的标志就是EXCEPTION_RECORD.ExceptionFlag为2,表
示正在展开,你可以进行相应的处理工作,但实际上经常用的是6这是因为还有一个
UNWIND_EXIT equ 4什么的,具体含义未知,不过kernel确实就是检测这个值,因此我
们也就检测这个值来判断展开.
注意在自己的异常处理例程中,unwind不是自动的,必须你自己自觉地引发,如果所
有例程都不处理系统最后的展开是注定的. 当然如果没有必要你也可以选择不展开
.win32提供了一个api RtlUnwind来引发展开,如果你想展开一下,就调用这个api吧
,少候讲述自己代码如何展开
RtlUnwind调用描述如下:
        PUSH Return value         ;返回值,一般不用
        PUSH pExceptionRecord     ;指向EXCEPTION_RECORD的指针
        PUSH OFFSET CodeLabel    ;展开后从哪里执行
        PUSH LastStackFrame       ;展开到哪个处理例程终止返回,通常是处理
异常的Err结构
        CALL RtlUnwind
调用这个api之前要注意保护ebx,esi和edi,否则...嘿嘿
MASM格式如下:        
Invoke  RtlUnwind,pFrame,OFFSET return_code_Address,pExceptionRecord,
Return_value  
这样在展开的时候,就以pExceptionRecord.flag=2 依次调用前面的异常处理例程
,到决定异常的处理例程停止,Jeremy Gordon手动展开代码和我下面的例子有所不
同.他描述最后决定处理异常的ERR结构的prev成员为-1,好像我的结果和他的有所
差异,因此采用了另外的方法,具体看下面的例子.
最后一点要注意在嵌套异常处理程序的时候要注意保存寄存器,否则你经常会得到
系统异常代码为C00000027h的异常调用,原因是异常处理中又产生异常,而这个异常
又无法解决,只好由操作系统终结你的程序.
一下给出一点垃圾代码演示可能有助于理解,注意link的时候要加入 /section:.
text,RWE 否则例子里面的代码段不能写,SMC功能会产生异常以致整个程序不能进
行.
注意:2K/XP下非法指令异常的代码不一致,另外用下面的例子在2K下不能工作,具体
错误未知.因此只能在9X下演示,为了在2k/Xp下也能运行我加了点代码,有兴趣看看
,另外帮我解决一下2K/Xp下SMC的问题?thx!
下面例子很烂,不过MASM格式写起来容易一点,也便于理解.看来比较长实际很简单
,耐心看下去.
;-----------------------------------------
;Ex5,演示堆栈展开和异常嵌套处理 by Hume,2002
;humewen@21cn.com  
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h
include mac.h
;;--------------
per_xHandler1proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler2proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler3proto C :DWORD,:DWORD,:DWORD,:DWORD
  
;-----------------------------------------
.data
sztit   db "except Mess,by hume[AfO]",0
count   dd 0,0
Expt1_frm   dd 0                     ;ERR结构指针,用于堆栈展开手动代码
Expt2_frm   dd 0
Expt3_frm   dd 0
         
;;-----------------------------------------
.CODE
_Start:
        assume  fs:nothing
        push    offset per_xHandler3
        push    fs:[0]
        mov     fs:[0],esp
        mov     Expt3_frm,esp
        push    offset per_xHandler2
        push    fs:[0]
        mov     fs:[0],esp
        mov     Expt2_frm,esp
        push    offset per_xHandler1
        push    fs:[0]
        mov     fs:[0],esp
        mov     Expt1_frm,esp
        ;--------------------------
        ;install xhnadler
        ;-----------------------------------------
         
        xor     ebx,ebx
        mov     eax,200
        cdq
        div     ebx                  ;除法错误
        invokeMessageBox,0,ddd("Good,divide overflow was solved!"),addr  
sztit,40h
        sub     eax,eax
        mov     [eax],ebx            ;内存写错误
succ:
        invokeMessageBox,0,ddd("Good,memory write violation solved!"),
addr sztit,40h
         
        db      0F0h,0Fh,0C7h,0C8h   ;什么cmpchg8b指令的非法形式?我从来没
有成功过!!
                                     ;演示程序中使用seh实现SMC技术,加密
??...
        invokeMessageBox,0,ddd("illeagal instruction was solved!"),addr  
sztit,20h
        ;--------------------------
        ;uninstall xhnadler
        ;-----------------------------------------
        pop     fs:[0]              
        add     esp,4
        pop     fs:[0]              
        add     esp,4
       ;或者add esp,10h
        
        pop     fs:[0]              
        add     esp,4
invokeExitProcess,0
;-----------------------------------------     
;异常处理句柄1,处理除法异常错误
per_xHandler1 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,
pDispatch:DWORD
        pushad
        MOV     ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        TEST    [ESI].ExceptionFlags,1
        JNZ     @cantdo1
        TEST    [ESI].ExceptionFlags,6
        JNZ     @unwind1
        CMP     [ESI].ExceptionCode,0C0000094h
        JNZ     @cantdo1
        MOV     EDI,pContext
ASSUME  EDI:PTR CONTEXT
        m2m     [edi].regEbx,20             ;将ebx置20,修复除法错误,继续
执行
        popad
MOV  EAX, ExceptionContinueExecution
        RET
@unwind1:
        invokeMessageBox,0,CTEXT("state: unwinding in xhandler1..."),addr
  sztit,0
@cantdo1:
        popad
        MOV     EAX,ExceptionContinueSearch
RET
per_xHandler1 ENDP
;-----------------------------------------
;异常处理句柄2,处理内存写错误,扩展可以有其他的例子如自动扩充堆栈
per_xHandler2 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,
pDispatch:DWORD
         
        pushad
MOV     ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        MOV     EDI,pContext
ASSUME  EDI:PTR CONTEXT
        call    Dispcont                             ;显示一点lame的消息
,自己调试用
        TEST    [ESI].ExceptionFlags,1
        JNZ     @cantdo2
        TEST    [ESI].ExceptionFlags,6
        JNZ     @unwind2
        CMP     [ESI].ExceptionCode,0C0000005h
        JNZ     @cantdo2
        .data                                         ;ASM的数据定义灵活
性,如果需要这是可以的
         validAddress dd 0
        .code
         
        m2m     [EDI].regEax,<offset  validAddress>    ;置eax为有效地址   
      
        popad
MOV  EAX, ExceptionContinueExecution
        RET
@unwind2:
        invokeMessageBox,0,CTEXT("hmmm... unwinding in xhandler2..."),
addr sztit,40h
@cantdo2:
        popad
        MOV     EAX,ExceptionContinueSearch
RET
per_xHandler2 ENDP
;-----------------------------------------
per_xHandler3 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,
pDispatch:DWORD
pushad
        MOV     ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        MOV     EDI,pContext
ASSUME  EDI:PTR CONTEXT        
        TEST    [ESI].ExceptionFlags,1
        JNZ     @cantdo3
        TEST    [ESI].ExceptionFlags,6
        JNZ     @unwind3
        ;-----------------------------------------
        push    ecx
        mov     ecx,cs
        xor     cl,cl
        jecxz   win2k_Xp         
win9X:  
        pop     ecx
        CMP     [ESI].ExceptionCode,0C000001DH       ;非法指令异常,与
2K/XP下的不一致
        JNZ     @cantdo3
        jmp     ok_here
win2k_Xp:
        pop     ecx                                  ;注意,只有在9X下才
可以
        CMP     [ESI].ExceptionCode,0C000001EH       ;非法指令异常
->2K/XP
        JNZ     @cantdo3                             ;sMc不成
        mov     [edi].regEip,offset safereturn
        popad
        mov     eax,0
        ret
         
        push    ebx
        push    esi
        push    edi        
comment $ 调用RtlUnwind展开堆栈         
        lea     ebx,unwindback
        invokeRtlUnwind,Expt3_frm,ebx,esi,0
        $
        mov     dword ptr [esi+4],2               ;置展开标志,准备展开,这
里是
                                               ;手动代码
        mov     ebx,fs:[0]
         
selfun:
        ;mov     eax,Expt2_frm                 ;这里显示了ASM手动展开的灵
活性
        mov     eax,Expt3_frm                        
        cmp     ebx,eax                    ;按照Jeremy Gordon的好像不大对

        ;cmp     dword ptr [ebx],-1         ;这样好像有问题,只好如上,请教
答案            
        jz      unwindback
        push    ebx
        push    esi                    ; 压入Err和Exeption_registration结

        call    dword ptr[ebx+4]
        add     esp,8
        mov     ebx,[ebx]
        jmp     selfun
unwindback:
        invokeMessageBox,0,CTEXT("I am Back!"),addr sztit,40h
        pop     edi
        pop     esi
        pop     ebx                              ;一定要保存这三个寄存器

        MOV     EAX,[EDI].regEip
        MOV     DWORD PTR[EAX],90909090H  ;改为nop指令...SMC? 很简单吧
                                               ;SMC注意连接/section:RWE
        popad
MOV  EAX, ExceptionContinueExecution
        RET
@unwind3:
        invokeMessageBox,0,CTEXT("Note... unwinding in xhandler3..."),
addr sztit,40h
@cantdo3:
        popad
        MOV     EAX,ExceptionContinueSearch
RET
per_xHandler3 ENDP
;-----------------------------------------
;lame routine for debug
Dispcontproc  
                inc     count
                call    dispMsg
                ret
Dispcontendp
dispMsgproc  
        local szbuf[200]:byte
        pushad
        mov     eax,dword ptr[esi]
        mov     ebx,dword ptr[esi+4]
        mov     ecx,dword ptr[edi+0b8h]
        mov     edx,dword ptr[edi+0a4h]
        .data
        fmt     db "Context eip--> %8X   ebx--> %8X ",0dh,0ah
                db "Flags   Ex.c-> %8x   ***--> %8X",0dh,0ah
                db "it's the %d times xhandler was called!",0
        .code
        invokewsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx,count
        invokeMessageBox,0,addr szbuf,CTEXT("related Mess of context"),
0
        popad
ret
dispMsgendp
;;------------------------------------------------
END_Start
;------------------下面是我在本文里用到到的宏,我的mac.h比较长,就不贴了
-----
    dddMACRO Text                 ;define data in .data section
        local name           ;This and other can be used as: ddd("My  
god!")
        .data           ;isn't cool?
            name    db Text,0
        .code
        EXITM <addr name>   
    ENDM
   CTEXT MACRO y:VARARG                     ;This is a good macro
LOCAL sym
CONST segment
IFIDNI <y>,<>
sym db 0
ELSE
sym db y,0
ENDIF
CONST ends
EXITM <OFFSET sym>
     ENDM
    m2m MACRO M1, M2                          ;mov is too boring  
sometimes!
      push M2
      pop  M1
    ENDM     
;-----------------------------------------
BTW:够长了吧,基本内容介绍完毕,更多内容下一部分介绍一点利用Seh的tricks,哪
位大侠有什么好的想法或者有什么错误,请不吝指正,毕竟我是菜鸟...
  
PART V 利用SEH进入ring0及单步自跟踪的实现--SEH的简单应用
实在太累了,这将是最后一部分.
一、ring0!并不遥远...
作为seh的一个有趣的应用是进入ring0,ring0意味着更多的权利,意味着你可以进
行一些其他ring3级应用程序不能进行的操作,譬如改自己的代码段(在不修改段属
性的前提下),改系统数据(病毒?)等等,在9X下进入ring0的方法很多,在NT下困难的
多,SEH只是其中较简单的一种.打开调试器看看系统kernel的工作状态,在9X下cs一
般是28h,ds,ss等通常是30h,因此只要我们的cs和ss等在异常处理程序中被赋予上
述ring0选择子值,进入ring0就可以实现.可能我们需要执行较复杂的操作,在
ring0下一般不能直接调用常用api,当然VxD,WDM等提供的系统服务是另外一种选择
. 否则,这在用下述简单方法进入ring0后执行会产生错误,因此,我们在ring0下尽
快完成需要完成的任务,然后迅速返回ring3.
  
在ring0下要完成如下任务:
1.取CR3的值,返回ring3显示.在ring3下不可以读取cr3的值.你可以打开kernel调
试器看看例子程序取到的值是否正确.
2.修改代码段后面的jmp ****代码,这在通常情况下只会导致保护错误.而在ring0
下是可以的,就像在前面例子中用seh实现SMC的效果是一样的,最后显示几个
MsgBox,证明我们曾经到达过ring0这个例子是参考owl的那个nasm写的例子用masm
改写,并增加ring0下SMC的代码部分以作演示.另外代码中iretd指令并不是简单实
现跳转,而是实现从ring0切回ring3的功能,在变换代码特权级的同时,堆栈的也要
变换到ring3.可能原例子ljtt前辈的中文注释容易引起初学者的误解.
别的不说,我发现进入ring0后修改代码段可以使trw的跟踪崩溃...hmmm,好消息?代
码如下:
其中用的一些宏在Ex5中已经贴了,就不再重复.
;-----------------------------------------
;Ex6,演示利用seh进入ring0! by Hume,2002
;humewen@21cn.com  
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h
include mac.h
;;--------------
ring0_xHandlerproto C :DWORD,:DWORD,:DWORD,:DWORD
        .data
szbuf   db 100 dup (0)
count   dd 0
;;-----------------------------------------
.CODE
_Start:
        assume fs:nothing
push    offset ring0_xHandler
        push    fs:[0]
        mov     fs:[0],esp
        ;--------------------
        mov     ecx,ds
        test    ecx,100b
        jz      NT_2K_XP         ;NT/2K/XP has no LDT  
        pushfd
        mov     eax,esp
        int     3
        mov     ebx,cr3          ;现在,正式宣布,进入ring0!  
                                 ;呵呵这样简单就进入ring0了,至于进入
        push    ebx              ;ring0有啥用,不要问我!
        lea     ebx,offset _modi  ;SMC
        mov     byte ptr[ebx],75h ;修改jmp addinfo为jnz addinfo指令
        pop     ebx
        push    edx                     ;ss
  
        push    eax                     ;esp
        push    dword ptr[eax]          ;eflags           
        push    ecx                     ;cs
        push    offset ring3back        ;eip
        iretd                           ;这里是通过iretd 指令返回特权级
3
ring3back:
        popfd
        invokewsprintf,addr szbuf,ddd("It's in ring0,please see  
CR3==%08X",0dh,oah,"following display Modified  
  
info..."),ebx
        invokeMessageBox,0,addr szbuf,ddd("Ring0! by Hume[AfO]"),40h   
        xor     eax,eax
        ;add     eax,2
        .data
        Nosmc db "Not modified area!",0
        besmc db "haha,I am modified by self in ring0!",0
        .code
        mov     ebx,offset Nosmc
        mov     eax,0
_modi:
        jmp     addinfo            ;SMC后将这里改为jnz addinfo  
        mov     ebx,offset besmc
        mov     eax,30h
addinfo:
        invokeMessageBox,0,ebx,ddd("Rin0 SMC test"),eax
_exit:
        ;--------------------         
        pop     fs:[0]
        add     esp,4
        invokeExitProcess,0
NT_2K_XP:
        invokeMessageBox,0,ddd("The example not support NT/2K/Xp,only  
9x!"),ddd("By hume"),20h
        jmp     _exit
;-----------------------------------------
ring0_xHandler PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,
pDispatch:DWORD
        pushad         
assume  edi:ptr CONTEXT
assume  esi:ptr EXCEPTION_RECORD
        mov     esi,pExcept
        mov     edi,pContext           
        test    dword ptr[esi+4],1              ;Exception flags
        jnz     @f         
        test    dword ptr[esi+4],6
        jnz     @f
        cmp    dword ptr[esi],80000003h        ;break ponit flag
        jnz     @f         
         
        m2m     [edi].regEcx,[edi].regCs        ;保存3级代码段选择子
        mov     [edi].regCs,28h                 ;0级代码段选择子
        m2m     [edi].regEdx,[edi].regSs        ;保存3级堆栈段选择子
        mov     [edi].regSs,30h                 ;0级堆栈选择子
        mov     dword ptr[esp+7*4],0
        popad         
        ret
@@:
        mov     dword ptr[esp+7*4],1
        popad     
        ret
ring0_xHandler ENDP
;-----------------------------------------
END_Start
由于在NT/2K/XP下这种进入ring0的方法不能使用,所以首先区别系统版本,如果是
NT/2K/XP则拒绝执行, 原理是在
  
NT/2K/XP下没有LDT,因此测试选择子是否指向LDT,这是一种简单的方法,但不推荐
使用, 最好使用GetVersionEx...
至于
         mov     dword ptr[esp+7*4],0
         popad         
是返回eax=1的实现.
二.seh实现单步自跟踪.
有时如果你对SICE,TRW或者其他调试器显示的信息有所怀疑的话,你可以用seh显示
一些信息作为简单的调试手段,当然,单步跟踪的用途远不止于此.首先回忆一下我
们以前了解的单步的概念,当EFLAGS的TF位为1的话执行完某条指令后CPU将产生单
步异常,与执行软指令int1类似.注意产生单步陷阱后eip已经指向下一条指令.但进
入单步异常处理程序后cpu自动清除TF,以便下条指令正常执行.
   
我们要作的就是在seh例程中继续置TF位为1,以便下一条指令执行完毕后继续产生
单步陷阱实现跟踪功能,直到遇到popfd指令为止,当然你也可以随便检测其他指令
或者用记数器来终止单步.
下面例子中如果没有单步跟踪eax的最后结果是3,由于有了单步自跟踪,在seh处理
例程中我们每中断一次要加1,所以最后的结果是7,呵呵.请看下面的例子.
;-----------------------------------------
;Ex7,演示利用seh单步自跟踪 by Hume,2002
;humewen@21cn.com  
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none   ; case sensitive
include hd.h
include mac.h
singlestep_xHandlerproto C :DWORD,:DWORD,:DWORD,:DWORD
;;--------------
.data
count   dd      0
Msg0    db      "Eax=="
DispEAX dd      0,0
;;-----------------------------------------
.CODE
_Start:
   assume fs:nothing
push    offset singlestep_xHandler
        push    fs:[0]
        mov     fs:[0],esp
        ;------------------
        xor     eax,eax
        pushfd
        pushfd
        or      dword ptr[esp],100h        
        popfd                             ;置TF标志进入单步状态
        nop                               ; nop执行完后单步异常引发   
        inc     eax                       ; eip指向,nop后面的指令,就是这

        inc     eax                       ; 单步执行
        inc     eax                       ; normal eax==3,but infact  
eax==7
        popfd
        ;------------------  
        add     eax,30h                   ;convert to ASCIIZ
        mov     DispEAX,eax
        invokeMessageBox,0,addr Msg0,ddd("The Eax equal to..."),0
         
        pop     fs:[0]
        add     esp,4
        invokeExitProcess,0
;-----------------------------------------
singlestep_xHandler PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,
pDispatch:DWORD
        pushad         
assume  edi:ptr CONTEXT
assume  esi:ptr EXCEPTION_RECORD
        mov     esi,pExcept
        mov     edi,pContext      
         
        test    dword ptr[esi+4],1              ;Exception flags test  
common stuff
        jnz     @f         
        test    dword ptr[esi+4],6
        jnz     @f
        cmp    dword ptr[esi],80000004h         ;是否为单步异常标志
  
        jnz     @f
        inc     [edi].regEax  
        mov     ebx,[edi].regEip
        cmp     byte ptr[ebx],9Dh               ;是否是popfd,因为目的是取

        jz     @finish_singlestep               ;单步状态,所以这时就不应
该重置TF
        or      [edi].regFlag,100h              ;否则,重置TF     
                                                ;每单步中断一次,eax加1
                                                ;所以eax最后不等于3,而是
  
                                                ;中断4次后,eax==7        
   
@finish_singlestep:         
         
        mov     dword ptr[esp+7*4],0            ;eax==0  hehe...
        popad                                 ;研究一下pushad指令就明白了
  
        ret
@@:
        mov     dword ptr[esp+7*4],1            ;eax==1
        popad     
        ret
singlestep_xHandler ENDP
;-----------------------------------------
END_Start
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
后话:     
最近很郁闷,生活上的工作上的,就这样懒懒懒散散地目无光彩地苟活于世,最近看
了一点C++,有的地方头大,于是拿起老家伙asm写了点以前许诺过的东西,感觉还是
ASM最有助于理解基本原理~~~不过C/C++给了我们另外的工具,虽然目标代码不够紧
凑,毕竟不需要我们每个人写内核,C/C++可以提高生产能力. crack也一样,如果一
些基本概念都不懂明白还谈什么crack?昨天看精华III一些高手的文章,真是惭愧啊
!
我的专业不是计算机或者以后永远也不会搞计算机,这些当也许只是业余爱好,永远
不会放弃的业余爱好.以后也许很长一段时间里面我会离开大家,毕竟,面临的还有
生活.
由于有关seh的文档资料比较少,所以有了这篇学习心得,在看到这里的时候如果前
面的已经完全理解相信seh对我们而言不会再是什么难题,由于seh是win32通用的错
误处理方法Nt/2K/Xp仍然支持但某些细节或许有所改变,这需要你自己的研究.
写这点笔记的过程也是我自己学习的一个过程,由于时间紧张,乱七八糟还算写完了
感谢你能耐心看到这里,如果有什么错误或者你有什么好的想法,请不要忘了告诉我
.  
作任何事情也许都要善始善终,由于前一段时间比较郁闷,曾一度想放弃.感谢
hying,夜月,破解勇,fpc,FiNALSErAPH(这次拼对了?呵呵...),皮皮,dREAMtHEATER
等还有许多其他朋友,没有你们的鼓励和督促,我不会坚持写到最后.
参考资料:
1.Jeffy Ritcher  <<windows高级编程指南>>  tsinghua  press
2.Matt Pietrek 1996 MSJ<<A Crash Course on the Depths of Win32?  
Structured Exception Handling>>
3.Jeremy Gordon <<Win32 Exception handling for assembler programmers>>  
4.owl和EliCZ等asm高手的汇编源代码
5.others I can’t remember
QQ: 8709369
humewen@21cn.com
humewen@263.net
humeasm.yeah.net
hume.longcity.net
hume/冷雨飘心
2002.1写毕
2002.2 整理
  
-=-=-=-=-=>
--
当你的电脑被病毒或者木马侵害的时候,请到virus版。
如果你受益于virus版,那么今后就请你帮助你的病友们,因为你深知他们所处的境地。
如果virus版未能如你所愿,那么也请你为了他们,将后来跟病毒斗争的经验告诉他们。
当你对病毒反病毒技术感兴趣的时候,请到virus版。共同提高、共同关心计算机安全。
virus版需要每一位网友的支持!

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 0
支持
分享
最新回复 (9)
雪    币: 32
活跃值: (34)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
---提高篇
                                      By Hume[AfO]/冷雨飘心

                        part 4  关于异常处理的嵌套和堆栈展开

    在实际程序设计过程中,不可能只有一个异常处理例程,这就产生了异常处理程序嵌套的问题,可能很多
处理例程分别监视若干子程序并处理其中某种异常,另外一个监视所有子程序可能产生的共性异常,这作
起来实际很容易,也方便调试.你只要依次建立异常处理框架就可以了.
    关于VC++异常处理可以嵌套很多人可能比较熟悉,用起来更容易不过实现比这里也就复杂得多,在VC++
中一个程序所有异常只指向一个相同的处理句例程,然后在这个处理例程里再实现对各个子异常处理例程的
调用,他的大致方法是建立一个子异常处理例程入口的数组表,然后根据指针来调用子处理例程,过程比较烦
琐,原来打算大致写一点,现在发现自己对C/C++了解实在太少,各位有兴趣还是自己
参考MSDN Matt Pietrek 1996年写的一篇文章<Crash Course on the Depths of Win32? Stru >
ctured Exception Handling>>,里面有非常详细的说明,对于系统的实现细节也有所讨论,不过相信很多
人都没有兴趣.hmmm...:)实际上Kernel的异常处理过程和VC++的很相似.

    有异常嵌套就涉及到异常展开的问题,也许你注意到了如果按照我前面的例子包括final都不处理异常的话,
最后系统在终结程序之前会来一次展开,在试验之后发现,展开不会调用final只是对per_thread例程展开
(right?).什么是堆栈展开?为什么要进行堆栈展开?如何进行堆栈展开?
   
    我曾经为堆栈展开迷惑过,原因是各种资料的描述很不一致,Matt Pietrek说展开后前面的ERR结构被释放,
并且好像链后面如果决定处理必须展开,很多C/C++讲述异常处理的书也如斯说这使人很迷惑,我们再来看看Jeremy
Gordon的描述,堆栈展开是处理异常的例程自愿进行的.呵呵,究竟事实如何?
   
    在迷惑好久之后我终于找到了答案:Matt Pietrek讲的没有错,那是VC++以及系统kernel的处理方法,Jeremy
Gordon说的也是正确的,那是我门asm Fans的自由!

    好了现在来说堆栈展开,堆栈展开是异常处理例程在决定处理某个异常的时候给前面不处理这个异常的处理
例程的一个清洗的机会,前面拒绝处理这个异常的例程可以释放必要的句柄对象或者释放堆栈或者干点别的工作...
那完全是你的自由,叫stack unwind似乎有点牵强.堆栈展开有一个重要的标志就是
EXCEPTION_RECORD.ExceptionFlag为2,表示正在展开,你可以进行相应的处理工作,但实际上经常用的是6这是
因为还有一个UNWIND_EXIT什么的,具体含义我也没有搞明白,不过对我们的工作好像没有什么影响.

  注意在自己的异常处理例程中,unwind不是自动的,必须你自己自觉地引发,如果所有例程都不处理系统最后的
展开是注定的,当然如果没有必要你也可以不展开.
  win32提供了一个api RtlUnwind来引发展开,如果你想展开一下,就调用这个api吧,少候讲述自己代码如何展开

RtlUnwind调用描述如下:
        PUSH Return value        ;返回值,一般不用
        PUSH pExceptionRecord    ;指向EXCEPTION_RECORD的指针
        PUSH OFFSET CodeLabel    ;展开后从哪里执行
        PUSH LastStackFrame      ;展开到哪个处理例程终止返回,通常是处理异常的Err结构
        CALL RtlUnwind
调用这个api之前要注意保护ebx,esi和edi,否则...嘿嘿

MASM格式如下:        
        invoke RtlUnwind,pFrame,OFFSET return_code_Address,pExceptionRecord,Return_value

这样在展开的时候,就以pExceptionRecord.flag=2 依次调用前面的异常处理例程,到决定异常的处理例程停止,
Jeremy Gordon手动展开代码和我下面的例子有所不同.他描述最后决定处理异常的ERR结构的prev成员为-1,好像
我的结果和他的有所差异,因此采用了另外的方法,具体看下面的例子.
   
  最后一点要注意在嵌套异常处理程序的时候要注意保存寄存器,否则你经常会得到系统异常代码为C00000027h
的异常调用,然后就是被终结.

  一下给出一点垃圾代码演示可能有助于理解,注意link的时候要加入 /section:.text,RWE 否则例子里面的
代码段不能写,SMC功能会产生异常以致整个程序不能进行.

注意:2K/XP下非法指令异常的代码不一致,另外用下面的方法SMC代码段也不可以!不知如何解决?
只用于9X,为了在2k/Xp下也能运行我加了点代码,有兴趣看看,另外帮我解决一下2K/Xp下SMC的问题?thx!

    下面例子很烂,不过MASM格式写起来容易一点,也便于理解.
;-----------------------------------------
;Ex4,演示堆栈展开和异常嵌套处理 by Hume,2002
;humewen@21cn.com
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h
include mac.h

;;--------------
per_xHandler1        proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler2        proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler3        proto C :DWORD,:DWORD,:DWORD,:DWORD
;-----------------------------------------

.data
sztit  db "except Mess,by hume[AfO]",0
count  dd 0,0
Expt1_frm  dd 0                    ;ERR结构指针,用于堆栈展开手动代码
Expt2_frm  dd 0
Expt3_frm  dd 0
        
;;-----------------------------------------
    .CODE
_Start:
        assume  fs:nothing
        push    offset per_xHandler3
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt3_frm,esp

        push    offset per_xHandler2
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt2_frm,esp

        push    offset per_xHandler1
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt1_frm,esp
        ;--------------------------
        ;install xhnadler
        ;-----------------------------------------
        
        xor    ebx,ebx
        mov    eax,200
        cdq
        div    ebx                  ;除法错误

        invoke    MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h

        sub    eax,eax
        mov    [eax],ebx            ;内存写错误

succ:
        invoke    MessageBox,0,ddd("Good,memory write violation solved!"),addr sztit,40h
        
        db      0F0h,0Fh,0C7h,0C8h  ;什么cmpchg8b指令的非法形式?我从来没有成功过!!
                                    ;演示程序中使用seh实现SMC技术,加密??...
        invoke    MessageBox,0,ddd("illeagal instruction was solved!"),addr sztit,20h
        ;--------------------------
        ;uninstall xhnadler
        ;-----------------------------------------

        pop    fs:[0]            
        add    esp,4
        pop    fs:[0]            
        add    esp,4
      ;或者add esp,10h
      
        pop    fs:[0]            
        add    esp,4

    invoke    ExitProcess,0
;-----------------------------------------        
;异常处理句柄1,处理除法异常错误
per_xHandler1 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
        pushad
        MOV    ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo1
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind1
        CMP    [ESI].ExceptionCode,0C0000094h
        JNZ    @cantdo1
        MOV    EDI,pContext

ASSUME  EDI:PTR CONTEXT
        m2m    [edi].regEbx,20            ;将ebx置20,修复除法错误,继续执行
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind1:
        invoke    MessageBox,0,CTEXT("state: unwinding in xhandler1..."),addr sztit,0
@cantdo1:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler1 ENDP
;-----------------------------------------
;异常处理句柄2,处理内存写错误,扩展可以有其他的例子如自动扩充堆栈
per_xHandler2 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
        
        pushad
    MOV    ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        MOV    EDI,pContext
ASSUME  EDI:PTR CONTEXT

        call    Dispcont                            ;显示一点lame的消息,自己调试用

        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo2
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind2
        CMP    [ESI].ExceptionCode,0C0000005h
        JNZ    @cantdo2
        .data                                        ;ASM的数据定义灵活性,如果需要这是可以的
        validAddress dd 0
        .code
        
        m2m    [EDI].regEax,<offset  validAddress>    ;置eax为有效地址      
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind2:
        invoke    MessageBox,0,CTEXT("hmmm... unwinding in xhandler2..."),addr sztit,40h
@cantdo2:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler2 ENDP
;-----------------------------------------

per_xHandler3 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
    pushad
        MOV    ESI,pExcept
ASSUME  ESI:PTR EXCEPTION_RECORD
        MOV    EDI,pContext
ASSUME  EDI:PTR CONTEXT      

        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo3
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind3
        ;-----------------------------------------
                                                   
        push    ecx
        mov    ecx,cs
        xor    cl,cl
        jecxz  win2k_Xp        
win9X:
        pop    ecx
        CMP    [ESI].ExceptionCode,0C000001DH      ;非法指令异常,与2K/XP下的不一致
        JNZ    @cantdo3
        jmp    ok_here
win2k_Xp:
        pop    ecx                                  ;注意,只有在9X下才可以
        CMP    [ESI].ExceptionCode,0C000001EH      ;非法指令异常->2K/XP
        JNZ    @cantdo3                            ;sMc不成

        mov    [edi].regEip,offset safereturn
        popad
        mov    eax,0
        ret
        
        
        
        push    ebx
        push    esi
        push    edi      
comment $ 调用RtlUnwind展开堆栈        
        lea    ebx,unwindback
        invoke    RtlUnwind,Expt3_frm,ebx,esi,0
        $
        mov    dword ptr [esi+4],2                    ;置展开标志,准备展开,这里是
                                                      ;手动代码
        mov    ebx,fs:[0]
        

selfun:
        ;mov    eax,Expt2_frm                    ;这里显示了ASM手动展开的灵活性
        mov    eax,Expt3_frm                     
        cmp    ebx,eax                            ;按照Jeremy Gordon的好像不大对头
        ;cmp    dword ptr [ebx],-1                ;这样好像有问题,只好如上,请教答案         
        jz      unwindback
        push    ebx
        push    esi                                ; 压入Err和Exeption_registration结构
        call    dword ptr[ebx+4]
        add    esp,8
        mov    ebx,[ebx]
        jmp    selfun

unwindback:
        invoke    MessageBox,0,CTEXT("I am Back!"),addr sztit,40h
        pop    edi
        pop    esi
        pop    ebx                                  ;一定要保存这三个寄存器!

        MOV    EAX,[EDI].regEip
        MOV    DWORD PTR[EAX],90909090H            ;改为nop指令...SMC呵呵这次不神秘了吧
                                                    ;SMC注意连接选项
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind3:
        invoke    MessageBox,0,CTEXT("Note... unwinding in xhandler3..."),addr sztit,40h
@cantdo3:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler3 ENDP
;-----------------------------------------
;lame routine for debug
Dispcont    proc
                inc    count
                call    dispMsg
                ret
Dispcont    endp

dispMsg    proc
        local szbuf[200]:byte
        pushad
        mov    eax,dword ptr[esi]
        mov    ebx,dword ptr[esi+4]
        mov    ecx,dword ptr[edi+0b8h]
        mov    edx,dword ptr[edi+0a4h]
        .data
        fmt    db "Context eip--> %8X  ebx--> %8X ",0dh,0ah
                db "Flags  Ex.c-> %8x  ***--> %8X",0dh,0ah
                db "it's the %d times xhandler was called!",0
        .code
        invoke    wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx,count
        invoke    MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
        popad
    ret
dispMsg    endp

;;------------------------------------------------
END    _Start
;---------------------------------下面是上面用到的宏,我的mac.h比较长,就不贴了-----
    ddd    MACRO Text                        ;define data in .data section
        local name                  ;This and other can be used as: ddd("My god!")
        .data                  ;isn't cool?
            name    db Text,0
        .code
        EXITM <addr name>  
    ENDM

  CTEXT MACRO y:VARARG                    ;This is a good macro
        LOCAL sym
    CONST segment
        IFIDNI <y>,<>
            sym db 0
        ELSE
            sym db y,0
        ENDIF
    CONST ends
        EXITM <OFFSET sym>
    ENDM

    m2m MACRO M1, M2                          ;mov is too boring sometimes!
      push M2
      pop  M1
    ENDM   
;-----------------------------------------
  最后更正一点前面介绍的传送给final型的参数是指向EXCEPTION_POINTERS 的指针,压栈前的堆栈
是如下的,不好意思,原来写的时候我也没深入研究,可能模糊了一点,如有错误,请大家指正
push    ptEXCEPTION_POINTERS
call    xHandler
下面补充一个final参数获得的一个例子
;--------------------------------------------
; Ex5,演示final处理句柄的参数获取,更正前面
; 模糊的介绍
;--------------------------------------------
.586
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h
include mac.h

;;--------------
.data
sztit  db "exceptION MeSs,by hume[AfO]",0
fmt    db "Context eip--> %8X  ebx--> %8X ",0dh,0ah
        db "Flags  Ex.c-> %8x  ***--> %8X",0
szbuf  db 200 dup(0)
;;-----------------------------------------
    .CODE
_Start:
        assume  fs:nothing
        push    offset _final_xHandler0
        call    SetUnhandledExceptionFilter
        xor    ebx,ebx
        mov    eax,200
        cdq
        div    ebx
        invoke    MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h
        xor    eax,eax
        mov    [eax],ebx
        
    invoke    ExitProcess,0   

;-----------------------------------------
_final_xHandler0:
        push    ebp
        mov    ebp,esp
        
        mov    eax,[ebp+8]      ;the pointer to EXCEPTION_POINTERS
        mov    esi,[eax]        ;pointer to _EXCEPTION_RECORD
        mov    edi,[eax+4]      ;pointer to _CONTEXT
        test    dword ptr[esi+4],1  
        jnz    @_final_cnotdo
        test    dword ptr[esi+4],6  
        jnz    @_final_unwind

        ;call    dispMsg
        

        cmp    dword ptr[esi],0c0000094h
        jnz    @_final_cnotdo

        mov    dword ptr [edi+0a4h],10
        call    dispMsg
      
        mov    eax,EXCEPTION_CONTINUE_EXECUTION      ;GO ON
        jmp    @f

@_final_unwind:
        invoke    MessageBox,0,CTEXT("state:In final unwind..."),addr sztit,0
                                          ;好像不论处理不处理异常,系统展开的时候
                                          ;都不会被调用,right?
@_final_cnotdo:                            ;请教是真的吗?还是我写的有问题
        mov    eax,EXCEPTION_CONTINUE_SEARCH
        jmp    @f        
@@:      
        mov    esp,ebp
        pop    ebp
        ret
;-----------------------------------------
dispMsg    proc
        pushad
        mov    eax,[esi]
        mov    ebx,[esi+4]
        mov    ecx,[edi+0b8h]
        mov    edx,[edi+0a4h]
        invoke    wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx
        invoke    MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
        popad
    ret
dispMsg    endp
;;------------------------------------------------
        
END    _Start
;====================================================================================

BTW:够长了吧,基本内容介绍完毕,更多内容下一部分介绍一点利用Seh的tricks,哪位大侠有什么好的想法
或者有什么错误,请不吝指正,毕竟我是菜鸟吗...
2012-8-5 17:11
0
雪    币: 4560
活跃值: (1002)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
好文一篇,收藏了
2012-8-5 17:49
0
雪    币: 32
活跃值: (34)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
神不知鬼不觉的使div溢出,得到正确执行。
。。。。。。。。。。。。。。。
mov        [edi].regEbx,2
mov        [edi].regEip,offset yes
.............................................
...
yes:
div        ebx
2012-8-5 18:17
0
雪    币: 32
活跃值: (34)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
文章三:

A Crash Course on the Depths of Win32 Structured Exception Handling

Matt Pietrek 著  
董岩 译

在所有 Win32 操作系统提供的机制中,使用最广泛的未公开的机制恐怕就要数结构化异常处理(structured exception handling,SEH)了。一提到结构化异常处理,可能就会令人想起 _try、_finally 和 _except 之类的词儿。在任何一本不错的 Win32 书中都会有对 SEH 详细的介绍。甚至连 Win32 SDK 里都对使用 _try、_finally 和 _except 进行结构化异常处理作了完整的介绍。既然有这么多地放都提到了 SEH,那我为什么还要说它是未公开的呢?本质上讲,Win32 结构化异常处理是操作系统提供的一种服务。编译器的运行时库对这种服务操作系统实现进行了封装,而所有能找到的介绍 SEH 的文档讲的都是针对某一特定编译器的运行时库。关键字 _try、_finally 和 _except 并没有什么神秘的。微软的 OS 和编译器定义了这些关键字以及它们的行为。其它的 C++ 编译器厂商也只需要尊从它们定好的语义就行了。在编译器的 SEH 层减少了直接使用纯操作系统的 SEH 所带来的危害的同时,也将纯操作系统的 SEH 从大家的面前隐藏了起来。

我收到过大量的电子邮件说他们都需要实现编译器级的 SEH 但却找不到公开的文档。本来,我可以指着 Visual C++ 和 Borlang C++ 的运行时库的源代码说看一下它们就行了。但是,不知道是什么原因,编译器级的 SEH 仍是个天大的秘密。微软和 Borland 都没有提供 SEH 最内层的源代码。

在本文中,我会从最基本的概念上讲解结构化异常处理。在讲解的时候,我会将操作系统所提供的与编译器代码生成和运行时库支持的分离开来。当深入关键性操作系统程序的代码时,我基于的都是 Intel 版的 Windows NT 4.0。然而。我所讲的大部分内容同样适用于其它的处理器。

我会避免提及实际的 C++ 的异常处理,C++ 下用的是 catch() 而不是 _except。其实,真正的 C++ 异常处理的实现方式和我所讲的方式也是极为相似的。但是,真正 C++ 异常处理特有的复杂性会影响到我这里所讲的概念。对于深挖那些晦涩的 .H 和 .INC 文件并拼凑出 Win32 SEH 的相关代码,最好的一个信息来源就是 IBM OS/2 的头文件(特别是 BSEXCPT.H)。这对有相关经验的人并没什么可希奇的,这里讲的 SEH 机制在微软开发 OS/2 时就定义了。因此,Win32 的 SEH 与 OS/2 的极为相似。

SEH in the Buff

若将 SEH 的细节都放到一起讨论,任务实在艰巨,因此,我会从简单的开始,一层一层往深里讲。如果之前从未使用过结构化异常处理,则正好心无杂念。若是用过,那就要努力将  _try、GetExceptionCode 和
EXCEPTION_EXECUTE_HANDLER从脑子中扫出,假装这是一个全新的概念。Are you ready?Good。

当线程发生异常时,操作系统会将这个异常通知给用户使用户能够得知它的发生。更特别的是,当线程发生异常时,操作系统会调用用户定义的回调函数。这个回调函数想做什么就能做什么。例如,它可以修正引起异常的程序,也可以播放一段 .WAV 文件。无论回调函数干什么,函数最后的动作都是返回一个值告诉系统下面该干些什么(这样说并不严格,但目前可以认为是这样)。既然在用户代码引起异常后,操作系统会回调用户的代码,那这个回调函数又是什么样的呢?换句话说,关于异常都需要知道哪些信息呢?其实无所谓,因为 Win32 已经定义好了。异常的回调函数的样子如下:

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

这个函数原型来自标准 Win32 头文件 EXCPT.H,初看上去让人有点眼晕。如果慢慢看的话,似乎情况还没那么严重。对于初学者来说,大可以忽略返回值的类型 (EXCEPTION_DISPOSITION)。所需知道的就是这个函数叫 _except_handler,需要四个参数。

第一个参数是一个指向 EXCEPTION_RECORD 的指针。这个结构体定义在 WINNT.H 中,定义如下:

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

参数 ExceptionCode 是操作系统分配给异常的号。在 WINNT.H 文件中查找开头为“STATUS_” 的宏就能找到一大堆这样的异常代号。例如,大家熟知的 STATUS_ACCESS_VIOLATION 的代号就是 0xC0000005。更为完整的异常代号可以从 Windows NT DDK 中的 NTSTATUS.H 文件里找到。EXCEPTION_RECORD 结构体的第四个元素是异常发生处的地址。其余的 EXCEPTION_RECORD 域目前都可以忽略掉。_except_handler 函数的第二个参数是一个指向 establisher frame 结构体的指针。在 SEH 里这可是个重要的参数,不过现在先不用管它。第三个参数是一个指向 CONTEXT 结构体的指针。CONTEXT 结构体定义在 WINNT.H 文件中,它保存着某一线程的寄存器的值。Figure 1 即为 CONTEXT 结构体的域。当用于 SEH 时,CONTEXT 结构体保存着发生异常时各寄存器的值。无独有偶,GetThreadContext 和 SetThreadContext 使用的也是相同的 CONTEXT 结构体。第四个也是最后的一个参数叫做 DispatcherContext,现在先不去管它。

简单总结一下,当发生异常时会调用一个回调函数。这个回调函数需要四个参数,其中三个都是结构体指针。在这些结构体中,有些域重要,有些并不重要。关键的问题是 _except_handler 回调函数收到了大量的信息,比如异常的类型和发生的位置。异常回调函数需要使用这些信息来决定所采取的行动。

我很想现在就给出一个样例程序来说明 _except_handler,只是仍有一些东西需要解释,即当异常发生时操作系统是如何知道在那里调用回调函数呢?答案在另一个叫 EXCEPTION_REGISTRATION 的结构体中。本文通篇都能见到这个结构体,因此对这部分还是不要囫囵吞枣为好。唯一能找到  EXCEPTION_REGISTRATION 正式定义的地方就是 Visual C++ 运行时库源代码中的 EXSUP.INC 文件:

_EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
_EXCEPTION_REGISTRATION ends

可以看到,在 WINNT.H 的 NT_TIB 结构体定义中,这个结构体被称为 _EXCEPTION_REGISTRATION_RECORD。然而 _EXCEPTION_REGISTRATION_RECORD 的定义是没有的,因此我所能用的只能是 EXSUP.INC 中的汇编语言的 struc 定义。对于我前面提到的 SEH 的未公开,这就是一例。

不管怎样,我们回到目前的问题上来。当异常发生时,OS 是如何知道调用位置的呢?EXCEPTION_REGISTRATION结构体有两个域,第一个先不用管。第二个域,handler,为一个指向 _except_ handler 回调函数的指针。有点儿接近答案了,但是还有个问题就是,OS 从哪里能找到这个 EXCEPTION_REGISTRATION 结构体呢?

为了回答这个问题,需要记住结构化异常处理是以线程为基础的。也就是说,每一个线程都有自己的异常处理回调函数。在1996年5月的专栏中,我讲了一个关键的 Win32 数据结构,线程信息块(TEB 或 TIB)。这个结构体中有一个域对于 Windows NT, Windows 95, Win32s 和 OS/2 都是相同的。TIB 中的第一个 DWORD 是一个指向线程的 EXCEPTION_REGISTRATION 结构体的指针。在 Intel 的 Win32 平台上,FS 寄存器永远指向当前的 TIB,因此,在 FS:[0] 就可以找到指向 EXCEPTION_REGISTRATION 结构体的指针。答案出来了!当异常发生时,系统察看出错线程的 TIB 并取回一个指向 EXCEPTION_REGISTRATION 结构体的指针,从而得到一个指向 _except_handler 回调函数的指针。现在操作系统已经有足够的信息来调用 _except_handler 函数了,见 Figure 2。

把目前这一小点儿东西凑到一起,我写了一个小程序来演示所讲到的这个非常简单的 OS 级的结构化异常处理。Figure 3 所示的就是 MYSEH.CPP,它只有两个函数。main 函数使用了三个内嵌的 ASM 块。第一个块使用两条 PUSH 指令(“PUSH handler”和“PUSH FS:[0]”)在堆栈上构建了一个 EXCEPTION_REGISTRATION 结构体。PUSH FS:[0] 将 FS:[0] 的上一个值保存为结构体的一部分,但是目前并不重要。重要的是堆栈上有一个8字节的 EXCEPTION_REGISTRATION 结构体。下一条指令(MOV FS:[0],ESP)将线程信息块的第一个 DWORD 指向新的 EXCEPTION_REGISTRATION 结构体。

在堆栈上构建 EXCEPTION_REGISTRATION 结构体而不是使用全局变量是由原因的。当使用编译器的 _try/_except 语义时,编译器也会在堆栈上构建 EXCEPTION_REGISTRATION 结构体。我只是要说明使用 _try/_except 后编译器所做的最起码的工作。回到 main 函数,下一个 __asm 块清零了 EAX 寄存器(MOV EAX,0)然后将寄存器的值作为内存地址,而下一条指令就向这个地址进行写入(MOV [EAX],1),这就引发了异常。最后的 __asm 块移除这个简单的异常处理:首先恢复以前的 FS:[0] 的内容,然后从堆栈中弹出 EXCEPTION_REGISTRATION 记录(ADD ESP,8)。

现在假设正在运行 MYSEH.EXE,看一下程序的执行情况。MOV [EAX],1 指令的执行引发了一个 access violation。系统察看 TIB 的 FS:[0] 并找到指向 EXCEPTION_REGISTRATION 结构体的指针。结构体中有一个指向 MYSEH.CPP 文件中的 _except_handler 函数的指针。系统将所需的四个参数入栈并调用 _except_handler 函数。一进入 _except_handler,代码首先用一条 printf 语句打印“Yo! I made it here!”。然后,_except_handler 修复引起异常的问题。问题在于 EAX 指向了不可写内存的地址(地址 0)。所做的修复就是修改 CONTEXT 中 EAX 的值,使其指向一个可写的内存单元。在这个简单的程序里,一个 DWORD 类型变量(scratch)就是用于此目的的。_except_handler 函数的最后的动作就是返回 ExceptionContinueExecution 类型的值,这个结构体定义在标准的 EXCPT.H 文件中。

当操作系统看到所返回的 ExceptionContinueExecution 时,就认为问题已被解决并重新执行引起异常的指令。因为我的 _except_handler 函数修改了 EAX 寄存器使其指向了有效的内存,MOV EAX,1 就再一次执行,main 函数正常继续。并不很复杂,不是吗?

Moving In a Little Deeper

有了这个最简单的情形,我们再回来填补几个空白。尽管异常回调如此伟大,但并不完美。对于任意大小的程序,编写一个函数来处理程序中可能发生的所有异常,那这个函数恐怕会是一团糟。更为可行的情形是能有多个异常处理函数,每一个函数都用于程序的某一特定的部分。操作系统提供了这个功能。

还记得系统查找异常处理回调函数所用的 EXCEPTION_REGISTRATION 结构体吧?此结构体的第一个参数,就是我前面忽略的那个,它叫做 prev。它确实是指向另一个 EXCEPTION_REGISTRATION 结构体的指针。这个第二个 EXCEPTION_REGISTRATION 结构体可以有一个完全不同的处理函数。而且它的 prev 域还可以指向第三个 EXCEPTION_REGISTRATION 结构体,依次类推。简单讲,就是一个 EXCEPTION_REGISTRATION 结构体数组的链表。此链表的表头总是由线程信息块的第一个 DWORD (Intel 机器上的 FS:[0] )所指向。

操作系统用这个 EXCEPTION_REGISTRATION 结构体链表做什么?当异常发生时,系统遍历此链表并查找回调函数与异常相符的 EXCEPTION_REGISTRATION。对于 MYSEH.CPP 来说,回调函数返回 ExceptionContinueExecution 型的值,与异常相符合。回调函数也可能不适合所发生的异常,这时系统就移向链表中下一个 EXCEPTION_REGISTRATION 结构体并询问异常回调是否要处理此异常。Figure 4 所示即为此过程。一旦系统找到了处理此异常的回调函数就停止对 EXCEPTION_REGISTRATION 链表的遍历。
我给出了一个异常回调不能处理异常的例子,见 Figure 5 的 MYSEH2.CPP。为简单起见,我用了一点编译器级的异常处理。main 函数只是建立一个 _try/_except 块。_try 块中的是一个对 HomeGrownFrame 函数的调用。函数与前面的 MYSEH 程序中的代码很类似。它在堆栈上创建了一个 EXCEPTION_REGISTRATION 记录并使 FS:[0] 指向此纪录。在建立了新的处理程序后,函数主动引起异常,向 NULL 指针处进行写入:

*(PDWORD)0 = 0;

这里的异常回调函数,也就是 _except_ handler,与前面的那个很不一样。代码先打印出函数的 ExceptionRecord 参数的异常代号和标志。后面会说明打印此异常标志的原因。因为这个 _except_handler 函数并不能修复引起异常的代码,它就返回 ExceptionContinueSearch。这就使得操作系统继续查找链表中的下一个 EXCEPTION_REGISTRATION 记录。下一个异常回调是用于 main 函数中的 _try/_except 代码的。_except 块只是打印“Caught the exception in main()”。此处的异常处理就是简单地将其忽略。此处的一个关键的问题就是执行控制流。当处理程序不能处理异常时,就是在拒绝使控制流在此处继续。接受异常的处理程序则在所有异常处理代码完成之后决定控制流在哪里继续。这一点并不那么显而易见。

当使用结构化异常处理时,如果一场处理程序没能处理异常,则函数可以用一种非正常的方式退出。例如,MYSEH2 的 HomeGrownFrame 函数中的处理程序并没有处理异常。因为异常处理链中后面的某个处理程序(main 函数)处理了此异常,所以引起异常的指令之后的 printf 从未获得执行。从某种意义上说,使用结构化异常处理和使用运行时库函数 setjmp 和 longjmp 差不多。

若是运行 MYSEH2,其输出可能会令人惊讶。看上去似乎调用了两次 _except_handler 函数。第一次是可以理解的,那第二次又是怎么回事呢?

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()

比较由“Home Grown Handler”开始的两行,其区别是显然的,即第一次的异常标志为0,而第二次则为2。这里就需要提到 unwinding 的概念。进一步讲,当异常回调拒绝处理异常时,就又被调用了一次。这次回调并没有立即发生,二是更为复杂。我还需要再进一步明确异常发生时的情景。

当异常发生时,系统遍历 EXCEPTION_REGISTRATION 结构体链表直至找到处理此异常的处理程序。一旦找到了处理程序,系统再一次遍历此链表,直到处理异常的节点。在第二次遍历中,系统对所有的异常处理函数进行第二次调用。关键的区别就是在第二次调用中,异常标志被设为值2。这个值对应着 EH_UNWINDING( EH_UNWINDING 的定义在 Visual C++ 运行时库源代码的 EXCEPT.INC 里,但 Win32 SDK 里并没有等价的定义)。

EH_UNWINDING 是什么意思呢?当异常回调被第二次调用时(带有 EH_UNWINDING 标志),操作系统就给处理函数一次做所需清理的机会。什么样的清理呢?一个很好的例子就是 C++ 类的析构函数。当函数的异常处理函数拒绝处理异常时,控制流一般并不会以正常的方式从函数中退出。现在考虑一个函数,此函数声明了一个局部的 C++ 类。C++ 规范指出析构函数是必须被调用的。第二次标志为 EH_UNWINDING 的异常处理回调就是为做调用析构函数和 _finally 块此类的清理工作提供机会。在异常被处理并且所有之前的 exception frames 都被调用以进行 unwind 之后,程序从回调函数选择的地方继续。但是记住,这并不等于将指令指针设为所要的代码地址并继续执行。继续执行出的代码要求堆栈和帧指针(Intel CPU 的 ESP 和 EBP 寄存器)都设为在处理异常的堆栈帧中相应的值。因此,接受某一异常的处理程序负责将堆栈指针和堆栈帧指针设为包含处理异常的 SEH 代码的堆栈帧中的值。

Figure 6 Unwinding from an Exception

更一般地说,从异常中的 unwinding 使得位于处理帧的堆栈区域之下的所有的东西都被移除,几乎相当于从未调用过那些函数。unwinding 的另一个效果就是链表中位于处理异常的 EXCEPTION_REGISTRATION 之前的所有 EXCEPTION_REGISTRATIONs 都被从链表中移除。这是有意义的,因为这些 EXCEPTION_REGISTRATION 一般都是在堆栈上构建的。在异常被处理后,堆栈指针和堆栈帧指针在内存中的地址要比从链表中移除的那些 EXCEPTION_REGISTRATIONs 高。Figure 6 所示即为所述。

Help! Nobody Handled It!

到目前为止我都是假设操作系统总能在  EXCEPTION_REGISTRATION 链表中找到处理程序。要是没有相应的处理程序怎么办?这种情况几乎不会发生。原因是操作系统私地下为每个线程都准备了一个默认的异常处理程序。这个默认的异常处理程序总是链表的最后一个节点并总被选来处理异常。它的行为与一般的异常回调函数有些不同,我之后会说明。

我们来看一下系统在那里安插这个默认的、最终的异常处理程序。这显然要在线程执行的前期进行,要在任何用户代码执行之前。Figure 7 为我为 BaseProcessStart 写的伪码,BaseProcessStart 是 Windows NT 的 KERNEL32.DLL 的一个内部函数。BaseProcessStart 需要一个参数,即线程入口的地址。BaseProcessStart 运行在新进程的上下文中并调用入口点来启动进程第一个线程。

注意在伪码中,对 lpfnEntryPoint 的调用被封装在了一对 _try 和 _except 中。这个 _try 块就是用来在异常处理链表中安装那个默认的最终异常处理程序的。所有之后注册的异常处理程序都会插在链表中这个处理程序的前面。若 lpfnEntryPoint 函数返回,线程就运行至完成而不引起异常。若是这样,BaseProcessStart 调用 ExitThread 来结束线程。

要是另一种情况,即线程发生了异常却再也没有异常处理程序了怎么办?在这种情况下,控制流流进 _except 关键字后的大括号里。在 BaseProcessStart 里,这段代码叫 UnhandledExceptionFilter API,我在后面还会回来介绍它。现在的关键是 UnhandledExceptionFilter API 包含着默认的异常处理函数。

若 UnhandledExceptionFilter 返回的是 EXCEPTION_EXECUTE_HANDLER,BaseProcessStart 的 _except 块就执行。_except 块代码所作的就是调用 ExitProcess 来结束当前进程。仔细考虑一下,这样做还是有意义的;一个常识就是,如果程序引起了异常又没有处理程序能处理此异常,系统就结束该进程。伪码中所展示的正是这种情况。

还要最后补充一点。如果引发异常的线程是作为服务运行的且是用于一个基于线程的服务,则 _except 块并不会调用 ExitProcess 而是调用 ExitThread。没有人会因为一个服务出错而结束整个服务进程。

UnhandledExceptionFilter 中的默认异常处理程序又作了些什么呢?当我在讨论班上提出这个问题时,没几个人能猜出未处理的异常发生时操作系统的默认行为。通过对默认处理程序行为的演示,答案一点即明,人们就都明白了。我只是运行了一个主动引起异常的程序,并指出其结果(见 Figure 8)。

  
Figure 8 Unhandled Exception Dialog

UnhandledExceptionFilter 显示了一个对话框,告诉你发生了一个异常。此时,要么可以结束进程,要么就调试引发异常的进程。在这幕后还有相当多的操作,我在本文结束前再来讲这些东西。正如我所提到的,当异常发生时,用户编写的代码可以得到执行(通常是这样的)。类似地,在 unwind 操作过程中,用户编写的代码也可以得到执行。用户的代码可能仍有问题并引起另一个异常。因此,异常回调函数还可以返回另外两个值:ExceptionNestedException 和 ExceptionCollidedUnwind。显然这些内容就很深了,我并不想在这里介绍。其对于理解基本事实来说太难了。

Compiler-level SEH

尽管我偶尔会使用 _try 和 _except,但目前我所讲到的都是由操作系统实现的。然而,看看我那两个使用纯操作系统 SEH 的程序的变态样子,编译器对此的封装实在是必要的。我们来看一下 Visual C++ 是如何在操作系统级的 SEH 支持之上构建其结构化异常处理的。

在继续进行之前要记住一件重要的事,那就是另一种编译器可能会与纯操作系统级的 SEH 的做法完全不同。没有人说过必须要实现 Win32 SDK 文档所描述的 _try/_except 模型。例如,Visual Basic 5.0 在其运行时代码里使用了结构化异常处理,但其数据结构与算法与我这里所讲的完全不同。若读一下 Win32 SDK 文档关于结构化异常处理的描述,就会找到所谓的“frame-based”的异常处理程序的语义,其形式如下:

try {
     // guarded body of code
}
except (filter-expression) {
     // exception-handler block
}

简单讲,try 中的所有的代码都被一个构建在函数堆栈帧上的 EXCEPTION_REGISTRATION 保护起来。在函数的入口,新的 EXCEPTION_REGISTRATION 被放入异常处理链表的表头。在 _try 块的结尾处,其 EXCEPTION_REGISTRATION 被从链表头移除。如前所述,异常处理链的表头保存在 FS:[0]。因此,若在调试器中的汇编代码中单步执行,就会看到以下的指令:

MOV DWORD PTR FS:[00000000],ESP

或是

MOV DWORD PTR FS:[00000000],ECX

可以十分确信代码正在建立或撤除一个 _try/_except 块。现在知道了一个 _try 块对应着堆栈上的一个 EXCEPTION_REGISTRATION 结构体,那 EXCEPTION_ REGISTRATION 里的回调函数呢?使用 Win32 的术语,异常回调函数对应着 filter-expression 代号。filter-expression 就是关键字 _except 后括号中的代码。正是这个 filter-expression 代号决定了是否执行后面 {} 块中的代码。

因为 filter-expression 是程序员写的,程序员可以决定代码中某处发生的异常是否在该处处理。filter-expression 代码可以简单到只有一个“EXCEPTION_EXECUTE_HANDLER”,也可以调用一个函数把 p 算到两千万再返回一个代号告诉系统下一步做什么,这是程序员的选择。关键一点是:filter-expression 的代号正对应我前面提到的异常回调函数。

我刚才所讲的都十分简单,但是只是理想中的美好的情形。残酷的现实是事情要复杂的多。对于初学者,filter-expression 并不是由操作系统直接调用的。实际的情形是每个 EXCEPTION_REGISTRATION 的异常处理程序域都指向同一个函数。这个函数在 Visual C++ 的运行时库中,叫做 __except_handler3。是  

另外一点就是并不是每次进入或退出 _try 块都要建立或撤除 EXCEPTION_REGISTRATION。对于使用 SEH 的每个函数,只创建一个 EXCEPTION_REGISTRATION。换句话说,在一个函数里可以使用多个 _try/_except 组合,但只在堆栈上建立一个 EXCEPTION_REGISTRATION。类似地,可以在一个函数的 _try 块中嵌套另一个 _try 块,Visual C++ 仍然只创建一个 EXCEPTION_REGISTRATION。如果对于整个 EXE 或 DLL 来说一个异常处理程序就足够了以及如果用一个 EXCEPTION_REGISTRATION 就可以处理多个 _try 块,那显然还要有比所见到的更多的机制。这是通过一个一般情况下看不到的表中的数据来完成的。然而,既然本文的目的就是要解剖结构化异常处理,我们就来看一下这些数据结构。

The Extended Exception Handling Frame

Visual C++ 的 SEH 实现并没有使用纯粹的 EXCEPTION_REGISTRATION 结构,而是在结构体的末尾加入了额外的数据域。这个额外的数据的关键之处在于它允许一个函数(__except_handler3)来处理所有的异常并将控制流转向相应的 filter-expressions 和代码中的 _except 块。关于这个 Visual C++ 扩展的 EXCEPTION_REGISTRATION 的一点信息可以从 Visual C++ 的运行时库源代码中的 EXSUP.INC 文件里找到。在这个文件里,可以找到一下定义:

;struct _EXCEPTION_REGISTRATION{
;     struct _EXCEPTION_REGISTRATION *prev;
;     void (*handler)(PEXCEPTION_RECORD,
;                     PEXCEPTION_REGISTRATION,
;                     PCONTEXT,
;                     PEXCEPTION_RECORD);
;     struct scopetable_entry *scopetable;
;     int trylevel;
;     int _ebp;
;     PEXCEPTION_POINTERS xpointers;
;};

前两个域前面已经见过了,prev 和 handler。他们组成了最基本的 EXCEPTION_REGISTRATION 结构体。新加的是最后的三个域:scopetable、trylevel 和 _ebp。scopetable 域指向一个 scopetable_entries 类型结构体数组,而 trylevel 是这个数组的索引。最后一个域,_ebp,是创建 EXCEPTION_REGISTRATION 之前的堆栈帧指针(EBP)的值。

_ebp 域成为扩展的 EXCEPTION_REGISTRATION 结构体的一部分不是偶然的。结构体包含它是因为大多数函数都以一个 PUSH EBP 开始。这就使得所有其它的 EXCEPTION_REGISTRATION 域可以通过帧指针的负偏移来访问。例如,trylevel 在 [EBP-04],scopetable 指针在 [EBP-08] 等等。

在扩展的 EXCEPTION_REGISTRATION 结构体后面,Visual C++ 压入了两个额外的值。第一个 DWORD 为一个指向 EXCEPTION_POINTERS 结构体(一个标准的 Win32 结构体)的指针保留空间。这个指针就是调用 GetExceptionInformation API 返回的指针。尽管 SDK 文档隐含提到 GetExceptionInformation 是一个标准的 Win32 API,但事实上 GetExceptionInformation 是一个编译器相关的函数。当调用此函数时,Visual C++ 生成下面的代码:

MOV EAX,DWORD PTR [EBP-14]

与 GetExceptionInformation 相同,GetExceptionCode 也依赖于编译器。GetExceptionCode 返回的值是 GetExceptionInformation 返回的数据结构中一个域的值。Visual C++ 会生成以下的代码,这些代码的作用留给读者作为练习。

MOV EAX,DWORD PTR [EBP-14]
MOV EAX,DWORD PTR [EAX]
MOV EAX,DWORD PTR [EAX]

回到扩展的 EXCEPTION_REGISTRATION 结构体,在结构体起始处之前的8个字节处,Visual C++ 保留了一个 DWORD 来保存所有 prologue 代码执行后最终的堆栈指针(ESP)。这个 DWORD 就是函数执行时一个普通的 ESP 寄存器的值(当然参数压栈是为了准备调用另外函数的情况除外)。

看起来我好像一股脑儿倒出了一大堆东西,确实是。在向下继续之前,我们先暂停一会儿,复习一下 Visual C++ 为用到结构化异常处理的函数生成的标准异常帧:

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

从操作系统的观点来看,构成纯 EXCEPTION_REGISTRATION 的域仅有两个:[EBP-10] 处的 prev 指针和 [EBP-0Ch] 处的处理函数指针。帧中其它的东西都是依赖于 Visual C++ 实现的。记住这些后,我们来看包含着编译器级 SEH 的 Visual C++ 的运行时库函数,__except_handler3。

__except_handler3 and the scopetable

尽管我非常想将 Visual C++ 的运行时库源代码指点出来并让读者自己去研究 __except_handler3 函数,但是我不能,因为此函数的代码并未提供。这里只好用我仓促拼凑出的 __except_handler3 的伪码来应付一下了(见 Figure 9)。

尽管 __except_handler3 看上去是成堆的代码,但要记着它只是一个异常回调函数,就像我在文章开头介绍的那样。和 MYSEH.EXE 和 MYSEH2.EXE 中的 homegrown 异常回调函数一样,此函数也需要四个参数。在最高一级上,__except_handler3 被一个 if 语句分为了两部分。这是因为函数可以被调用两次,一次是正常调用,一次是在 unwind 过程中。大部分的代码都用在了 non-unwinding 的回调中。

这段代码的开头首先在堆栈上创建一个 EXCEPTION_POINTERS 结构体,并用 __except_handler3 的两个参数将其初始化。此结构体的地址,即伪码中的 exceptPtrs,被放在 [EBP-14]。这就初始化了  GetExceptionInformation 和 GetExceptionCode 函数用到的指针。接着,__except_handler3 从 EXCEPTION_REGISTRATION 帧([EBP-04])取得当前的 trylevel。trylevel 变量用作 scopetable 数组的索引,使得一个 EXCEPTION_REGISTRATION 可以用于一个函数中的多个 _try 块和嵌套的 _try 块。每一个 scopetable 的成员定义如下:

typedef struct _SCOPETABLE
{
     DWORD       previousTryLevel;
     DWORD       lpfnFilter
     DWORD       lpfnHandler
} SCOPETABLE, *PSCOPETABLE;

SCOPETABLE 中的第二个和第三个参数都很容易理解。它们是 filter-expression 和相应的 _except 块代码的地址。previousTryLevel 域有点复杂。简言之,它是用于嵌套 try 块的。重要的一点是对函数中每一个 _try 块都有一个 SCOPETABLE 成员。

如前所述,当前的 trylevel 指定了要使用的 scopetable 数组成员,也就指定了 filter-expression 和 _except 块的地址。现在,考虑一种情形,即一个 _try 块嵌套在另一个 _try 里。若内层 _try 块的 filter-expression 并没有处理异常,则外层的 _try 块的 filter-expression 就必须处理。 __except_handler3 如何知道哪一个 SCOPETABLE 成员对应着外层的 _try 呢?它的索引由 SCOPETABLE 成员的 previousTryLevel 域给出。使用这种机制,就可以创建任意嵌套的 _try 块。previousTryLevel 域为函数中可能的异常处理链表的一个节点。链表的结尾由一个 0xFFFFFFFF 的 trylevel 指示。

回到 __except_handler3 的代码,在取得当前 trylevel 之后,代码就指向了相应的 SCOPETABLE 成员并调用了 filter-expression 代码。若 filter-expression 返回 EXCEPTION_CONTINUE_SEARCH,__except_handler3  继续查找下一个 SCOPETABLE 成员,这个成员由 previousTryLevel 域指定。若在遍历过程中没有找到处理程序,__except_handler3 就返回 DISPOSITION_CONTINUE_SEARCH,这就使系统移向下一个 EXCEPTION_REGISTRATION 帧。

若 filter-expression 返回 EXCEPTION_EXECUTE_HANDLER,则意味着异常应该由相应的 _except 块代码来处理。这就意味着所有之前的 EXCEPTION_REGISTRATION 帧都要从链表中移除而且要执行 _except 块。第一个活儿是通过调用 __global_unwind2 来完成的,之后我再介绍。在一些清理代码之后,代码的执行就离开了 __except_handler3 并进入 _except 块。奇怪的是控制流从未从 _except 块返回,尽管 __except_handler3 调用了它。如何设置当前的 trylevel 呢?这是由编译器暗自处理的,编译器以 on-the-fly 的方式完成对扩展的 EXCEPTION_REGISTRATION 结构体中的 trylevel 域的修改。如果察看使用 SEH 的函数的汇编代码就会发现函数代码的不同位置都有修改 [EBP-04] 处的当前 trylevel 的代码。__except_handler3 如何调用的 _except,而控制流又为何从不返回呢?因为一个 CALL 指令将返回地址压入堆栈,可以认为这就打乱了堆栈。如果察看一下为 _except 块生成的代码,就会发现它所作的第一件事就是将 EXCEPTION_REGISTRATION 结构体之后的8字节处的 DWORD 加载到 ESP 寄存器中。作为其 prologue 代码的一部分,函数将 ESP 的值保存起来,这样 _except 之后还可以将其取回。

The ShowSEHFrames Program

此时是不是对 EXCEPTION_REGISTRATIONs、scopetables、trylevels、filter-expressions 和 unwinding 这些东西感到有些招架不住,我当初也是这样的。编译器级的结构化异常处理的主题对更多的学习并没有什么帮助。如果没有总体上的了解的话,其中的很多东西就没有意义。当面对一大堆理论时,我很自然地倾向于写些使用这些理论的代码。如果程序能工作,我就知道我的理解(通常是)是正确的。

Figure 10 是 ShowSEHFrames.EXE 的源代码。它使用 _try/_except 块来建立起由几个 Visual C++ SEH 帧构成的链表。之后,显示每一帧的信息,以及 Visual C++ 为每一帧建立的 scopetables。程序并不生成任何异常。我包含了所有的 _try 块来强制 Visual C++ 生成多个 EXCEPTION_ REGISTRATION 帧,每一帧有多个 scopetable 成员。

ShowSEHFrames 里重要的函数是 WalkSEHFrames 和 ShowSEHFrame。WalkSEHFrames 首先打印出 __except_handler3 的地址,原因一会儿再讲。接着,函数从 FS:[0] 得到一个指向异常链表表头的指针然后遍历链表中的每一个节点。每个节点都是 VC_EXCEPTION_REGISTRATION 类型的,我定义这个结构体是为了描述 Visual C++ 的异常处理帧。对于链表中的每一个节点,WalkSEHFrames 将指向节点的指针传递给 ShowSEHFrame 函数。

ShowSEHFrame 首先打印异常帧的地址、回调函数地址、前一异常帧的地址和一个指向 scopetable 的指针。接着,对于每一个 scopetable 成员,代码打印出 previous trylevel,filter-expression 的地址和 _except 块的地址。我又是怎么知道 scopetable 中到底有多少个成员的呢?其实我并不知道。我假设 VC_EXCEPTION_REGISTRATION 中的当前 trylevel 比 scopetable 的成员总数少一。

Figure 11 所示即为 ShowSEHFrames 的运行结果。首先看以“Frame:”开头的每一行。注意每个后继的实例是如何显示堆栈上高地址的异常帧的。接着,在前三个 Frame: 行里,注意 Handler 的值都是相同的(004012A8)。看看输出的开头就知道这个 004012A8 就是 Visual C++ 运行时库的 __except_handler3 函数的地址。这就证实了我前面所说的一个成员处理多个异常。

也许有人会疑惑,因为 ShowSEHFrames 只有两个使用 SEH 的函数,而却有三个使用 __except_handler3 作为回调函数的异常帧。第三个异常帧来自于 Visual C++ 的运行时库。Visual C++ 的运行时库的 CRT0.C 源代码显示对 main 或 WinMain 的调用被封装在了 _try/_except 块中。这个 _try 块的 filter-expression 代码在 WINXFLTR.C 文件中。

回到 ShowSEHFrames,最后一帧的 Handler:此行包含一个不同的地址,77F3AB6C。经过查找,就会发现这个地址是在 KERNEL32.DLL 里。这个特殊的帧是由 KERNEL32.DLL 的 BaseProcessStart 函数安装的,这个函数我在前面讲到过。

Unwinding

在深挖 unwinding 的实现代码之前,我们先来简要总结一下 unwinding 的含义。前面我曾提到异常处理程序信息是如何保存在链表里的,又是如何由线程信息块的第一个 DWORD (FS:[0])来指向的。因为某一异常的处理程序不一定是链表的头节点,这就需要有一种有序的方法来移除此实际处理程序之前链表中的所有异常处理程序。

正如在 Visual C++ 的 __except_handler3 函数中见到的,unwinding 是由 __global_unwind2 RTL 函数完成的。此函数是对未公开的 RtlUnwind API 的非常简单的封装:

__global_unwind2(void * pRegistFrame)
{
     _RtlUnwind( pRegistFrame,
                 &__ret_label,
                 0, 0 );
     __ret_label:
}

尽管 RtlUnwind 是实现编译器级 SEH 的关键的 API,但却没有公开。它是一个 KERNEL32 函数,Windows NT 将 KERNEL32.DLL 调用 forward 到了 NTDLL.DLL,在 NTDLL.DLL 里的也是一个 RtlUnwind 函数。我拼凑了这个函数的一些伪码,即 Figure 12 所示。

尽管 RtlUnwind 看起来很繁琐,但如果合理地划分一下还是不难理解的。此 API 首先从 FS:[4] 和 FS:[8] 取得线程堆栈的当前栈顶和栈底。这两个值对于后面的健壮性检查是很重要的,这里的健壮性检查就是保证所有被 unwound 的异常帧都落在堆栈的范围内。

接着,RtlUnwind 在堆栈上建立一个 EXCEPTION_RECORD,并将 ExceptionCode 域设为 STATUS_UNWIND。而且 EXCEPTION_RECORD 的 ExceptionFlags 域中的 EXCEPTION_UNWINDING 标志也要置位。指向此 EXCEPTION_RECORD 结构体的指针之后会作为参数传递给每一个异常回调函数。此后,代码调用 _RtlpCaptureContext 函数来创建一个 CONTEXT 结构体,此结构体也会作为异常回调的 unwind 调用的一个参数。RtlUnwind 后面的部分就遍历 EXCEPTION_REGISTRATION 结构体的链表。对于每一帧,代码调用 RtlpExecuteHandlerForUnwind 函数,后面会讲到此函数。正是这个函数用 EXCEPTION_UNWINDING 标志调用了异常回调函数。每次回调之后,相应的异常帧通过调用 RtlpUnlinkHandler 将其移除。当 RtlUnwind 到达第一个参数指定地址的帧时,就停止 unwinding 帧。这些代码中间还有许多用于错误检查的代码,这些代码保证了程序的正常执行。如果出现了问题,RtlUnwind 就会引起异常来告知所遇到的问题,而且此异常的 EXCEPTION_NONCONTINUABLE 标志是置位的。当此标志置位时,是不允许进程继续执行的,因此进程必须结束。

Unhandled Exceptions

本文前面部分我完整描述了 UnhandledExceptionFilter API。一般不用直接调用这个 API(尽管可以)。大多数情况下,它是由 KERNEL32 的默认异常回调的 filter-expression 代码来调用的。前面的 BaseProcessStart 伪码说明了这一点。

Figure 13 是我给出的 UnhandledExceptionFilter 的伪码。这个 API 的开头有些奇怪(至少在我看来是这样的)。若是一个 EXCEPTION_ACCESS_ VIOLATION 异常,代码就调用 _BasepCheckForReadOnlyResource。尽管我没有提供此函数的伪码,但我这里可以大概说一下。如果是因为向 EXE 或 DLL 的 resource section(.rsrc)进行写入而发生异常,_BasepCurrentTopLevelFilter 就修改引起异常的页的属性,从而允许写操作,UnhandledExceptionFilter 返回 EXCEPTION_ CONTINUE_EXECUTION,并重新执行引起异常的指令。

UnhandledExceptionFilter 的下一个任务是决定进程是否要在 Win32 调试器下运行。也就是说,进程是以 DEBUG_PROCESS 或 DEBUG_ONLY_THIS_PROCESS 标志创建的。UnhandledExceptionFilter 使用 NtQueryInformationProcess 函数来判断进程是否正在被调试。如果是,此 API 就返回 EXCEPTION_CONTINUE_SEARCH,说明系统的其它部分会唤醒调试器进程并告知调试器被调试进程引起了异常。如果有 user-installed unhandled exception filter,则调用它。一般都没有 user-installed 的回调,但是可以用 SetUnhandledExceptionFilter API 装一个。我提供了这个 API 的伪码。这个 API 只是用新的用户回调的地址来修改一个全局变量,然后返回旧回调的值。

做好准备工作后,UnhandledExceptionFilter 就可以进行其主要的工作:用那个老面孔的应用程序错误对话框来通知程序的错误。有两种办法可以避免此对话框的出现。第一种就是进程调用了 SetErrorMode 并设置了 SEM_NOGPFAULTERRORBOX 标志。另一种就是将 AeDebug 注册键值下的 Auto 值设为 1。这时,UnhandledExceptionFilter 略过程序错误对话框并自动启动由 AeDebug 键的 Debugger 值所指定的调试器。如果对“just in time debugging”比较熟悉的话,这就是操作系统对其的支持,之后还会讨论。

大多数情况下,这两种逃避此对话框的条件都为假,UnhandledExceptionFilter 就调用 NTDLL.DLL 函数中的 NtRaiseHardError 函数。正是这个函数唤出了程序错误对话框。这个对话框等待用户点击 OK 结束进程或 Cancel 调试进程。

若点击了 OK,UnhandledExceptionFilter 就返回 EXCEPTION_EXECUTE_HANDLER。调用 UnhandledExceptionFilter 的代码通常以结束自己来回应(就像 BaseProcessStart 代码中那样的)。这就带来一个有趣的问题。多数人认为系统没有处理异常而将进程结束。实际上更准确的说法是系统作了一些工作,这样未处理的异常使进程自己将自己结束。

如果点击了程序错误对话框的 Cancel 才执行了 UnhandledExceptionFilter 真正有意思的代码,这时会调试器加载引起异常的进程。在调试器 attach 到出错进程后,代码首先调用 CreateEvent 来创建一个事件以通知调试器。事件句柄和当前进程 ID 都要传递给 sprintf,sprintf 格式化启动调试器的命令行。万事俱备后,UnhandledExceptionFilter 调用 CreateProcess 来启动调试器。若 CreateProcess 成功,代码对前面创建的事件调用 NtWaitForSingleObject。此调用一直阻塞直到调试器进程通知此事件,指示调试器已经成功地 attach 到出错进程上。UnhandledExceptionFilter 还有其它的零星代码,但我这里只捡重要的说了说。

Into the Inferno

到了目前这个地步,如果还保留什么就太不公平了。我已经讲了发生异常时操作系统如何调用用户定义的函数;讲了一般回调的内部运行以及编译器如何使用它们来实现 _try 和 _catch;讲了没人处理异常时的情况以及系统对其的处理。所剩下的只有起初异常回调是从何处开始的。是的,我们来深入系统内幕来看看结构化异常处理的开始阶段。

Figure 14 所示为我为 KiUserExceptionDispatcher 和一些相关函数写的伪码。KiUserExceptionDispatcher 位于 NTDLL.DLL 中,它是异常发生后执行的起点。这样说也不是百分之百的准确。例如,在 Intel 体系下,异常会使控制转到一个 ring 0 (内核模式)的处理程序。此处理程序由对应此异常的中断描述符表表项所定义。我将跳过所有的内核模式代码并假设发生异常时 CPU 直接执行 KiUserExceptionDispatcher。

KiUserExceptionDispatcher 的关键就是对 RtlDispatchException 的调用。这个调用启动了对注册的异常处理程序的查找。如果处理程序处理了异常并继续执行,则对 RtlDispatchException 的调用不再返回。如果 RtlDispatchException 返回了,则有两种可能:要么调用了 NtContinue 使进程继续,要么就是产生了另一个异常。若是后者,异常就不能再继续了,进程必须结束。接着说  RtlDispatchExceptionCode,这就是遍历异常帧的代码。函数获得一个指向 EXCEPTION_REGISTRATIONs 链表的指针并遍历每一个节点查找处理程序。因为堆栈可能崩溃掉,这个函数非常谨慎。在调用每个 EXCEPTION_REGISTRATION 指定的处理程序之前,代码要保证在线程堆栈中 EXCEPTION_REGISTRATION 是 DWORD 对齐的且前面的 EXCEPTION_REGISTRATION 的地址高。

RtlDispatchException 并不直接调用 EXCEPTION_REGISTRATION 结构体中指定的地址,而是调用 RtlpExecuteHandlerForException 来做这个脏累活儿。根据 RtlpExecuteHandlerForException 内部发生的情况,RtlDispatchException 要么继续遍历异常帧要么产生另一个异常。这个二级异常指示异常回调函数中出现问题不能继续执行。RtlpExecuteHandlerForException 的代码和另一个函数 RtlpExecutehandlerForUnwind 紧密相关。我在前面讲 unwinding 时曾提到这个函数。这两个函数都在将控制送到 ExecuteHandler 函数之前用不同的值加载 EDX 寄存器。换种说法就是 RtlpExecuteHandlerForException 和 RtlpExecutehandlerForUnwind 是同一个 ExecuteHandler 函数的不同的前端。

ExecuteHandler就是 EXCEPTION_REGISTRATION 的 handler 域被取出和执行的地方。也许看上去有些奇怪,对异常回调函数的调用本身也被一个结构化异常处理程序封装了起来。在这里使用 SEH 尽管有点儿怪,但认真考虑一下还是合理的。如果异常回调引起了另一个异常,操作系统需要知道此事件。根据异常是发生在初始的回调还是 unwind 中的回调,ExecuteHandler 返回 DISPOSITION_NESTED_ EXCEPTION 或 DISPOSITION_COLLIDED_UNWIND。这两个可都是“红色警戒!立即关闭!”级别的代号。读者也许像我一样很难让所有的函数都与 SEH 直接关联。类似地,也很难记住谁调用了谁。为了帮助我自己,我画了个图即 Figure 15。

现在,在执行 ExecuteHandler 前设置 EDX 寄存器干什么呢?其实很简单。若调用用户的处理程序时出错,则不管 EDX 里是什么 ExecuteHandler 都会将其作为纯粹的异常处理程序。它将 EDX 寄存器压栈作为最小 EXCEPTION_REGISTRATION 结构体的 handler 域。本质上讲,ExecuteHandler 使用的纯粹的异常处理和我在 MYSEH 和 MYSEH2 程序里使用的差不多。

Conclusion

结构化异常处理是 Win32 的一个奇妙特性。多亏了像 Visual C++ 这样的编译器在它上面加上的支持层,一般的程序员才能用较少的学习代价而从 SEH 中受益。然而,在操作系统这一级,事情可就比 Win32 文档所讲的复杂多了。不幸的是,因为几乎所有的人都觉得系统级 SEH 是个很难的课题,所以至今没有什么这方面的文章。系统级细节方面的文档的缺乏状况一直未得改善。在本文中,我已经展示了系统级的 SEH 是围绕一个相对简单的回调函数展开的。如果理解了回调函数的本质,再在此基础上层曾构建其它的理解层次,系统级的结构化异常处理其实也没那么难掌握。
2012-8-5 18:46
0
雪    币: 60
活跃值: (25)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6
好贴,,,顶了。。。
2012-8-5 19:07
0
雪    币: 602
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
异常处理,标记 好文
2012-8-5 19:39
0
雪    币: 32
活跃值: (34)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
标记:

start:
assume        fs:nothing
push        offset _handle
push        fs:[0]
mov        fs:[0],esp

mov        eax,88
mov        edx,0
mov        ebx,0
;异常
yes:
div        ebx
invoke        MessageBox,0,0,0,MB_OK
jmp        ext0

;退出
ext0:
pop        fs:[0]
pop        eax

;异常处理
pushad
mov        esi,_lpexceptionrecord
mov        edi,lpcontext
assume        edi:ptr CONTEXT,esi:ptr EXCEPTION_RECORD
invoke        wsprintf,addr szbuffer,addr szform,[edi].regEip,[esi].ExceptionCode,[esi].ExceptionFlags
;invoke        MessageBox,0,addr szbuffer,0,MB_OK

;解决异常,并重回div
mov        [edi].regEbx,2
mov        [edi].regEip,offset yes

;清除断点
xor        ebx,ebx
mov        [edi].iDr0,ebx;断点寄存器
mov        [edi].iDr1,ebx;断点寄存器
mov        [edi].iDr2,ebx;断点寄存器
mov        [edi].iDr3,ebx;断点寄存器

assume        edi:nothing,esi:nothing
mov        eax,ExceptionContinueExecution

popad

在调试的时候,执行到div的时候,注意观看OD堆栈  会出现SEH的处理地址,对地址下断点,就可以跟踪seh对异常的处理过程.  上面是简写的一个测试。试想也可以把div换成检查flags  然后检查是否有断点,有的话.跳到误区或者清除断点。
2012-8-5 20:01
0
雪    币: 645
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
此文好文章啊 看完受益匪浅 多谢大牛
2013-1-26 11:01
0
雪    币: 332
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
这个不是写的不错的 ,大侠威武 MARK 一下
2013-1-26 13:08
0
游客
登录 | 注册 方可回帖
返回
//