Windows异常的分发处理流程
根据异常来源,一般分硬件异常和软件异常,它们处理的流程大致一样,下面简单讲一下。
如果是硬件异常,CPU会根据中断类型号转而执行对应的中断处理程序。CPU会在IDT中查找对应的函数来处理,各个异常处理函数不仅仅处理异常还需要将异常信息封装,以便对后续处理,KiTrapXX例程在完成针对本异常的特别动作后,通常会调用CommonDispatchException函数,它会在栈中分配一个EXCEPTION_RECORD结构,并把异常信息存储到该结构中。在准备好这个结构后,它会调用内核中的KiDispatchExcption函数来分发异常。
如果是软甲异常,是通过直接或间接调用内核服务KiRaiseException而产生的。函数内部会把Context上下背景文复制到当前线程的内核栈,接下来调用KiDispatchExcption函数来进行分发。
综上所述,不管什么异常最后都会调用内核中的KiDispatchExcption函数进行分发,也就是说Windows用统一的方式来管理。异常封装完成后,系统会调用nt!KiDispatchException来处理异常,所以分析KiDispatchException函数就可以了解异常是如何被处理的。
首先来看看KiDisPatchException函数的函数原型
void KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
ExceptionRecord也就是前面提的描述异常的结构,TrapFrame指向的结构用来描述发生异常时候的上下文,PreviousMode来说明异常来自Kernel还是User,最后的FirstChance用来表示异常是不是第一次被处理,实际上这些结构的集合就形成了一个虚拟的、完整的“异常”结构,再去进行下面的处理。下面先来看看KiDispatchException 的分发示意图:
从图中我们可以看到,KiDispatchException会先调用KeContextFromKframes函数,目的是根据TrapFrame参数指向的KTRAP_FRAME结构产生一个CONTEXT结构,以供向调试器和异常处理器函数报告异常时使用。右边是内核的异常,左边边是用户的异常。
内核异常处理分发流程:
但PreviousMode为0时,就会进入Kernel的异常分发,系统会维护一个KiDebugRoutine的函数,当内核的调试器启动时,它就帮我们把异常送往了内核调试器,而在未启动时,它只是一个“存根”函数(stub),返回一个False。这一步也就是图中的debug
当第一次debug返回False后会接着调用RtlDispatchException,试图寻找已经注册的结构化异常处理器(SEH),函数的原型:BOOLEAN RtlDispatchException(PEXCEPTION_RECORD ExceptionRecord,PCONTEXT ContextRecord)。如果没有处理的话就会进行第二轮调试,重复上面的debug内容,如果依然是没有启用调试器的话就那么就会把这个异常当作UnhandleException,也就是我们常说的未处理异常,在kernel下未处理异常可是个大问题,毕竟这可是操作系统最最重要的也最最完善的内核,这样的未处理异常一般都不是小问题,为了防止异常引发更大的问题,这时候系统就会调用KeBugCheckEx中止系统运行显示蓝屏,并将导致异常的地址打印在屏幕上。
具体步骤如下:
用户模式异常处理分发流程:
当PreviousMode==1时就进入了用户态的异常分发,相较于Kernel来说,user的异常处理还包括了我们自己在编写程序的过程中用到的try catch,下面就具体来看看。
首先还是检查是否有调试器,具体的措施和Kernel相仿,不过找的函数是内核的DbgForwardException,这个函数涉及到了用户态的调试。简单点说就是用户态的调试器是不是要接手这个异常,如果成了就交给它处理,如果没有的话那就会通过KeUserExceptionDispatcher来找到KiUserExceptionDispatcher函数,要注意,此时已经返回到了用户态,且异常的相关信息(比如KTRAP_FRAME)已经被放入了用户态的栈上。之后会调用了RtlDispatchException(注意,该函数依然名字和作用都与Kernel的几乎相同,但是它是位于NTDLL的,而Kernel的则是位于NTOSKRNL)来遍历异常处理器的链表,但这次的链表又了“保底措施”,在链表的最末尾是UnhandledExceptionFilter(未处理异常过滤函数),一旦走到了这里,那就会出现“应用程序错误”的对话框并强制结束程序(之后会写这个函数的详细分析),异常也就算是处理完成了。
既然有了UnhandledExceptionFilter那岂不是所有的异常都会最终被直接处理了,那第二轮又是怎么回事?实际上如果在非调试状态下确实如此,用户态的异常如果在非调试状态下的话仅仅只有一轮的分发,而只有在调试状态下才会进行第二轮,再次判断调试器是否要接手异常。
具体处理步骤: