系统故障解析:Windows异常处理流程(转)


  先来说说异常和中断的区别。中断可在任何时候发生,与CPU正在执行什么指令无关,中断主要由I/O设备、处理器时钟或定时器等硬件引发,可以被允许或取消。而异常是由于CPU执行了某些指令引起的,可以包括存储器存取违规、除0或者特定调试指令等,内核也将系统服务视为异常。中断和异常更底层的区别是当广义上的中断(包括异常和硬件中断)发生时如果没有设置在服务寄存器(用命令号0xb向8259-1中断控制器0x20端口读出在服务寄存器1,用0xb向8259-2中断控制器的0xa0端口读出在服务寄存器2)相关的在服务位(每个在服务寄存器有8位,共对应IRQ 0-15)则为CPU的异常,否则为硬件中断。[@more@]

  

  下面是WINDOWS2000根据INTEL x86处理器的定义,将IDT中的前几项注册为对应的异常处理程序(不同的操作系统对此的实现标准是不一样的,这里给出的和其它一些资料不一样是因为这是windows的具体实现):

  

  中断号  名字    原因

  0x0  除法错误    1、DIV和IDIV指令除0

  2、除法结果溢出

  0x1  调试陷阱    1、EFLAG的TF位置位

  2、执行到调试寄存器(DR0-DR4)设置的断点

  3、执行INT 1指令

  0x2  NMI中断    将CPU的NMI输入引脚置位(该异常为硬件发生非屏蔽中断而保留)

  0x3  断点    执行INT 3指令

  0x4  整数溢出    执行INTO指令且OF位置位

  0x5  BOUND边界检查错误  BOUND指令比较的值在给定范围外

  0x6  无效操作码  指令无法识别

  0x7  协处理器不可用  1、CR0的EM位置位时执行任何协处理器指令

  2、协处理器工作时执行了环境切换

  0x8  双重异常    处理异常时发生另一个异常

  0x9  协处理器段超限  浮点指令引用内存超过段尾

  0xA  无效任务段  任务段包含的描述符无效(windows不

  使用TSS进行环境切换,所以发生该异常说明有其它问题)

  0xB  段不存在    被引用的段被换出内存

  0xC  堆栈错误    1、被引用内存超出堆栈段限制

  2、加载入SS寄存器的描述符的present位置0

  0xD  一般保护性错误  所有其它异常处理例程无法处理的异常

  0xE  页面错误    1、访问的地址未被换入内存

  2、访问操作违反页保护规则

  0x10  协处理器出错  CR0的EM位置位时执行WAIT或ESCape指令

  0x11  对齐检查错误  对齐检查开启时(EFLAG对齐位置位)访问未对齐数据

  

  其它异常还包括获取系统启动时间服务int 0x2a、用户回调int 0x2b、系统服务int 0x2e、调试服务int 0x2d等系统用来实现自己功能的部分,都是通过异常的机制,触发方式就是执行相应的int指令。

  

  这里给出几个异常处理中重要的结构:

  

  陷阱帧TrapFrame结构(后面提到的异常帧ExceptionFrame结构其实也是一个KTRAP_FRAME结构):

  

  typedef struct _KTRAP_FRAME {

  ULONG  DbgEbp;

  ULONG  DbgEip;

  ULONG  DbgArgMark;

  ULONG  DbgArgPointer;

  ULONG  TempSegCs;

  ULONG  TempEsp;

  ULONG  Dr0;

  ULONG  Dr1;

  ULONG  Dr2;

  ULONG  Dr3;

  ULONG  Dr6;

  ULONG  Dr7;

  ULONG  SegGs;

  ULONG  SegEs;

  ULONG  SegDs;

  ULONG  Edx;

  ULONG  Ecx;

  ULONG  Eax;

  ULONG  PreviousPreviousMode;

  PEXCEPTION_REGISTRATION_RECORD ExceptionList;

  ULONG  SegFs;

  ULONG  Edi;

  ULONG  Esi;

  ULONG  Ebx;

  ULONG  Ebp;

  ULONG  ErrCode;

  ULONG  Eip;

  ULONG  SegCs;

  ULONG  EFlags;

  ULONG  HardwareEsp;

  ULONG  HardwareSegSs;

  ULONG  V86Es;

  ULONG  V86Ds;

  ULONG  V86Fs;

  ULONG  V86Gs;

  } KTRAP_FRAME;

  

  环境Context结构:

  

  typedef struct _CONTEXT {

  ULONG ContextFlags;

  ULONG  Dr0;

  ULONG  Dr1;

  ULONG  Dr2;

  ULONG  Dr3;

  ULONG  Dr6;

  ULONG  Dr7;

  FLOATING_SAVE_AREA FloatSave;

  ULONG  SegGs;

  ULONG  SegFs;

  ULONG  SegEs;

  ULONG  SegDs;

  ULONG  Edi;

  ULONG  Esi;

  ULONG  Ebx;

  ULONG  Edx;

  ULONG  Ecx;

  ULONG  Eax;

  ULONG  Ebp;

  ULONG  Eip;

  ULONG  SegCs;

  ULONG  EFlags;

  ULONG  Esp;

  ULONG  SegSs;

  UCHAR  ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

  } CONTEXT;

  

  异常记录ExceptionRecord结构:

  

  typedef struct _EXCEPTION_RECORD {

  NTSTATUS ExceptionCode;

  ULONG ExceptionFlags;

  struct _EXCEPTION_RECORD *ExceptionRecord;

  PVOID ExceptionAddress;

  ULONG NumberParameters;

  ULONG_PTR ExceptionInformatio[EXCEPTION_MAXIMUM_PARAMETERS];

  } EXCEPTION_RECORD;

  

  当发生异常后,CPU记录当前各寄存器状态并在内核堆栈中建立陷阱帧TrapFrame,然后将控制交给对应异常的陷阱处理程序。当陷阱处理程序能处理异常时,比如缺页时通过调页程序MmAccessFault将页换入物理内存后通过iret返回发生异常的地方。但大多数无法处理异常,这时先是调用CommonDispatchException在内核堆栈中建立异常记录ExceptionRecord和异常帧ExceptionFrame。ExceptionRecord很重要,它记录了异常代码、异常地址以及一些其它附加的参数。然后调用KiDispatchException进行异常的分派。这个函数是WINDOWS下异常处理的核心函数,负责异常的分派处理。

  

  KiDispatchException的处理流程(每当异常被某个例程处理时处理的例程将返回TRUE到上一个例程,未处理则返回FALSE。当任何一个例程处理了异常返回TRUE时,则KiDispatchException正常返回):

  

  在进行用户态内核态的异常的分派前,先判断异常是否来自用户模式,是的话将Context.ContextFlags(这时候Context结构还刚初始化完,还未赋初值) or上CONEXT_FLOATING_POINT,意味着对来自用户模式的异常总是尝试分派浮点状态,这样可以允许异常处理程序或调试器检查和修改协处理器的状态。然后从陷阱帧中取出寄存器值填入Context结构,并判断是否是断点异常(int 0x3和int 0x2d),如果是的话先将Context.Eip减一使它指向int 0x3指令(无论是由int 0x3还是由int 0x2d引起的异常,因为前面的陷阱处理程序里已经改变过TrapFrame里面的Eip了)。然后判断异常是发生于内核模式还是用户模式,根据不同模式而采取不同处理过程。

  

  如果异常发生于内核模式,会给予内核调试器第一次机会和第二次机会处理异常。当异常被处理后就将设置好陷阱帧并返回到陷阱处理程序,在那里iret返回发生异常的地方继续执行。

  

  内核模式异常处理流程为:

  

  (第一次机会)判断KiDebugRoutine是否为空,不为空就将Context、陷阱帧、异常记录、异常帧、发生异常的模式等压入栈并将控制交给KiDebugRoutine。

  

  若KiDebugRoutine为空(正常的系统这里不为空。正常启动的系统KiDebugRoutine为KdpStub,在Boot.ini里加上/DEBUG启动的系统的KiDebugRoutine为KdpTrap。如果这里为空的话会因为处理不了DbgPrint这类int 0x2d产生的异常而导致系统崩溃)或者KiDebugRoutine未处理异常,则将Context结构和异常记录ExceptionRecord压栈并调用内核模式的RtlDispatchException在内核堆栈中查找基于帧的异常处理例程。

  

  RtlDispatchException调用RtlpGetRegistrationHead从fs:[0](0xffdff000)处获取当前线程异常处理链表指针,并调用RtlpGetStackLimits从0xffdff004和0xffdff008取出当前线程堆栈底和顶。然后开始由异常处理链表指针遍历链表查找异常处理例程(若在XP和2003下先处理VEH再处理SEH),其实这就是SEH,只是和用户态有一点不同是既没有顶层异常处理例程(TOP LEVEL SEH)也没有默认异常处理例程。然后对每个当前异常处理链表指针检查判断堆栈是否有效(是否超出了堆栈范围或者未对齐)及堆栈是否是DPC堆栈。若0xffdff80c处DpcRoutineActive为TRUE且堆栈顶和底在0xffdff81c处取出的DpcStack到DpcStack-0x3000(一个内核堆栈大小),若是则更新堆栈顶和底为DpcStack和DpcStack-0x3000并继续处理,否则将异常记录结构里的异常标志ExceptionRecord.ExceptionFlags设置EXCEPTION_STACK_INVALID表示为无效堆栈并返回FALSE。

  

  调用异常处理链表上的异常处理例程之前会在异常处理例程链表上插入一个新的节点,对应的异常处理例程是用来处理嵌套异常,也就是在处理异常时发生另一个异常。处理后

  RtlDispatchException判断异常处理例程的返回值:

  若为ExceptionContinueExecution,若异常标志ExceptionRecord.ExceptionFlags未设置EXCEPTION_NONCONTINUABLE不可恢复执行,则返回TRUE到上一层,否则在做了一些工作后调用RtlRaiseException进入到KiDispatchException的第二次机会处理部分。

  若为ExceptionContinueSearch,则继续查找异常处理例程。

  若为ExceptionNestedException,嵌套异常。保留当前异常处理链表指针为内层异常处理链表并继续查找异常处理例程。当发现当前异常处理链表地址大于保留的内层异常处理链表时,表示当前的异常处理链表比保留的更内层(因为堆栈是由高向低扩展的,地址越高则入栈越早,表示更内层),则将其值赋予内层异常处理链表指针,除了第一次赋初值外发生修改保留的内层

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10144097/viewspace-934656/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10144097/viewspace-934656/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值