上面的内容只是一些基础知识,虽然简单,但有必要了解一下。现在,我将正式开始我的第一个专题:结
构化异常处理(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