在读《软件调试》的十一章时,感受到异常处理在VC中是十分重要的。以前自己写代码或者是看身边的人写的代码都很少用到异常处理,但最近在工作中会接触到老外牛人写的代码,几乎在每个关键的代码块都提供了异常处理,虽然在这些异常处理代码中只是简单的将异常的相关信息写入Event Viewer,但这已经对我们找到bug和了解系统运行情况提供了很大的帮助。于是乎我把学习这一章的心得总结出来,供大家分享。
首先我们看window为描述异常定义的数据结构 EXCEPTION_RECORD,具体如下
typedef struct _EXCEPTION_RECORD{
DWORD ExceptionCode; // 异常代码
DWORD ExceptionFlags; // 异常标志
struct _EXCEPTION_RECORD* ExceptionRecord; // 相关的另外一个异常
PVOID ExceptionAddress; // 异常发生地址
DWORD NumberParameters; // 参数数组中的元素个数
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 参数数组
}
其中的ExcptionCode,我们应当非常熟悉了,最常见的为EXCEPTION_ACCESS_VIOLATION, 值为0xC0000005L, 就是非法访问,我们遇到的大多数Crash问题都是来源于此。
了解了异常的结构,下面我们看异常是如何分发的:
异常分发包括用户态的异常分发和内核态的异常分发,这里只介绍用户态的异常分发。KiDispatchException是个内核函数,它是分发windows异常的枢纽,它的函数原型为KiDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPPROCESSOR_MODE PreviousMode,
IN BOOLEN FirstChange);
其中的参数ExceptionRecord指的是上面我介绍的_EXCEPTION_RECORD结构。其他的参数我就不详细介绍了,我重点介绍一下最后的一个参数FirstChange,相信使用过windbg的同学们都会发现总能看到FirstChange这个参数吧,其实异常在分发过程中最多可以分发两次,下图是摘自《软件调试》,清楚的显示了kiDispatchException的分发异常的过程。其中左侧是用户态的,右侧是内核态的。
如上图所示,当变量FirstChange为TRUE时,调用DbgkForwardException分发给调试子系统,也就是当存在调试器时,分首先分发给调试器,所以当使用windbg时我们常会发现First Change字段。如果函数DbgForwardException返回FALSE,那么就说明没有发现调试器,会调用DbgkForwardException进行第二次分发,这次分发会寻找异常的处理块,一般就是__exception{}字段的代码或是catch{}字段的代码,等一会在介绍。如果这次仍然返回FALSE,就会调用ZwTerminateThread终止当前的线程,会弹出对话框,里面包含一些异常的信息,这个也是很常见的。如果出现在内核状态,那么就直接出现蓝屏(BSOD)。
下面介绍一下结构化异常处理(SEH)和向量化异常处理(VEH)
结构化异常处理,从系统的角度来看,SEH是对windows操作系统中异常分发和处理机制的总称,其实现遍布在windows系统的很多模块和数据结构中。从编程的角度来看,SEH是一套规范,利用这套规范,程序员可以编写处理代码来复用系统的异常处理设施。可以理解为是操作系统的异常机制的对外接口,也就是如何在windows程序中使用windows系统的异常处理机制。(摘自《软件调试》291页)
结构化异常处理提供了终结处理(Termination Handling)和异常处理(Exception Handling)两种功能。以VC++为例,终结处理的结构如下
__try
{
// 被保护体
}
__finally
{
// 终结处理代码
}
终结处理的用途是只要被保护体内的代码执行了,除非是调用了ExitThread或ExitProcess,那么终结处理的代码就一定会执行。所以终结处理的这种特性,适合做资源的释放工作,包括内存的释放,避免内存泄露,以及状态的恢复,包括信号设置等,避免多线程出现死锁。
终结处理的执行过程有一点需要说明,如果被保护体内存在代码return,那意味着退出函数,这时终结块是如何被执行的呢?其实编译器会定义一个局部变量来保存return的值,然后在目标代码中出入指令调用名为__local_unwind2的局部展开函数,来进行调用finally块的代码。注:加双下划线的关键字是编译器定义的。
下面我们看异常的处理方式:
__try
{
// 被保护的代码块
}
__except( 过滤表达式)
{
// 异常处理块
}
是不是觉得眼熟,它与C++的异常处理很相似,C++的try-catch-throw结构如下:
try {
// code that could throw an exception
}
[ catch (exception-declaration) {
// code that executes when exception-declaration is thrown
// in the try block
}
[catch (exception-declaration) {
// code that handles another exception type
} ] . . . ]
// The following syntax shows a throw expression:
throw [expression]
本想将两个特点与不同详细介绍一下,但偶然间发现前辈已经总结了,它博客的地址为http://blog.****.net/lingqinghua/archive/2005/12/21/558182.aspx
所以SEH与C++异常的不同我就不介绍了,感兴趣的可以直接查看他的博客。
现在我们看SEH的过滤表示式,这个表达式可以是常量,条件运算符,逗号表达式或者其他的函数。但其结果必须为0,1,-1这三个值。其中EXCEPTION_CONTIONUE_SERARCH的值为0,表示本保护块不处理该异常,让系统寻找其他异常处理块。EXCEPTION_CONTINUE_EXECUTION的值为1,表式已经处理了该异常,让程序返回到异常发生点,继续执行。这种情况是如果该异常没有清除,可能还会发生异常。该过程存在很大的风险,因为对于实际问题,很难找到完美的异常解决方案,一旦返回异常点继续执行,再次发生异常,因此VC编译器不准返回EXCEPTION_CONTINUE_EXECUTION。如果强行返回,将会导致EXECPTION_NONCONTINUABLE_EXCEPTION异常。EXCEPTION_EXECUTE_HANDLER的值为-1。它的含义是这是本保护块预计到的异常,当发生该异常后,会执行本块的异常代码,执行完成后会继续向下执行。所以可以在异常处理块中进行记录异常的一些信息,包括寄存器的信息。
下面我们看VEH(向量化结构处理)
VEH的基本思想是通过注册回调函数的方式来接收和处理异常。回调函数的原型为LONG CALLBACK VectoredHandler ( PEXCEPTION_POINTERS ExceptionInfo );
对应的有两个windows API函数AddVectoredExceptionHandler(ULONG FirstHandler, PVECTORD_EXCEPTION_HANDLER vectoredHandler)和RemoveVectoredExceptionHandler分别用来注册和注销回调函数。参数FirstHandler用来指定该回调函数的调用顺序, 为0表示希望最后被调用,为1表示希望最先被调用。如果注册多个回调函数,而且FirstHandler都为非零值,那么最后注册的会最先被调用。
VEH的示例可以参考《软件调试》的304页。
最后总结一下VEH与SEH的区别。
1. SEH既可以用在用户态的代码中,也可以用在内核态的代码中。但VEH只能用在用户态的代码中。VEH需要在windows XP以上的版本中使用
2. 如果同时注册了VEH和SEH,那么VEH优先处理
3. SEH的注册信息是以固定的结构存储在线程栈中的, VEH的注册信息是存储在进程的内存堆中的。这让我想到了一个问题,能够在进程的内存中手动添加VEH信息,当有异常发生时就会执行VEH的代码,这是不是可以破坏一些程序。纯属异想天开,望高人指点!
4. VEH对整个进程都有效,而SEH是动态建立在函数的栈帧上的,会随着函数的返回而被注销。
5. SEH是依赖编译器编译时生成的数据结构和代码。而VEH的注册和注销都是通过系统的API显式完成的,灵活~
谢谢,大部分都是参考的《软件调试》,仅一点点心得,希望对不是很了解的人有些帮助!