第八章——Windows下异常处理-异常处理基本概念

前言:
中断和异常的区别,中断是由外部硬件设备或异步事件产生的。异常是由内部事件产生,可以分为故障,陷阱和终止三类。
由CPU引发的异常成为硬件异常,例如访问一个无效的内存地址。由操作系统或应用程序引发的异常成为软件异常。
我们也可以主动抛出一个异常,通过RaiseException()函数
void WINAPI RaiseException(
  _In_       DWORD     dwExceptionCode,                //标识所引发异常的代码
  _In_       DWORD     dwExceptionFlags,                //异常是否继续执行的标识
  _In_       DWORD     nNumberOfArguments,        //附加信息
  _In_ const ULONG_PTR *lpArguments                  //附加信息
);
 
异常处理基本过程
1.IDT
    在windows启动后,在保护模式下,有中断或者异常发生时,CPU会通过中断描述符表来查找处理函数(IDT表)
    IDT表共有256项,在32位下每个IDT项的长度为8字节,在64位下长度为64字节,操作系统会在启动阶段初始化这个表
    IDT的位置和长度由CPU和IDTR寄存器藐视,IDTR寄存器有48位,其中高32位是基地址。低16位是表的长度
 
    IDT的每一项都是一个门结构,其中有三种门:
  • 任务门:用于CPU任务切换(TSS)
  • 中断门:用于描述中断处理程序入口
  • 陷阱门:主要用于描述异常处理程序入口
 
2.异常处理的准备工作
一、当有中断或异常发生时,CPU会根据中断类型号转而执行对应的中断处理程序。CPU会在IDT中查找对应的函数来处理,各个异常处理函数不仅仅处理异常还需要将异常信息封装,以便对后续处理(_EXCEPTION_RECORD结构体记录封装的异常信息)
这个结构体主要描述了异常的代码,异常标志,还有异常发生的地址,没有描述异常发生时,具体的异常环境。具体的异常环境在另一个结构体_KTRAP_FRAME(陷阱帧)中,这里面包括了每个寄存器的状态,这个结构体通常在内核中,而我们编写调试器的时候,通常用的是context结构体
二、上述总而言之是对异常信息的封装,封装完成后,系统会调用nt!KiDispatchException来处理异常,所以分析KiDispatchException函数就可以了解异常是如何被处理的
      函数原型:
                KiDispatchException (
                    IN PEXCEPTION_RECORD ExceptionRecord,            //异常结构信息(就是上面_EXCEPTION_RECORD结构体内容)
                    IN PKEXCEPTION_FRAME ExceptionFrame,              
                    IN PKTRAP_FRAME TrapFrame,                                //发送异常的陷阱帧(内核:_KTRAP_FRAME,三环:context)
                    IN KPROCESSOR_MODE PreviousMode,                 //发送异常时CPU模式是内核还是用户
                    IN BOOLEAN FirstChance                                        //是否第一次发生异常
                    )
                    
3.内核态的异常处理过程
    PreviousMode字段是KernelMode时候,表示内核模式下产生异常,具体处理步骤:
    ①系统会先检测是否有内核调试器,如果没有,就跳过这一步,如果有,就把异常处理的权限交给内核调试器,并且注明是第一次来执行的这个异常(FirstChance),内核调试器如果处理了该异常就继续回到原来异常地方继续执行,如果没有处理则发生中断,将控制权交给用户,用户决定是否继续处理
    ②如果不存在内核调试器,或者第一次的异常没有被处理,系统就会调用RtDispatchException,这里会根据用户注册的SEH异常处理结构来处理(注意,内核态下只有SEH)
    ③上述过后,如果异常处理了,程序继续运行,如果第一次没有处理,则进行第二次异常处理,系统会再将控制权交给内核调试器
    ④如果不能存在内核调试器,或者第二次处理失败了,这时系统就会调用KeBugCheckEx产生一个错误码为"KERNEL_MODE_EXCEPTION_NOT_HANDLED"蓝屏错误
 
4.用户态的异常处理过程
    PreviousMode字段是UserMode时候,表示用户模式下产生异常,此时KiDispatchException函数仍然会检测内核调试器是否存在,如果内核调试器存在,系统还是会将控制权交给内核调试器进行处理,内核调试器对用户态的程序是可以调试,并且还不依赖进程的调试窗口。但是在大多数情况下,内核调试器是不调试用户态程序,所以KiDispatchException函数还是会和内核调试器一样,分两次在用户态下处理异常信息,具体处理步骤:
    ①如果存在异常的程序被调试,系统会将异常信息发送给正常调试的用户态调试器,给调试器一次机会,如果没有被调试,跳过此步
    ②如果不存在用户调试器,或者调试器未处理该异常,那么栈上放置EXCEPTION_RECORD和CONTEXT,并将控制权返回用户态的KiDispatchException函数,这一步涉及SEH,VEH顶级异常处理,如果调试器存在,顶级异常处理函数就会被跳过,否则就会被顶级处理函数接管
    ③如果RtlDispatchException函数在调用用户态的异常处理过程中未处理该异常,那么异常处理过程会再次返回kisdispathchexception,进行第二次异常分发
    ④,如果第二次还没有处理,则 kisdispathchexception会尝试将异常分发给进程的异常端口进行处理,该端口由csrss.exe进行监听,如果监听到错误,则会显示一个应用程序错误,如果调试器还不能附加其上,则会调用exitprocess结束进程
 
原文地址:https://www.cnblogs.com/Tempt/p/10218883.html