未处理异常和C + +异常(上)
前一章讨论了当一个异常过滤器返回 E X C E P T I O N _ C O N T I N U E _ S E A R C H时会发生什么事情。返回EXCEPTION_CONTINUE_SEARCH 是告诉系统继续上溯调用树,去寻找另外的异常过滤器。但是当每个过滤器都返回E X C E P T I O N _ C O N T I N U E _ S E A R C H时会出现什么情况呢?在这种情况下,就出现了所谓的“未处理异常”(Unhandled exception)。
在第6章里我们已经知道,每个线程开始执行,实际上是利用 K e r n e l 3 2 . d l l中的一个函数来调用B a s e P r o c e s s S t a r t或B a s e T h r e a d S t a r t。这两个函数实际是一样的,区别在于一个函数用于进程的主线程(Primary thread):
另一个函数用于进程的所有辅助线程(Secondary thread):
注意这两个函数都包含一个S E H框架。每个函数都有一个t r y块,并从这个t r y块里调用主线程或辅助线程的进入点函数。所以,当线程引发一个异常,所有过滤器都返回 E X C E P T I O N _C O N T I N U E _ S E A R C H时,将会自动调用一个由系统提供的特殊过滤器函数: U n h a n d l e dE x c e p t i o n F i l t e r。
这个函数负责显示一个消息框,指出有一个进程的线程存在未处理的异常,并且能让用户
结束或调试这个进程。
下面是写个测试代码来看下:
测试代码,引发异常,但是没有用__except去处理。根据上面的那个创建进程的函数,会先调用U n h a n d l e dE x c e p t i o n F i l t e r弹个窗,点击关闭,然后全局展开,执行finally的代码。
25.1 即时调试
随时将调试程序连接到任何进程的能力称为即时调试(Just-in-time Debugging)。这里我们对它如何工作稍加说明:当程序员点击 C a n c e l按钮,就是告诉U n h a n d l e d E x c e p t i o n F i l t e r函数对进程进行调试。在内部,U n h a n d l e d E x c e p t i o n F i l t e r调用调试程序,这需要查看下面的注册表子关键字:
在这个子关键字里,?有一个名为D e b u g g e r的数值,在安装Visual Studio时被设置成下面的值:
这一行代码是告诉系统要将哪一个程序(这里是 M S D e v. ? e x e)作为调试程序运行。当然也可以选择其他调试程序。U n h a n d l e d E x c e p t i o n F i l t e r还在这个命令行中向调试程序传递两个参数。第一个参数是被调试进程的 I D。第二个参数规定一个可继承的手工复位事件,这个事件是由U n h a n d l e d E x c e p t i o n F i l t e r按无信号状态建立的。厂商必须实现他们的调试程序,这样才能认识指定进程I D和事件句柄的- p和- e选项。
在进程 I D和事件句柄都合并到这个串中之后, U n h a n d l e d E x c e p t i o n F i l t e r通过调用C r e a t e P r o c e s s来执行调试程序。?这时,调试程序进程开始运行并检查它的命令行参数。如果存在- p选项,调试程序取得进程I D,并通过调用D e b u g A c t i v e P r o c e s s将自身挂接在该进程上。
BOOL DebugActiveProcess(DWORD dwProcessID);
一旦调试程序完成自身的挂接,操作系统将被调试者(d e b u g g e e)的状态通报给调试程序。例如,系统将告诉调试程序,在被调试的进程中有多少线程?哪些 D D L加载到被调试进程的地址空间中?调试程序需要花时间来积累这些数据,以准备调试进程。在这些准备工作进行的时候,U n h a n d l e d E x c e p t i o n F i l t e r中的线程必须等待。为此,这要调用 Wa i t F o r S i n g l e O b j e c t函数并传递已经建立的手工复位事件的句柄作为参数。这个事件是按无信号状态建立起来的,所以被调试进程的线程要立即被挂起以等待事件。
在调试程序完全初始化之后,它要再检查它的命令行,找 - e选项。如果该选项存在,调试程序取得相应的事件句柄并调用 S e t E v e n t。调试程序可以直接使用事件的句柄值,因为事件句柄具有创建的可继承性,并且被调试进程对 U n h a n d l e d E x c e p t i o n F i l t e r函数的调用也使调试程序进程成为一个子进程。设定这个事件将唤醒被调试进程的线程。被唤醒的线程将有关未处理异常的信息传递给调试程序。调试程序接收这些通知并加载相应的源代码文件,再将自身放在引发异常的指令位置上。
还有,不必在调试进程之前等待异常的出现。可以随时将一个调试程序连接在任何进程上,只需运行“MSDEV -p PID”,其中P I D是要调试的进程的 I D。实际上,利用 Windows 2000Task Manager,做这些事很容易。当观察P r o c e s s标记栏时,可以选择一个进程,点击鼠标右键,并选择D e b u g菜单选项。这将引起 Task Manager去查看前面讨论过的注册表子关键字,调用C r e a t e P r o c e s s,并传递所选定的进程的 I D作为参数。在这里,Task Manager为事件句柄传送0值。
尝试了下那个函数,结果如下:
25.2 关闭异常消息框
有时候,在异常发生时,你可能不想在屏幕上显示异常消息框。例如,你可能不想让这些消息框出现在你产品的发售版本中。如果出现了消息框,很容易导致最终用户意外地启动调试程序来调试你的程序。最终用户只需点击一下消息框中的 C a n c e l按钮,就进入了不熟悉的、令人恐惶的区域 — 调试程序。可以使用几种不同的方法来防止这种消息框的出现。
25.2.1 强制进程终止运行
为防止U n h a n d l e d E x c e p t i o n F i l t e r显示异常消息框,可以调用下面的S e t E r r o r M o d e l函数,并向它传递一个S E M _ N O G P FA U LT E R R O R B O X标识符:
UINT SetErrorMode(UINT fuErrorMode);
然后,当调用U n h a n d l e d E x c e p t i o n F i l t 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。这将导致全局展开并执行B a s e P r o c e s s S t a r t或B a s e T h r e a d S t a r t中的处理程序。该处理程序结束进程。
主线程里异常不会崩溃
在其他线程里,也不会崩溃,相当于这个设置是整个进程的。
25.2.2 包装一个线程函数
使用另外一种办法也可以避免出现这个消息框,就是针对主线程进入点函数( m a i n、
w m a i n、Wi n M a i n或w Wi n M a i n)的整个内容安排一个t r y - 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,这样就保证异常得到处理,防止了系统再调用U n h a n d l e d E x c e p t i o n F i l t e r函数。
在你的异常处理程序中,你可以显示一个对话框,在上面显示一些有关异常的诊断信息。
用户可以记录下这些信息,并通报给你公司的客户服务部门,以便能够找到程序的问题根源。
你应该建立这个对话框,这样用户只能结束程序而不能调用调试程序。
这种方法的缺点是它只能捕捉进程的主线程中发生的异常。如果其他线程在运行,并且其中有一个线程发生了一个未处理异常,系统就要调用内部的 U n h a n d l e d E x c e p t i o n F i l t e r函数。为了改正这一点,需要在所有的辅助线程进入点函数中包含t r y - e x c e p t块。
25.2.3 包装所有的线程函数
Wi n d o w s还提供另外一个函数,S e t U n h a n d l e d E x c e p t i o n F i l t e r,利用它可以按S E H格式包装所有的线程函数:
PTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
PTOP_LEVEL_EXCEPTION_FILTER pTopLevelExceptionFilter);
在进程调用这些函数之后,进程的任何线程中若发生一个未处理的异常,就会导致调用程序自己的异常过滤器。需要将这个过滤器的地址作为参数传递给 S e t U n h a n d l e d E x c e p t i o n F i l t e r。过滤器函数原型必须是下面的样子:
LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);
你可能会注意到这个函数同U n h a n d l e d E x c e p t i o n F i l t e r函数的形式是一样的。程序员可以在自己的异常过滤器中执行任何想做的处理,但要返回三个 E X C E P T I O N _ *标识符中的一个。表2 5 - 1给出了当返回各标识符时所发生的事。
为了使 U n h a n d l e d E x c e p t i o n F i l t e r函数再成为默认的过滤器,可以调用 S e t U n h a n d l e dE x c e p t i o n F i l t e r并传递 N U L L给它。而且,每当设置一个新的未处理的异常过滤器时,SetUnhandled ExceptionFilter就返回以前安装的异常过滤器的地址。如果 Unhandled ExceptionF i l t e r是当前所安装的过滤器,则这个返回的地址就是 N U L L。 如果你自己的过滤器要返回E X C E P T I O N _ C O N T I N U E _ S E A R C H,你就应该调用以前安装的过滤器,其地址通过S e t U n h a n d l e d E x c e p t i o n F i l t e r函数返回。
以下是一个测试例子:
25.2.4 自动调用调试程序
现在再介绍关闭U n h a n d l e d E x c e p t i o n F i l t e r消息框的最后一种方法。在前面提到的同一个注册表子关键字里,还有另外一个数据值,名为 A u t o。这个值用来规定U n h a n d l e d E x c e p t i o n F i l t e r是应该显示消息框,还是仅启动调试程序。如果 A u t o设置成1,U n h a n d l e d E x c e p t i o n F i l t e r就不显示消息框向用户报告异常,而是立即调用调试程序。如果 A u t o子关键设置成 0,U n h a n d l e d E x c e p t i o n F i l t e r就显示异常消息框,并按前面描述的那样操作。
25.3 程序员自己调用U n h a n d l e d E x c e p t i o n F i l t e r
U n h a n d l e d E x c e p t i o n F i l t e r函数是一个公开的、文档完备的 Wi n d o w s函数,程序员可以直接在自己的代码中调用这个函数。这里是使用这个函数的一个例子:
在F u n c a d e l i c函数中,t r y块中的一个异常导致E x p F l t r函数被调用。G e t E x c e p t i o n I n f o r m a t i o n的返回值作为参数传递给 E x p F l t r函数。在异常过滤器内,要确定异常代码并与 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 _ C O N T I N U E _ E X E C U T I O N。这个返回值导致系统从最初引起异常的指令继续执行。
如果发生了其他异常,E x p F l t r调用U n h a n d l e d E x c e p t i o n F i l t e r,将E X C E P T I O N _ P O I N T E R S结构的地址传递给它作为参数。U n h a n d l e d E x c e p t i o n F i l t e r显示消息框,可使程序员结束进程或开始调试进程。U n h a n d l e d E x c e p t i o n F i l t e r的返回值再由E x p F l t r返回。