蓝弧计算机维护网:提供电脑维护,企业局域网建设,病毒防御与查杀,企业网站制作及维护推广,电脑维修等方案  
首页 |  解决方案 |  服务项目 |  服务流程 |  IT外包 |  维护宝典 |  收费标准 |  关于我们 
     
 
电脑维护之Windows异常处理流程(下)
 
  用户模式异常处理流程:

  若KiDebugRoutine不为空,则不为空就将Context、陷阱帧、异常记录、异常帧、发生异常的模式等压入栈并将控制交给KiDebugRoutine。当处理完毕用Context设置陷阱帧并返回到上一级例程。(第一次机会)否则把异常记录压栈并调用DbgkForwardException,在DbgkForwardException里判断当前线程ETHREAD结构的HideFromDebugger成员如果为FALSE(为TRUE表示该异常对用户调试器不可见)则向当前进程的调试端口(DebugPort)发送LPC消息。
    |
    |
    |
  当上一步无法处理异常时将Context结构拷贝到用户堆栈,在堆栈中设置一个陷阱帧,陷阱帧的Eip为Ke(i)UserExceptionDisptcher((i)表示这个函数的Ke和Ki打头的符号其实是一回事),接着返回陷阱处理程序,由陷阱处理程序iret返回用户态执行Ke(i)UserExceptionDispatcher(这个函数虽然是Ke(Ki)打头,却不是内核里的函数。同样性质特殊的还有Ke(i)RaiseUserExceptionDispatcher、Ke(i)UserCallbackDispatcher、Ke(i)UserApcDispatcher。它们的共同特点就是不是被调用的,而是由内核例程设置了陷阱帧TrapFrame.Eip为该函数后iret执行到这里的)。Ke(i)UserExceptionDisptcher调用RtlDispatchException(用户态下的)寻找堆栈中基于帧的异常处理例程(若在XP和2003下先处理VEH再处理SEH),这个流程大家应该很熟了,就是搜索SEH链表,若都不处理就调用顶层异常处理(TOP LEVEL SEH)例程。当再无法处理时就调用默认异常处理例程终止进程(有VC时这里就换成了VC)。有点不同的是用户态下的RtlDispatchException只判断返回值是ExceptionContinueExecution还是ExceptionContinueSearch。若RtlDispatchException找到异常处理例程能够处理异常,则调用ZwContinue按照设置好的Context结构继续执行,否则调用ZwRaiseException,并且把第三个布尔参数设为FALSE,表示进入第二次机会处理。
    |
    |
    |
  ZwRaiseException经过一系列调用最后直接调用KiDispatchException,由于把布尔值FirstChance设置为FALSE,在KiDispatchException里直接进入第二次机会处理。
    |
    |
    |
  (第二次机会)向进程的DebugPort发消息,若无法处理,则改向进程的ExceptionPort发消息(这里同样如果该异常对用户调试器不可见,则只会发送到ExceptionPort)。DebugPort和ExceptionPort的区别在于,若向ExceptionPort发消息,先停止目标进程所有线程的执行,直到收到回应消息后线程才恢复执行,而向DebugPort发消息则不需要停止线程运行。还有DebugPort是向会话管理器发消息,而ExceptionPort是向Win32子系统发消息,当向ExceptionPort发消息时,已经不给用户态调试器任何机会了:)。
    |
    |
    |
  当异常还是无法处理,则终止当前线程,并调用KeBugCheckEx蓝屏,错误代码为KMODE_EXCEPTION_NOT_HANDLED。至此用户模式下异常处理完毕。

  有一点要说明的是,这里只是列出了操作系统的流程。如果现在去看比方说驱动或者一个应用程序里,加入了__try/__except代码,却没发现SEH上的节点对应的异常处理例程和自己写的有啥关系。其中的原因就是因为M$的编译器(比方说VC++、DDK)在系统内部封装了SEH的机制。在异常处理例程链表节点上的异常处理例程实际上是_except_handler3,这个函数自己在内部又实现了一套类似SEH的机制,就是通过对每个函数建立一个表,包括了该函数中所有__try块对应的过滤异常例程指针(__try()括号里的对应函数,如果是括号里是EXCEPTION_EXECUTE_HANDLER之类的则该过滤异常例程很简单地把eax赋为相应的1、0或-1然后返回,对应了EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTE)和处理例程指针(__except{}和__finally{}里面代码的地址)。所以对应每个被调用到的函数都在SEH链表上注册一个节点,并建立一张表。象诸如EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTE实际上是返回给_except_handler3的返回值,而不是返回给RtlDispatchException的。

  看得有点晕吧,呵呵。总结一下流程(指异常未被处理的完整流程,若异常在任一个环节被处理都从该环节上退出继续原来正常程序代码的执行):

  内核模式下:
  KiDispatchException->(第一次机会)KiDebugRoutine->RtlDispatchException在内核堆栈寻找SEH(VEH)->(第二次机会)KiDebugRoutine->KeBugCheckEx

  用户模式下:
  KiDispatchException->KiDebugRoutine->(第一次机会)发送消息到进程调试端口->RtlDispatchException在用户堆栈寻找SEH(VEH)->ZwRaiseException回到KiDispatchException->(第二次机会)发送消息到进程调试或异常端口->KeBugCheckEx。

  就这么简单,呵呵。举个例子来说明异常的处理。
  
  我们程序中通过加入int 0x3来对程序进行调试,那么int 0x3产生的异常是怎么处理的呢?

  若int 0x3发生在内核模式下(驱动程序里),又分为内核调试器是否加载。内核调试器未加载时,KiDebugRoutine(KdpStub)不处理int 0x3异常,则在堆栈中搜索由驱动程序编写者注册的SEH,若也没有对int 0x3异常的处理,则只好调用KeBugCheckEx蓝屏,错误代码是KMODE_EXCEPTION_NOT_HANDLED(谁都不干活,系统只好悲愤地自我了结了)。若有内核调试器加载,则KiDebugRoutine(KdpTrap)可以处理int 0x3异常,异常处理到这里被正常返回。处理方法是将当前的处理器状态(比如各寄存器等重要信息)发送给通过串口相连的主机上的内核调试器,并一直在等待内核调试器的回应,系统这时在KdpTrap里调用一个Kd函数KdpSendWaitContinue循环等待来自串口的数据(内核调试器和被调试系统通过串口联系),直到内核调试器下达继续执行的命令,系统可以正常从int 0x3后面一条指令执行。

  若int 0x3发生在用户模式下,也分为用户调试器是否加载。用户调试器未加载时,KiDebugRoutine不处理int 0x3异常,且用户进程无DebugPort,将记录异常的相关结构拷入用户堆栈交由用户模式的RtlDispatchException搜索用户堆栈中的SEH。若也没有对int 0x3异常的处理,则调用默认异常处理例程,默认情况下是将发生异常的进程终止。若有用户调试器加载,则向进程的DebugPort或ExceptionPort发送有关异常的LPC消息并判断发送函数的返回状态,若用户调试器继续执行,则返回STATUS_SUCCESS,内核视为异常已解决继续执行。

  这里也说明了一点,就是相同的错误,发生在內核态下比发生在用户态下致命得多。其它异常的处理流程基本也一样。少数不一样的异常象DebugPrint、DebugPrompt、加载和卸载symbol等是通过调用DebugService的(这实际上是通过产生一个异常int 0x2d)。可以在KdpStub中就被处理(处理很简单,只是把Context结构的Eip加一略过当前int 0x2d后那条int 0x3指令),若在KdpTrap中被处理则是和内核调试器更进一步的交互。关于KdpStub、KdpTrap和DebugService更详细的介绍参见我的另一篇文章《windows内核调试器原理浅析》。
 
 
  更多内容
  •  电脑维护之Windows异常处理流程(中)
  •  Win 2000操作中Hosts文件的作用
  •  浏览器无法显示Flash文件