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

|