Windows 系统编程初探 (四)结构化异常处理之一:SEH的基本原理与进程相关异常处理

类别:编程语言 点击:0 评论:0 推荐:

        上面的内容只是一些基础知识,虽然简单,但有必要了解一下。现在,我将正式开始我的第一个专题:结

构化异常处理(SEH)。SEH 是 Windows 系统提供的功能,跟开发工具无关。值得一提的是,VC 将 SEH 进行

了封装,也就是我们平常用到的 __try{}__except(){} 和 __try{}__finally{},我没有研究过它的实现方法,这里也

不进行讨论,而我将要讲述的是 SEH 的手动实现,也就是 SEH 的本来面貌。

 1.SEH 的工作原理。

         Windows 程序设计中最重要的理念就是消息传递,事件驱动。当GUI应用程序触发一个消息时,系统将把

该消息放入消息队列,然后去查找并调用窗体的消息处理函数(CALLBACK),传递的参数当然就是这个消息。

我们同样可以把异常也当作是一种消息,应用程序发生异常时就触发了该消息并告知系统。系统接收后同样会

找它的“回调函数”,也就是我们的异常处理例程。当然,如果我们在程序中没有做异常处理的话,系统也不

会置之不理,它将弹出我们常见的应用程序错误框,然后结束该程序。所以,当我们改变思维方式,以

CALLBACK 的思想来看待 SEH,SEH 将不再神秘。

 2.进程相关异常处理。
 

        SEH 可分为进程相关和线程相关,我们先来了解进程相关的 SEH,所谓进程相关,就是说在应用程序的

任何地方发生的异常都可以(并不必须)用该处理例程来处理。 按照前面的思路,做异常处理就是设置一个回调

函数,可如何设置呢?Windows 为设置窗体回调函数提供了一个API:SetWindowLong(),它同样也为异常处

理提供了类似的API:SetUnhandledExceptionFilter(),传递给该函数的参数就是我们的异常处理例程。所以,

我们只需要编写一个函数,然后再程序开始的时候调用 SetUnhandledExceptionFilter()将它设置为异常处理函

数就OK了! 下一步,就是怎样编写异常处理函数了。首先,我们看一下异常处理函数的定义:
 
  long __stdcall ExceptionFilterProc(EXCEPTION_POINTERS *);
  
        返回值是 long;调用规则是 __stdcall;函数名无所谓,愿意怎么起都行;参数只是一个结构指针。所有

的都很简单,只有参数看起来陌生一点,那么我们先来观察一下参数,这个结构在 WINNT.H 中定义如下:

  
     typedef struct _EXCEPTION_POINTERS {
            PEXCEPTION_RECORD ExceptionRecord;
            PCONTEXT ContextRecord;
     }EXCEPTION_POINTERS;
 
 又嵌套了两个结构指针,呵呵!
 
 EXCEPTION_RECORD 结构定义:
 
      typedef struct _EXCEPTION_RECORD {
              DWORD ExceptionCode;
              DWORD ExceptionFlags;
              struct _EXCEPTION_RECORD *ExceptionRecord;
              PVOID ExceptionAddress; 
              DWORD NumberParameters; 
              DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    }EXCEPTION_RECORD, * PEXCEPTION_RECORD; 
 
这个结构有必要说明一下,内容比较多,没必要都记住,用到时翻出文档参考一下就行了。
 
 DWORD ExceptionCode;
 异常代码,指出异常原因。常见异常代码有:
 
  EXCEPTION_ACCESS_VIOLATION = C0000005h
  读写内存冲突
 
  EXCEPTION_INT_DIVIDE_BY_ZERO = C0000094h
  非法除0
 
  EXCEPTION_STACK_OVERFLOW = C00000FDh
  堆栈溢出或者越界
 
  EXCEPTION_GUARD_PAGE = 80000001h
  由Virtual Alloc建立起来的属性页冲突
 
 EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025h
  不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常

 EXCEPTION_INVALID_DISPOSITION = C0000026h
  在异常处理过程中系统使用的代码
 
 EXCEPTION_BREAKPOINT = 80000003h
  调试时因代码中 INT 3 中断
 
 EXCEPTION_SINGLE_STEP = 80000004h
  处于被单步调试状态(INT 1)

DWORD ExceptionFlags;
 异常标志
 = 0
  可修复异常
  
 EXCEPTION_NONCONTINUABLE = 1
  不可修复异常
  
 EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025H
  不可修复异常继续执行导致的异常 
 
struct _EXCEPTION_RECORD *ExceptionRecord;
 当异常处理程序中发生异常时,此字段被填充,否则为NULL

PVOID ExceptionAddress;
 发生异常的地址(EIP)
 

DWORD NumberParameters;
 规定与异常相关的参数数量(0-15),现在版本的Windows总是0
 

DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
 异常描述信息,目前只有 EXCEPTION_ACCESS_VIOLATION 异常有描述信息
 
 ExceptionInformation[0]
  描述导致异常的操作类型
  = 0 读异常
  = 1 写异常
  
 ExceptionInformation[1]
  发生读写异常的内存地址 
 

 CONTEXT 结构定义:
 
 typedef struct _CONTEXT{
  ...
 }CONTEXT, * PCONTEXT;
 
 这个结构非常庞大,这里就不一一罗列了,可以参看 WINNT.H,但我们必须清楚的一点是:CONTEXT 结构描述
的是异常发生时 CPU 中各个寄存器的状态。

再来看看返回值的意义,返回值可以有三个,分别是:
 EXCEPTION_EXECUTE_HANDLER  = 1   
  已经处理了异常,结束程序,这样程序将无疾而终。
 
 EXCEPTION_CONTINUE_SEARCH  = 0   
  不处理异常,转交系统处理,弹出常见的错误消息框。
 
 EXCEPTION_CONTINUE_EXECUTION = -1  
  修复错误,从异常发生处继续执行,最理想的做法,不过非常困难。

了解了这些之后,我们来看看一个异常处理函数的简单流程:
 1.C/C++ 写法 
 long WINAPI ExceptionFilter(EXCEPTION_POINTERS * lParam){
  ...
  return 1;//(0,-1)
 }
 
 2.ASM 写法 
 ExceptionFilter PROC
  ;取得参数
  MOV  ESI,DWORD PTR [ESP + 4]
  
  ;处理异常
  ...
  ...
  
  ;设置返回值,高级语言约定返回值存放于 EAX 中。
  MOV  EAX,_return_Value
  RET  4
 ExceptionFilter ENDP
 

        说了这么多,也当不住一个例子有说服力。下面,我将给出一个 ASM 写的例程,程序启动后,将生成两个

线程,主线程中将产生一个除0异常,子线程中将产生一个非法内存访问异常,异常处理程序会处理他们。仔细

研究一下吧!

 ;****************************************************************
 ;进程相关异常处理实例
 ;****************************************************************
 .386
 .MODEL FLAT
 
 ;包含常用结构的头文件,和 C/C++ 的 .H 类似
 include ..\INCLUDE\PERELATION.INC
 
 ;API 申明
 EXTRN MessageBoxA:PROC
 EXTRN CreateThread:PROC
 EXTRN VirtualProtect:PROC
 EXTRN WaitForSingleObject:PROC
 EXTRN CloseHandle:PROC
 EXTRN SetUnhandledExceptionFilter:PROC
 EXTRN ExitProcess:PROC
 
 ;数据定义
 .Data
  ddTemp  DD 0
  ddHandle DD 0
  ddThreadID DD 0
  
  szTitle   DB "提示",0
  szExcDivZero DB "应用程序发生除 0 错误",0
  szExcAccess  DB "应用程序发生非法内存访问错误,是否修复?",0
 
 ;代码开始(主线程)
 .Code
 _Header:
  PUSH EBP
  
  ;设置异常处理函数
  PUSH OFFSET ExceptionFilter
  CALL SetUnhandledExceptionFilter
  
  ;触发除 0 异常
  XOR  EBX,EBX
  DIV  BL
  
  ;*********************************************** 
  ;此间执行顺序将被打乱,进入异常处理例程
  ;***********************************************
  
  ;创建子线程
  PUSH OFFSET ddThreadID
  PUSH 0
  PUSH NULL
  PUSH OFFSET ThreadProc
  PUSH 0
  PUSH NULL
  CALL CreateThread
  
  ;创建线程失败
  TEST EAX,EAX
  JE  _Error_Exit
  
  ;保存线程句柄
  MOV  ddHandle,EAX
  
  ;等待子线程结束
  PUSH 0FFFFFFFFH
  PUSH EAX
  CALL WaitForSingleObject
  
  ;关闭线程句柄
  PUSH ddHandle
  CALL CloseHandle
  
 _Error_Exit:
  POP  EBP
  
  ;退出程序
  PUSH 0
  CALL ExitProcess
  


  ;********************************************************************
  ;代码段内定义的字符串。Windows 程序的代码段默认是不可写的,
  ;下面的线程函数将以尝试将该字符串按字翻转,从而导致非法内存
  ;访问异常。
  ;********************************************************************
  szMessage DB "落花人独立,微雨燕双飞。当时明月在,曾照彩云归。",0
  
 ;子线程函数体
 ThreadProc PROC
  PUSHAD
  
  ;**********************************************************
  ;这段指令将完成扫描 NULL-T 字符串长度的功能,
  ;估计是函数 strlen() 的原始码,十分精彩!
  ;指令说明(REPNE SCASB):
  ;EDI 寄存器指向字符串头,然后按子节与寄存器AL
  ;比较,相等则结束,每比较一个字符EDI将自加1,
  ;ECX 寄存器中设置扫描次数,也就是循环计数器,
  ;因为字符串长度不定,所以将ECX设置为0FFFFFFFF
  ;**********************************************************
  CLD
  XOR  EAX,EAX
  XOR  ECX,ECX
  DEC  ECX
  LEA  EDI,szMessage
  REPNE SCASB
  
  ;ECX取反则得到字符串长度(包含0)
  NOT  ECX
  ;回复EDI到字符串头
  SUB  EDI,ECX
  
  ;按字翻转字符串,0保留在末尾
  XOR  EBX,EBX
  DEC  ECX
  
 _Rever_Loop:
  DEC  ECX
  DEC  ECX
  
  CMP  EBX,ECX
  JGE  _Rever_Over
  
  ;分别读取头、尾的两个字
  MOV  AX,WORD PTR [EDI + EBX]
  MOV  DX,WORD PTR [EDI + ECX]
  
  ;翻转写入,本条指令将导致非法内存访问异常
  MOV  WORD PTR [EDI + ECX],AX
  MOV  WORD PTR [EDI + EBX],DX
  
  INC  EBX
  INC  EBX
  
  JMP  _Rever_Loop
 _Rever_Over:
 
  ;反转完成,显示反转后的字符串
  PUSH MB_OK
  PUSH OFFSET szTitle
  PUSH OFFSET szMessage
  PUSH NULL
  CALL MessageBoxA
 
  POPAD
  RET  4
 ThreadProc ENDP
 
 ;异常处理函数
 ExceptionFilter PROC
  ;从栈中取得参数 EXCEPTION_POINTERS *
  ;此时栈的状态是:
  ;[ESP + 4] EXCEPTION_POINTERS *
  ;[ESP]  Return address
  MOV  EAX,DWORD PTR [ESP + 4]
  PUSHAD
  
  ;PEXCEPTION_RECORD => ESI
  MOV  ESI,[EAX].ExceptionRecord
  
  ;PCONTEXT => EDI
  MOV  EDI,[EAX].ContextRecord
  
  ;取异常代码
  MOV  EAX,[ESI].ExceptionCode
  
  ;是否非法除0异常
  CMP  EAX,0C0000094H
  JE  _IsDivZero
  
  ;是否非法内存访问异常
  CMP  EAX,0C0000005H
  JE  _IsAccessViolation
  
  ;其它异常
  JMP  _ExceptOther
  
  ;除 0 异常处理
 _IsDivZero:
  ;MessageBox 提示一下
  PUSH MB_OK
  PUSH OFFSET szTitle
  PUSH OFFSET szExcDivZero
  PUSH NULL
  CALL MessageBoxA
  
  ;*********************************************************
  ;修复方法:前面代码中,我们以BL作为除数,要修
  ;复异常,只需BL != 0 就可以了,所以这里改变寄
  ;存器EBX的值,从而达到修复的目的。
  ;这里使用到了结构 CONTEXT,注意一下。
  ;*********************************************************
  INC  [EDI].C_Ebx
  JMP  _Filter_Exit
  
  ;非法内存访问错误
 _IsAccessViolation:
  ;消息提示是否修复
  PUSH MB_YESNOCANCEL
  PUSH OFFSET szTitle
  PUSH OFFSET szExcAccess
  PUSH NULL
  CALL MessageBoxA
  
  ;选择“YES”则修复异常
  CMP  EAX,IDYES
  JE  _FixException
  
  ;选择“NO”则不修复,结束进程
  CMP  EAX,IDNO
  JE  _ExceptOther
  
  ;选择“CANCEL”则转交系统处理,会弹出错误框
  ;返回值 EAX = 0
  POPAD
  XOR  EAX,EAX
  RET  4
  
  ;修非法复内存访问异常
 _FixException:
  ;*****************************************************************
  ;修复方法:调用函数 VirtualProtect 更改内存的
  ;址的保护属性,让它可写(PAGE_EXECUTE_READWRITE)
  ;*****************************************************************
  MOV  EAX,[ESI].ExceptionInformation[4]
  
  PUSH OFFSET ddTemp
  PUSH PAGE_EXECUTE_READWRITE
  PUSH 01000H
  PUSH EAX
  CALL VirtualProtect
  
  TEST EAX,EAX
  JNE  _Filter_Exit
  
  ;如果发生其他异常,直接退出程序
 _ExceptOther:
 
  ;返回值 EAX = 1
  POPAD
  XOR  EAX,EAX
  INC  EAX
  RET  4
  
  ;异常修复,继续执行
 _Filter_Exit:
 
  ;返回值 EAX = -1
  POPAD
  XOR  EAX,EAX
  DEC  EAX 
  RET  4
 ExceptionFilter ENDP
 
 END  _Header

本文地址:http://com.8s8s.com/it/it22326.htm