📄 014.txt
字号:
14.1 异常处理的用途
对程序执行中的异常(Exception)大家都不陌生,先来回顾一下DOS操作系统对异常的处理方法。在DOS操作系统中,当操作系统在运行中发生“异常”时,会去调用INT 24h中断,系统默认的INT 24h中断处理程序会将出错代码翻译成文本信息显示在屏幕上,然后让用户选择“Ignore”,“Retry”,“Fail”或者“Abort”,并根据用户的输入结果来选择不同的操作——忽略异常、重试产生异常的操作或者强行终止程序的执行。
系统默认的异常处理方法有时候不是很合适,比如对于在图形方式下运行的DOS程序来说,系统显示的出错信息会破坏屏幕的美观;另外,程序可能希望不要向用户提供“Abort”选项来保证程序不会因为一些微不足道的错误而被终止。为了处理这些情况,程序可以用自定义的异常处理程序来替换系统默认的处理程序,而这是很容易实现的——由于DOS系统在检测到异常的时候仅仅去调用24h号中断并根据中断的返回值决定下一步的处理方式,只要应用程序截获INT 24h中断,就可以在自己提供的中断服务程序中按照自己的意图决定系统处理异常的走向。
Windows操作系统对异常的处理流程相对比较复杂,与DOS操作系统相比,最大的区别在于DOS的异常处理是被动的,一般仅用来处理操作系统内部的异常,对于其他层次的异常是无法处理的,比如使用INT 21h去读盘的时候发生错误会激发INT 24h中断,但在BIOS服务程序级别用INT 13h去读盘时发生错误就不会激发INT 24h中断,对应用程序胡作非为引发的异常更是束手无策;而Windows的异常处理机制是依靠80x86处理器的保护机制来主动捕获异常,所以Win32下异常处理程序的用途不仅仅局限于防止程序被Windows野蛮地终止,合理利用它们可以让有些功能的实现方式变得更加简单,一般来说,可以在下面这些情况下使用异常处理程序:
● 用来处理非致命的错误
程序执行中发生某些异常时只需要终止发生异常的模块(或子程序),并没有必要终止整个程序的运行,这时可以在异常处理程序中指定让程序转移到一个“安全”的地方去执行,并在这里完成资源释放、删除临时文件、显示错误提示等扫尾工作后从出错模块返回。
● 处理“计划内”的异常
程序中的有些功能本来就是设计在异常处理模块中实现的。Windows系统中虚拟内存的实现就是一个绝好的例子(如图1.5所示),第10章中介绍的内存映射文件也是以同样的方法实现的。在这些情况下,“异常”是作为一个触发条件使用的。
另外,在Windows API中使用异常处理程序进行参数的合法性检测也是很常见的。一般来说,大部分子程序都需要对输入的参数进行合法性检测,特别是对于指针类型的参数,但是当参数涉及的数据结构太复杂的时候,合法性检测会大大降低程序的效率,这时可以假定参数全部合法并尝试直接使用这些参数,如果异常处理程序没有捕获到错误,那么表示参数是合法的,这样要比在每个步骤中检测参数(或操作结果)的合法性要简洁得多。
● 处理致命错误
虽然捕获到致命错误的时候终止程序是最好的选择,但是程序在退出之前,可以在异常处理程序中进行释放资源、删除临时文件等操作,甚至可以详细记录产生异常的指令位置和环境,以便用来分析产生异常的原因。
显然,Windows中的用户自定义的异常处理函数不会再以INT 24h的方式被调用,读者也可以猜到它必定会以“回调函数”的方式来实现,但是如何写回调函数,回调函数的参数是什么,在什么地方定义回调函数呢?接下来将介绍这些内容。
14.2 使用筛选器处理异常(1)
Windows下的异常处理可以有两种方式:筛选器异常处理和SEH异常处理。
筛选器异常处理的方式是由程序指定一个异常处理回调函数(在下面将统一简称为“回调函数”),当发生异常的时候,系统将调用这个回调函数,并根据回调函数的返回值决定如何进行下一步操作,这种方法和DOS系统中使用INT 24h中断来处理异常的方法是很像的。
在进程范围内,筛选器异常处理回调函数是惟一的,设置了一个新的回调函数后,原来的就失效了。
14.2.1 注册回调函数
可以使用SetUnhandledExceptionFilter函数来设置一个筛选器异常处理回调函数,准确地讲,这个回调函数不是替换了系统默认的异常处理程序,而是在它前面进行了一些预处理,操作的结果还是会被送到系统默认的异常处理程序中去,这个过程就相当于对异常进行一次“筛选”,这正是函数名中“Filter”一词的含义。
SetUnhandledExceptionFilter函数的使用方法是:
invoke SetUnhandledExceptionFilter,offset _Handler
mov lpPrevHandler,eax
函数的惟一参数是回调函数的地址,如果地址参数被指定为NULL的话,那么系统将去掉这个“筛子”而直接将异常送往系统默认的异常处理程序。函数的返回值是上一次设置的回调函数的入口地址,如果原来没有安装“筛子”,那么返回值将为NULL。
如果原来已经设置了一个回调函数的话,那么新的回调函数将换掉原来的回调函数,注意:不是在原先的回调函数前面再挂上一个新的回调函数。也就是说,当异常发生的时候,系统调用新的回调函数,在这个函数返回的时候并不会再去调用上一次设置的回调函数。一个形象的比喻就是Windows系统不会用两层筛子去筛东西。
本书所附光盘的Chapter14\TopHandler目录中有一个简单的筛选器异常处理的例子,其中的汇编源代码如下:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
lpOldHandler dd ?
.const
szMsg db ~异常发生位置:%08X,异常代码:%08X,标志:%08X~,0
szSafe db ~回到了安全的地方!~,0
szCaption db ~筛选器异常处理的例子~,0
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Exception Handler 异常处理程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Handler proc _lpExceptionPoint
local @szBuffer[256]:byte
pushad
mov esi,_lpExceptionPoint
assume esi:ptr EXCEPTION_POINTERS
mov edi,[esi].ContextRecord
mov esi,[esi].pExceptionRecord
assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
invoke wsprintf,addr @szBuffer,addr szMsg,\
[edi].regEip,[esi].ExceptionCode,[esi].ExceptionFlags
invoke MessageBox,NULL,addr @szBuffer,NULL,MB_OK
mov [edi].regEip,offset _SafePlace
assume esi:nothing,edi:nothing
popad
mov eax,EXCEPTION_CONTINUE_EXECUTION
ret
_Handler endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke SetUnhandledExceptionFilter,addr _Handler
mov lpOldHandler,eax
;********************************************************************
; 会引发异常的指令
;********************************************************************
xor eax,eax
mov dword ptr [eax],0 ;产生异常,然后_Handler被调用
; ...
; 如果这中间有指令,这些指令将不会被执行!
; ...
_SafePlace:
invoke MessageBox,NULL,addr szSafe,addr szCaption,MB_OK
invoke SetUnhandledExceptionFilter,lpOldHandler
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
程序的入口处使用SetUnhandledExceptionFilter函数将_Handler子程序指定为异常处理回调函数,函数返回的原回调函数地址被保存到lpOldHandler变量中(当然,在这个例子中,这个值肯定为0,程序中进行这一步操作是为了演示保存和恢复的方法),在程序退出之前会再次使用SetUnhandledExceptionFilter函数将这个地址设置回去。
在设置好回调函数后,程序人为地产生一个读异常:在xor eax,eax指令将eax清零以后,mov dword ptr [eax],0指令将导致读写0地址处的内存,而这是不允许的,所以这条指令执行时会产生一个异常,系统将捕获到它并调用程序设置的_Handler子程序来进行预处理。在_Handler子程序中,将跳过产生异常的指令并将程序转移到由_SafePlace标号指定的“安全”位置去执行。
14.2.2 异常处理回调函数
筛选器异常处理回调函数的格式如下所示:
_Handler proc l pExceptionInfo
回调函数带一个参数,这个参数是个指针,指向一个包含所发生异常详细信息的EXCEPTION_POINTERS结构,结构定义如下:
EXCEPTION_POINTERS STRUCT
pExceptionRecord DWORD ?
ContextRecord DWORD ?
EXCEPTION_POINTERS ENDS
要处理一个异常,必须详细了解这个异常的各种信息,EXCEPTION_POINTERS结构中包含的正是这些内容,其中的pExceptionRecord字段指向一个EXCEPTION_RECORD结构,这个结构中包含了异常产生的原因、产生的位置等情况,而ContextRecord字段指向一个CONTEXT结构,结构中记录了异常产生时刻的运行环境。这两个结构的定义在13.3.3小节中介绍调试API的时候介绍过。
14.2 使用筛选器处理异常(2)
1. 获取产生异常的原因
重新来看一下EXCEPTION_RECORD结构的定义:
EXCEPTION_RECORD STRUCT
ExceptionCode DWORD ? ;异常事件码
ExceptionFlags DWORD ? ;标志
PexceptionRecord DWORD ? ;下一个EXCEPTION_RECORD结构地址
ExceptionAddress DWORD ?
NumberParameters DWORD ?
ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORD ENDS
结构中的ExceptionCode字段定义了产生异常的原因,这些原因已经被预定义为一系列以EXCEPTION_开头或者以STATUS_开头的常量,读者可以在源程序中直接使用这些预定义值。表14.1中列出了一些最常用的异常原因。除了表中列出的原因之外,系统中还定义了许多其他异常原因代码,由于MASM32软件包中所带的Windows.inc文件中的定义值也不是很详尽,为此笔者整理了一份详细的异常原因代码,放在本书所附光盘的Chapter14\Exception.inc文件中,注意:文件中定义的代码都是以STATUS_开头的,它们的定义值与以EXCEPTION_开头的一样。
表14.1 异常原因代码的列表
异 常 原 因
对 应 值
说 明
EXCEPTION_ACCESS_VIOLATION
0C0000005h
尝试读写没有可读写属性的地址
EXCEPTION_BREAKPOINT
080000003h
断点异常(遇到INT 3指令)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -