WIN32汇编语言教程:第14章 异常处理 · 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指令) |
EXCEPTION_ILLEGAL_INSTRUCTION | 0C000001Dh | 遇到无效指令 |
EXCEPTION_IN_PAGE_ERROR | 0C0000006h | 存取不存在的内存页面 |
EXCEPTION_INT_DIVIDE_BY_ZERO | 0C0000094h | 除零错误 |
EXCEPTION_SINGLE_STEP | 080000004h | 单步中断 |
EXCEPTION_STACK_OVERFLOW | 0C00000FDh | 堆栈溢出 |
EXCEPTION_UNWIND | 0C0000027h | 展开操作 |
例子中的mov dword ptr [eax],0指令去写一个没有写权限的地址,所以会引发一个EXCEPTION_ACCESS_VIOLATION异常,从例子程序运行后显示的消息中可以验证这点,读者也可以尝试将这条指令修改为各式各样的错误指令,看看它们引发的异常对应哪个异常原因代码。
异常原因代码的含义是按照数据位划分的,其规则如表14.2所示。
表14.2 异常原因代码各数据位的含义
数 据 位 | 含 义 | 取 值 说 明 |
位31~30 | 严重性系数 | 00=成功,01=信息,10=警告,11=错误 |
位29 | 定义者 | 0=Microsoft定义,1=客户应用程序定义 |
位28 | 保留位 | 必须为0 |
位27~16 | 设备代码 | 表示引发异常的位置 |
位15~0 | 异常代码 | 用来分辨产生异常的原因 |
其中的位27~16定义了设备代码,用来表示异常代码发生在哪个特定的设备中,当前已经定义的设备代码如表14.3所示。
表14.3 异常原因代码中的设备代码定义
设 备 代 码 | 取 值 | 设 备 代 码 | 取 值 |
FACILITY_NULL | FACILITY_CONTROL | 10 | |
FACILITY_RPC | 1 | FACILITY_CERT | 11 |
FACILITY_DISPATCH | 2 | FACILITY_INTERNET | 12 |
FACILITY_STORAGE | 3 | FACILITY_MEDIASERVER | 13 |
FACILITY_ITF | 4 | FACILITY_MSMQ | 14 |
FACILITY_WIN32 | 7 | FACILITY_SETUPAPI | 15 |
FACILITY_WINDOWS | 8 | FACILITY_SCARD | 16 |
FACILITY_SECURITY | 9 | FACILITY_COMPLUS | 17 |
举例来讲,断点异常和单步中断并不属于程序错误,所以这两种异常代码中的严重性系数10,表示属于警告信息而非错误,但是遇到内存越权访问、除零错误等时候,这两位的值就是11了,表示这个错误让线程无法继续执行。
EXCEPTION_RECORD结构中的ExceptionCode字段定义了异常标志,它由一系列的数据位构成,定义如下:
● 位0——代表发生的异常是否允许被恢复执行。当位0被复位的时候,表示回调函数在对异常进行处理后可以指定让程序继续运行,置位的时候表示这个异常是不可恢复的,这时程序最好进行退出前的扫尾工作并选择终止程序,如果这时非要指定让程序继续执行的话,Windows会再次以EXCEPTION_NONCONTINUABLE_EXCEPTION异常代码调用回调函数。为了程序的可读性,可以通过两个预定义值EXCEPTION_CONTINUABLE(定义为0)和EXCEPTION_NONCONTINUABLE(定义为1)来测试这个标志位。
● 位1——EXCEPTION_UNWINDING标志。表示回调函数被调用的原因是进行展开操作(详见14.3.4小节)。
● 位2——EXCEPTION_UNWINDING_FOR_EXIT标志。表示回调函数被调用的原因是进行最终退出前的展开操作。
当处理异常的代码设计得不完善而在运行中引发新的异常时,回调函数会被嵌套调用。在这种情况下,EXCEPTION_RECORD结构中的pExceptionRecord字段会指向下一个EXCEPTION_RECORD结构,这条EXCEPTION_RECORD结构链定义了嵌套发生的多个异常的情况;如果没有嵌套的异常,pExceptionRecord的值为NULL。
结构中的ExceptionAddress字段定义了引发异常的指令的地址。
有了这些信息后,回调函数就可以根据类型来对不同的异常进行不同的处理,比如,对那些“计划内”的异常执行功能性的代码,而发生其他非致命异常的时候转到“安全”位置去执行。
2. 修正错误
在筛选器异常处理回调函数被系统调用的时候,参数中指定的EXCEPTION_POINTERS结构中的ContextRecord字段指向一个CONTEXT结构,这个结构中保存了异常发生时刻的运行环境,也就是所有寄存器的值。程序也可以通过这个结构中的regEip字段来得知异常发生的位置。在例子中,程序用一个对话框显示出异常代码、异常标志和CONTEXT结构中的EIP值,对比一下就可以发现,显示的EIP值正是那句产生异常的mov [eax],0指令的地址。
在第12章介绍多线程时,已经介绍过操作系统为每个线程保存单独的寄存器环境和单独的堆栈,那么当异常发生的时候,CONTEXT结构指出的环境会对应哪个线程的环境呢?其实答案很简单:Windows将会在产生异常的线程中运行回调函数,CONTEXT结构对应的是出错线程的环境,回调函数使用的堆栈也是这个线程的堆栈,在这样的安排下,回调函数才可能去修复线程中的错误。
修正错误的操作反映在对这个CONTEXT结构的修改上,当回调函数修改了结构中的值并返回后,系统会将线程的运行环境设置为新的值,所以要修正某个寄存器中的错误取值,只要修改这个CONTEXT结构就可以了。在例子中,异常处理程序将regEip字段的值修改 为_SafePlace标号的地址,这样线程恢复运行时是从_SafePlace标号的地方开始执行的。当然,这样处理后,在错误指令和_SafePlace标号之间的其他指令就不会被执行了。
3. 回调函数的返回值
回调函数返回后,Windows执行默认的异常处理程序,这个程序会根据回调函数的返回值决定如何进行下一步动作。
回调函数的返回值可以有3种取值:EXCEPTION_EXECUTE_HANDLER(定义为1)、EXCEPTION_CONTINUE_SEARCH(定义为0)和EXCEPTION_CONTINUE_EXECUTION(定义为-1)。
当返回值是EXCEPTION_EXECUTE_HANDLER时,进程将被终止,但是在终止之前系统不会显示出错提示对话框;当返回值是EXCEPTION_CONTINUE_SEARCH时,系统同样将终止程序的执行,但是在终止前会首先显示出错提示对话框。使用这两种返回值的时候,异常处理程序完成的工作一般是退出前的扫尾工作。
而返回值是EXCEPTION_CONTINUE_EXECUTION时,系统将CONTEXT设置回去并继续执行程序,例子程序中就是这样使用的。
当异常标志中包含EXCEPTION_NONCONTINUABLE标志位时,不应该使用EXCEPTION_CONTINUE_EXECUTION作为返回值,这样只会引发一个新的异常。(例子程序中为了简化代码,没有判断并处理异常标志为EXCEPTION_NONCONTINUABLE的情况)
上页:第14章 异常处理 · 14.2 使用筛选器处理异常(1) 下页:第14章 异常处理 · 14.3 使用SEH处理异常(1)