结构化异常处理

时间:2022-12-15 14:04:12

结构化异常处理( S E H)

S E H实际包含两个主要功能:结束处理( termination handling)和异常处理( e x c e p t i o nh a n d l i n g)。本章讨论结束处理,下一章讨论异常处理。

一个结束处理程序能够确保去调用和执行一个代码块(结束处理程序,termination handler),而不管另外一段代码(保护体, guarded body)是如何退出的。结束处理程序的文法结构(使用微软的Visual C++编译程序)如下:

__try
{
//Guarded body
...
}

__finally
{
//Termination handler
...
}

__try和__finally关键字用来标出结束处理程序两段代码的轮廓。在上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的- - f i n a l l y代码块能够被执行,不管保护体( t r y块)是如何退出的。不论你在保护体中使用r e t u r n,还是g o t o,或者是l o n g j u m p,结束处理程序(f i n a l l y块)都将被调用。下面将通过几个例子来说明这一点。

通过使用结束处理程序,可以避免r e t u r n语句的过早执行。当r e t u r n语句试图退出t r y块时,编译程序要确保f i n a l l y块中的代码首先被执行。要保证f i n a l l y块中的代码在t r y块中的r e t u r n语句退出之前执行。

23.12 关于finally块的说明

我们已经明确区分了强制执行f i n a l l y块的两种情况:

• 从t r y块进入f i n a l l y块的正常控制流。

• 局部展开:从t r y块的过早退出(g o t o、l o n g j u m p、c o n t i n u e、b r e a k、r e t u r n等)强制控制转移到f i n a l l y块。

第三种情况,全局展开( global unwind),在发生的时候没有明显的标识,我们在本章前面F u n c f u r t e r 1函数中已经见到。在F u n c f u r t e r 1的t r y块中,有一个对F u n c i n a t o r函数的调用。如果F u n c i n a t o r函数引起一个内存访问违规( memory access violation ),一个全局展开会使F u n c f u r t e r 1的f i n a l l y块执行。下一章将详细讨论全局展开。

由于以上三种情况中某一种的结果而导致f i n a l l y块中的代码开始执行。为了确定是哪一种情况引起f i n a l l y块执行,可以调用内部函数(或内蕴函数,intrinsic function)Abnormal Te r m i n a t i o n:

BOOL AbnormalTermination();

这个内部函数只在f i n a l l y块中调用,返回一个B o o l e a n值。指出与f i n a l l y块相结合的t r y块是否过早退出。换句话说,如果控制流离开t r y块并自然进入f i n a l l y块,A b n o r m a l Te r m i n a t i o n将返回FA L S E。如果控制流非正常退出t r y块—通常由于g o t o、r e t u r n、b r e a k或c o n t i n u e语句引起的局部展开,或由于内存访问违规或其他异常引起的全局展开—对A b n o r m a l Te r m i n a t i o n的调用将返回T R U E。没有办法区别f i n a l l y块的执行是由于全局展开还是由于局部展开。但这通常不会成为问题,因为可以避免编写执行局部展开的代码。

注意内部函数是编译程序识别的一种特殊函数。编译程序为内部函数产生内联(i n l i n e)代码而不是生成调用函数的代码。例如, m e m c p y是一个内部函数(如果指定/ O i编译程序开关)。当编译程序看到一个对m e m c p y的调用,它直接将m e m c p y的代码插入调用m e m c p y的函数中,而不是生成一个对m e m c p y函数的调用。其作用是代码的长度增加了,但执行速度加快了。

第24章 异常处理程序和软件异常

异常是我们不希望有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误还是常常会发生的。C P U负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。C P U引发的异常,就是所谓的硬件异常( hardware exception)。在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常( software exception)

当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。下面就是异常处理程序的文法:

__try 
{
//Guarded body

...

}
__except(exception filter)
{
// Exception handler

...

}

注意__e x c e p t关键字。每当你建立一个t r y块,它必须跟随一个f i n a l l y块或一个e x c e p t块。一个try 块之后不能既有f i n a l l y块又有e x c e p t块。但可以在t r y - e x c e p t块中嵌套t r y - f i n a l l y块,反过来也可以。

异常过滤器( exception filter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。下面几节的内容举例说明t r y - e x c e p t块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。

表24-1 标识符及其定义

标识符 定义为
E X C E P T I O N _ E X E C U T E _ H A N D L E R 1
E X C E P T I O N _ C O N T I N U E _ S E A R C H 0
E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N -1

 

24.2 EXCEPTION_EXECUTE_HANDLER

在F u n c m e i s t e r 2中,异常过滤器表达式的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开(本章后面将讨论),然后执行向e x c e p t块中代码(异常处理程序代码)的跳转。在e x c e p t块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使Wi n d o w s应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。

但是,当e x c e p t块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能性。

第一种可能性是从产生异常的C P U指令之后恢复执行。在F u n c m e i s t e r 2中执行将从对d w Te m p加1 0的指令开始恢复。这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的指令不能够继续成功地执行。

在F u n c m e i s t e r 2中,代码可以继续正常执行,但是, F u n c m e i s t e r 2已不是正常的情况。代码应该尽可能地结构化,这样,在产生异常的指令之后的C P U指令有望获得有效的返回值。例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操作。如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。

这里是另外一个例子,说明为什么在一个失败的C P U指令之后,执行不能够继续。我们用下面的程序行来替代F u n c m e i s t e r 2中产生异常的C语句:

malloc(5 / dwTemp);

对上面的程序行,编译程序产生C P U指令来执行除法,将结果压入栈中,并调用m a l l o c函数。如果除法失败,代码就不能继续执行。系统必须向栈中压东西,否则,栈就被破坏了。

所幸的是,微软没有让系统从产生异常的指令之后恢复指令的执行。这种决策使我们免于面对上面的问题。

第二种可能性是从产生异常的指令恢复执行。这是很有意思的可能性。如果在e x c e p t块中有这样的语句会怎么样呢:

dwTemp = 2;

在e x c e p t块中有了这个赋值语句,可以从产生异常的指令恢复执行。这一次,将用2来除5,执行将继续,不会产生其他的异常。可以做些修改,让系统重新执行产生异常的指令。你会发现这种方法将导致某些微妙的行为。我们将在“ EXCEPTION_ CONTINUE_EXECUTION”一节中讨论这种技术。

第三种可能性是从e x c e p t块之后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为E X C E P T I O N _ E X E C U T E _ H A N D L E R时所发生的事。在e x c e p t块中的代码结束执行后,控制从e x c e p t块之后的第一条指令恢复。

  1. If an exception occurs during execution of the guarded section or in any routine the guarded section calls, the __except expression is evaluated and the value determines how the exception is handled. There are three values:

    EXCEPTION_CONTINUE_EXECUTION (–1)   Exception is dismissed. Continue execution at the point where the exception occurred.

    EXCEPTION_CONTINUE_SEARCH (0)   Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence.

    EXCEPTION_EXECUTE_HANDLER (1)   Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the first statement after the __except compound statement

 

24.3 EXCEPTION_CONTINUE_EXECUTION

我们再仔细考察一下异常过滤器,看它是如何计算出定义在E x c p t . h中的三个异常标识符之一的。在“F u n c m e i s t e r 2”一节中,为简单起见,在过滤器里直接硬编码了标识符E X C E P T I O N _E X E C U T E _ H A N D L E R,但实际上可以让过滤器调用一个函数确定应该返回哪一个标识符。这里是另外一个例子。

char g_szBuffer[100];

void FunclinRoosevelt1()
{
int x = 0;
char *pchBuffer = NULL;

__try
{
*pchBuffer = 'J';
x = 5 / x;
}
__except(OilFilter1(&pchBuffer))
{
MessageBox(NULL, "An exception occurred", NULL, MB_OK);
}
MessageBox(NULL, "Function completed", NULL, MB_OK);
}

LONG OilFilter1(char **ppchBuffer)
{
if(*ppchBuffer == NULL)
{
*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);
}
return(EXCEPTION_EXECUTE_HANDLER);
}
这里,首先遇到的问题是在我们试图向p c h B u ff e r所指向的缓冲区中放入一个字母‘ J’时发生的。因为这里没有初始化p c h B u ff e r,使它指向全局缓冲区g _ s z B u ff u r。p c h B u ff e r实际指向N U L L。C P U将产生一个异常,并计算与异常发生的t r y块相关联的e x c e p t块的异常过滤器。在e x c e p t块中,对O i l F i l t e r函数传递了p c h B u ff e r变量的地址。

当O i l F i l t e r获得控制时,它要查看* p p c h B u ff e r是不是N U L L,如果是,把它设置成指向全局缓冲区g _ s z B u ff e r。然后这个过滤器返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。当系统看到过滤器的值是E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N时,系统跳回到产生异常的指令,试图再执行一次。这一次,指令将执行成功,字母‘ J’将放在g _ s z B u ff e r的第一个字节。

随着代码继续执行,我们又在t r y块中碰到除以0的问题。系统又要计算过滤器的值。这一次,O i l F i l t e r看到* p p c h B u ff e r不是N U L L,就返回E X C E P T I O N _ E X E C U T E _ H A N D L E R,这是告诉系统去执行e x c e p t块中的代码。这会显示一个消息框,用文本串报告发生了异常。

如你所见,在异常过滤器中可以做很多的事情。当然过滤器必须返回三个异常标识符之一,但可以执行任何其他你想执行的任务。

24.4 EXCEPTION_CONTINUE_SEARCH

char g_szBuffer[100];

void FunclinRoosevelt3()
{
char *pchBuffer = NULL;

__try
{
FuncAtude3(pchBuffer);
}
__except(OilFilter3(&pchBuffer))
{
MessageBox(…);
}
}

void FuncAtude3(char *sz)
{
__try
{
*sz = 0;
}
__except(EXCEPTION_CONTINUE_SEARCH)
{
// This never executes.

...

}
}

LONG OilFilter3(char **ppchBuffer)
{
if(*ppchBuffer == NULL)
{
*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);
}
return(EXCEPTION_EXECUTE_HANDLER);
}

现在,当FuncAtude 3试图向地址N U L L里存放0时,会引发一个异常。但这时将执行F u n c A t u d e 3 的异常过滤器。F u n c A t u d e 3的异常过滤器很简单,只是取值E X C E P T I O N _CONTINUE_ SEARCH。这个标识符是告诉系统去查找前面与一个e x c e p t块相匹配的t r y块,并调用这个t r y块的异常处理器。

因为F u n c A t u d e 3的过滤器的值为E X C E P T I O N _ C O N T I N U E _ S E A R C H,系统将查找前面的t r y块(在F u n c l i n R o o s e v e l t 3里),并计算其异常过滤器的值,这里异常过滤器是O i l F i l t e r 3。O i l F i l t e r 3看到p c h B u ff e r是N U L L,将p c h B u ff e r设定为指向全局缓冲区,然后告诉系统恢复执行产生异常的指令。这将使F u n c A t u d e 3的t r y块中的代码执行,但不幸的是, F u n c A t u d e 3的局部变量s z没有变化,恢复执行失败的指令只是产生另一个异常。这样,又造成死循环。

前面说过,系统要查找最近执行的与e x c e p t块相匹配的t r y块,并计算它的过滤器值。这就是说,系统在查找过程当中,将略过那些与f i n a l l y块相匹配而不是与e x c e p t块相匹配的t r y块。这样做的理由很明显: f i n a l l y块没有异常过滤器,系统没有什么要计算的。如果前面例子中F u n c A t u d e 3包含一个f i n a l l y块而不是e x c e p t块,系统将在一开始就通过F u n c l i n R o o s e v e l t 3的O i l F i l t e r 3计算异常过滤器的值。

24.5 GetExceptionCode

一个异常过滤器在确定要返回什么值之前,必须分析具体情况。例如,异常处理程序可能知道发生了除以0引起的异常时该怎么做,但是不知道该如何处理一个内存存取异常。异常过滤器负责检查实际情况并返回适当的值。

下面的代码举例说明了一种方法,指出所发生异常的类别:

__try 
{
x = 0;
y = 4 / x;
}

__except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// Handle divide by zero exception.
}

内部函数G e t E x c e p t i o n C o d e返回一个值,该值指出所发生异常的种类:

DWORD GetExceptionCode();

下面列出所有预定义的异常和相应的含意,这些内容取自Platform SDK文档。这些异常标识符可以在Wi n B a s e . h文件中找到。我们对这些异常做了分类。

1. 与内存有关的异常

• E X C E P T I O N _ A C C E S S _ V I O L AT I O N。线程试图对一个虚地址进行读或写,但没有做适当的存取。这是最常见的异常。

• E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T。线程试图读或写不支持对齐( a l i g n m e n t)的硬件上的未对齐的数据。例如, 1 6位数值必须对齐在2字节边界上,3 2位数值要对齐在4字节边界上。

• E X C E P T I O N _ A R R AY _ B O U N D S _ E X C E E D E D。线程试图存取一个越界的数组元素,相应的硬件支持边界检查。

• E X C E P T I O N _ I N _ PA G E _ E R R O R。由于文件系统或一个设备启动程序返回一个读错误,造成不能满足要求的页故障。

• E X C E P T I O N _ G U A R D _ PA G E。一个线程试图存取一个带有PA G E _ G U A R D保护属性的内存页。该页是可存取的,并引起一个E X C E P T I O N _ G U A R D _ PA G E异常。

• EXCEPTION_STA C K _ O V E R F L O W。线程用完了分配给它的所有栈空间。

• E X C E P T I O N _ I L L E G A L _ I N S T R U C T I O N。线程执行了一个无效的指令。这个异常由特定的C P U结构来定义;在不同的C P U上,执行一个无效指令可引起一个陷井错误。

• E X C E P T I O N _ P R I V _ I N S T R U C T I O N。线程执行一个指令,其操作在当前机器模式中不允许。

2. 与异常相关的异常

• E X C E P T I O N _ I N VA L I D _ D I S P O S I T I O N。一个异常过滤器返回一值,这个值不是E X C E P T I O N _ E X E C U T E _ H A N D L E R 、E X C E P T I O N _ C O N T I N U E _ S E A R C H、E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N三者之一。

• E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。一个异常过滤器对一个不能继续的异常返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。

3. 与调试有关的异常

• EXCEPTION_BREAKPOINT。遇到一个断点。

• E X C E P T I O N _ S I N G L E _ S T E P。一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕。

• E X C E P T I O N _ I N VA L I D _ H A N D L E。向一个函数传递了一个无效句柄。

4. 与整数有关的异常

• EXCEPTION_INT_DIVIDE_BY_ZERO。线程试图用整数0来除一个整数

• EXCEPTION_INT_OVERFLOW。一个整数操作的结果超过了整数值规定的范围。

5. 与浮点数有关的异常

• E X C E P T I O N _ F LT _ D E N O R M A L _ O P E R A N D。浮点操作中的一个操作数不正常。不正常的值是一个太小的值,不能表示标准的浮点值。

• EXCEPTION_FLT _ D I V I D E _ B Y _ Z E R O。线程试图用浮点数0来除一个浮点。

• EXCEPTION_FLT _ I N E X A C T _ R E S U LT。浮点操作的结果不能精确表示成十进制小数。

• EXCEPTION_FLT _ I N VA L I D _ O P E R AT I O N。表示任何没有在此列出的其他浮点数异常。

• EXCEPTION_FLT _ O V E R F L O W。浮点操作的结果超过了允许的值。

• EXCEPTION_FLT _ S TA C K _ C H E C K。由于浮点操作造成栈溢出或下溢。

• EXCEPTION_FLT _ U N D E R F L O W。浮点操作的结果小于允许的值。

内部函数G e t E x c e p t i o n C o d e只能在一个过滤器中调用( - - e x c e p t之后的括号里),或在一个异常处理程序中被调用。下面的代码是合法的:

__try
{
   y = 0;
   x = 4 / y;
}

__except(
   ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ||
    (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ?
   EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
   switch(GetExceptionCode())
   {
      case EXCEPTION_ACCESS_VIOLATION:
         //Handle the access violation.

         ...

         break;

      case EXCEPTION_INT_DIVIDE_BY_ZERO:
         //Handle the integer divide by?.

         ...

         break;
   }
}

但是,不能在一个异常过滤器函数里面调用G e t E x c e p t i o n C o d e。编译程序会捕捉这样的错误。当编译下面的代码时,将产生编译错。

__try
{
   y = 0;
   x = 4 / y;
}

__except(CoffeeFilter())
{
   // Handle the exception.

   ...

}

LONG CoffeeFilter(void)
{
   //Compilation error: illegal call to GetExceptionCode.
   return((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ?
      EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}

可以按下面的形式改写代码:

__try
{
   y = 0;
   x = 4 / y;
}

__except(CoffeeFilter(GetExceptionCode()))
{
   //Handle the exception.

   ...

}

LONG CoffeeFilter(DWORD dwExceptionCode)
{
   return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ?
      EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}

 

24.6 GetExceptionInformation

当一个异常发生时,操作系统要向引起异常的线程的栈里压入三个结构,这三个结构是:E X C E P T I O N _ R E C O R D结构、C O N T E X T结构和E X C E P T I O N _ P O I N T E R S结构。

E X C E P T I O N _ R E C O R D结构包含有关已发生异常的独立于C P U的信息,C O N T E X T结构包含已发生异常的依赖于C P U的信息。E X C E P T I O N _ P O I N T E R S结构只有两个数据成员,二者都是指针,分别指向被压入栈的E X C E P T I O N _ R E C O R D和C O N T E X T结构:

typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

为了取得这些信息并在你自己的程序中使用这些信息,需要调用G e t E x c e p t i o n I n f o r m a t i o n函数:

PEXCEPTION_POINTERS GetExceptionInformation();

这个内部函数返回一个指向E X C E P T I O N _ P O I N T E R S结构的指针。

关于G e t E x c e p t i o n I n f o r m a t i o n函数,要记住的最重要事情是它只能在异常过滤器中调用,因为仅仅在处理异常过滤器时, C O N T E X T、E X C E P T I O N _ R E C O R D和E X C E P T I O N _P O I N T E R S才是有效的。一旦控制被转移到异常处理程序,栈中的数据就被删除。

如果需要在你的异常处理程序块里面存取这些异常信息(虽然很少有必要这样做),必须将E X C E P T I O N _ P O I N T E R S结构所指向的C O N T E X T数据结构和/或E X C E P T I O N _ R E C O R D数据结构保存在你所建立的一个或多个变量里。下面的代码说明了如何保存E X C E P T I O N _R E C O R D和C O N T E X T数据结构:

void FuncSkunk() 
{
//Declare variables that we can use to save the exception
//record and the context if an exception should occur.
EXCEPTION_RECORD SavedExceptRec;
CONTEXT SavedContext;

...

__try
{

...

}

__except(
SavedExceptRec =
*(GetExceptionInformation())->ExceptionRecord,
SavedContext =
*(GetExceptionInformation())->ContextRecord,
EXCEPTION_EXECUTE_HANDLER)
{
//We can use the SavedExceptRec and SavedContext
//variables inside the handler code block.
switch(SavedExceptRec.ExceptionCode)
{

...

}
}

...

}
注意在异常过滤器中C语言逗号(,)操作符的使用。许多程序员不习惯使用这个操作符。它告诉编译程序从左到右执行以逗号分隔的各表达式。当所有的表达式被求值之后,返回最后的(或最右的)表达式的结果。

在F u n c S k u n k中,左边的表达式将执行,将栈中的E X C E P T I O N _ R E C O R D结构保存在Saved ExceptRec局部变量里。这个表达式的结果是S a v e d E x c e p t R e c的值。这个结果被丢弃,再计算右边下一个表达式。第二个表达式将栈中的C O N T E X T结构保存在S a v e d C o n t e x t局部变量里。第二个表达式的结果是S a v e d C o n t e x t,同样当计算第三个表达式时丢弃第二个表达式的结果。第三个表达式很简单,只是一个数值E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个最右边的表达式的结果就是整个由逗号分隔的表达式组的结果。

由于异常过滤器的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R,e x c e p t块中的代码要执行。这时,已被初始过的S a v e d E x c e p t R e c和S a v e d C o n t e x t变量可以在e x c e p t块中使用。要记住,S a v e d E x c e p t R e c和S a v e d C o n t e x t变量要在t r y块之外说明,这一点很重要。

我们都可以猜到, E X C E P T I O N _ P O I N T E R S 结构的E x c e p t i o n R e c o r d 成员指向E X C E P T I O N _ R E C O R D结构:

typedef struct _EXCEPTION_RECORD 
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
E X C E P T I O N _ R E C O R D结构包含有关最近发生的异常的详细信息,这些信息独立于C P U:

• ExceptionCode包含异常的代码。这同内部函数G e t E x c e p t i o n C o d e返回的信息是一样的。

• E x c e p t i o n F l a g s包含有关异常的标志。当前只有两个值,分别是0(指出一个可以继续的异常)和E X C E P T I O N _ N O N C O N T I N U A B L E(指出一个不可继续的异常)。在一个不可继续的异常之后,若要继续执行,会引发一个E X C E P T I O N _ N O N C O N T I N U A B L E _E X C E P T I O N异常。

• E x c e p t i o n R e c o r d指向另一个未处理异常的E X C E P T I O N _ R E C O R D结构。在处理一个异常的时候,有可能引发另外一个异常。例如,异常过滤器中的代码就可能用零来除一个数。当嵌套异常发生时,可将异常记录链接起来,以提供另外的信息。如果在处理一个异常过滤器的过程当中又产生一个异常,就发生了嵌套异常。如果没有未处理异常,这个成员就包含一个N U L L。

• ExceptionAddress指出产生异常的C P U指令的地址。

• N u m b e r P a r a m e t e r s 规定了与异常相联系的参数数量( 0 到1 5 )。这是在E x c e p t i o n I n f o r m a t i o n数组中定义的元素数量。对几乎所有的异常来说,这个值都是零。

• E x c e p t i o n I n f o r m a t i o n规定一个附加参数的数组,用来描述异常。对大多数异常来说,数组元素是未定义的。

E X C E P T I O N _ R E C O R D结构的最后两个成员,N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n向异常过滤器提供一些有关异常的附加信息。目前只有一种类型的异常提供附加信息,就是E X C E P T I O N _ A C C E S S _ V I O L AT I O N。所有其他可能的异常都将N u m b e r P a r a m e t e r s设置成零。我们可以检验E x c e p t i o n I n f o r m a t i o n的数组成员来查看关于所产生异常的附加信息。

对于一个E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常来说,E x c e p t i o n I n f o r m a t i o n [ 0 ]包含一个标志,指出引发这个存取异常的操作的类型。如果这个值是0,表示线程试图要读不可访问的数据。如果这个值是1,表示线程要写不可访问的数据。ExceptionInformation[1] 指出不可访问数据的地址。

通过使用这些成员,我们可以构造异常过滤器,提供大量有关程序的信息。例如,可以这样编写异常过滤器:

__try
{
...
}
__except(ExpFltr(GetExceptionInformation()->ExceptionRecord))
{
...
}

LONG ExpFltr(PEXCEPTION_RECORD pER)
{
char szBuf[300], *p;
DWORD dwExceptionCode = pER->ExceptionCode;

sprintf(szBuf, "Code = %x, Address = %p",
dwExceptionCode, pER->ExceptionAddress);

//Find the end of the string.
p = strchr(szBuf, 0);

// I used a switch statement in case Microsoft adds
// information for other exception codes in the future.
switch(dwExceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
sprintf(p, "Attempt to %s data at address %p",
pER->ExceptionInformation[0] ? "write" : "read",
pER->ExceptionInformation[1]);
break;

default:
break;
}

MessageBox(NULL, szBuf, "Exception", MB_OK | MB_ICONEXCLAMATION);

return(EXCEPTION_CONTINUE_SEARCH);
}
E X C E P T I O N _ P O I N T E R S结构的C o n t e x t R e c o r d成员指向一个C O N T E X T结构(第7章讨论过)。这个结构是依赖于平台的,也就是说,对于不同的C P U平台,这个结构的内容也不一样。

本质上,对C P U上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的C O N T E X T结构。最好的办法是在代码中安排一个# i f d e f s指令。Wi n d o w s支持的不同C P U的C O N T E X T结构定义在Wi n N T. h文件中。

引发一个软件异常很容易,只需要调用R a i s e E x c e p t i o n函数:

VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR *pArguments);
第一个参数d w E x c e p t i o n C o d e是标识所引发异常的值。H e a p A l l o c函数对这个参数设定S TAT U S _ N O _ M E M O RY。如果程序员要定义自己的异常标识符,应该遵循标准Wi n d o w s错误代码的格式,像Wi n E r r o r. h文件中定义的那样。参阅表2 4 - 1。

如果要建立你自己的异常代码,要填充D W O R D的4个部分:

• 第3 1位和第3 0位包含严重性系数( s e v e r i t y )。

• 第2 9位是1(0表示微软建立的异常,如H e a p A l l o c的S TAT U S _ N O _ M E M O RY)。

• 第2 8位是0。

• 第2 7位到1 6位是某个微软定义的设备代码。

• 第1 5到0位是一个任意值,用来标识引起异常的程序段。

R a i s e E x c e p t i o n 的第二个参数d w E x c e p t i o n F l a g s ,必须是0 或E X C E P T I O N _N O N C O N T I N U A B L E。本质上,这个标志是用来规定异常过滤器返回E X C E P T I O N _CONTINUE _EXECUTION来响应所引发的异常是否合法。如果没有向R a i s e E x c e p t i o n传递EXCEPTION_ NONCONTINUABLE参数值,则过滤器可以返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。正常情况下,这将导致线程重新执行引发软件异常的同一C P U指令。但微软已做了一些动作,所以在调用R a i s e E x c e p t i o n函数之后,执行会继续进行。

如果你向R a i s e E x c e p t i o n传递了E X C E P T I O N _ N O N C O N T I N U A B L E标志,你就是在告诉系统,你引发异常的类型是不能被继续执行的。这个标志在操作系统内部被用来传达致命(不可恢复)的错误信息。另外,当H e a p A l l o c引发S TAT U S _ N O _ M E M O RY软件异常时,它使用E X C E P T I O N _ N O N C O N T I N U A B L E标志来告诉系统,这个异常不能被继续。意思就是没有办法强制分配内存并继续运行。

如果一个过滤器忽略E X C E P T I O N _ N O N C O N T I N U A B L E并返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N,系统会引发新的异常:E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。

当程序在处理一个异常的时候,有可能又引发另一个异常。比如说,一个无效的内存存取有可能发生在一个f i n a l l y块、一个异常过滤器、或一个异常处理程序里。当发生这种情况时,系统压栈异常。回忆一下G e t E x c e p t i o n I n f o r m a t i o n函数。这个函数返回EXCEPTION_ POINTERS结构的地址。E X C E P T I O N _ P O I N T E R S的E x c e p t i o n R e c o r d成员指向一个EXCEPTION_ R E C O R D结构,这个结构包含另一个E x c e p t i o n R e c o r d成员。这个成员是一个指向另外的E X C E P T I O N _R E C O R D的指针,而这个结构包含有关以前引发异常的信息。

通常系统一次只处理一个异常,并且E x c e p t i o n R e c o r d成员为N U L L。然而如果处理一个异常的过程中又引发另一个异常,第一个E X C E P T I O N _ R E C O R D结构包含有关最近引发异常的信息,并且这个E X C E P T I O N _ R E C O R D结构的E x c e p t i o n R e c o r d成员指向以前发生的异常的E X C E P T I O N _R E C O R D结构。如果增加的异常没有完全处理,可以继续搜索这个E X C E P T I O N _ R E C O R D结构的链表,来确定如何处理异常。

R a i s e E x c e p t i o n的第三个参数n N u m b e r O f A rg u m e n t s和第四个参数p A rg u m e n t s,用来传递有关所引发异常的附加信息。通常,不需要附加的参数,只需对p A rg u m e n t s参数传递N U L L,这种情况下, R a i s e E x c e p t i o n函数忽略n N u m b e r O f A rg u m e n t s参数。如果需要传递附加参数,n N u m b e r O f A rg u m e n t s参数必须规定由p A rg u m e n t s参数所指向的U L O N G _ P T R数组中的元素数目。这个数目不能超过E X C E P T I O N _ M A X I M U M _ PA R A M E T E R S,EXCEPTION_ MAXIMUM_PARAMETERS 在Wi n N T. h中定义成1 5。

在处理这个异常期间,可使异常过滤器参照E X C E P T I O N _ R E C O R D结构中的N u m b e rP a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n成员来检查n N u m b e r O f A rg u m e n t s和p A rg u m e n t s参数中的信息。

你可能由于某种原因想在自己的程序中产生自己的软件异常。例如,你可能想向系统的事件日志发送通知消息。每当程序中的一个函数发现某种问题,你可以调用R a i s e E x c e p t i o n并让某些异常处理程序上溯调用树查看特定的异常,或者将异常写到日志里或弹出一个消息框。你还可能想建立软件异常来传达程序内部致使错误的信息。