windows操作系统的异常处理机制

465 阅读6分钟

从信号源的角度来讲,异常大体可以分为两类:软件异常和硬件异常。前者由操作系统/应用程序引发,后者由CPU引发。

异常和中断的区别在于,中断可以在任何时候发生,和CPU正在执行什么指令无关,可以被取消。而异常是由于CPU执行了某条指令而引起的,不能被取消。

软件异常

用户:RaiseException -> RltRaiseException -> NtRaiseException -> KiRaiseException 
内核:RtlRaiseException -> NtRaiseException -> KiRaiseException

软件异常归根结底都是基于 RaiseException 这个用户态 API 和 NtRaiseException 的内核服务建立起来的。

void RaiseException(
	DWORD dwExceptionCode ,			// 异常状态码
	DWORD dwExceptionFlags,			// 异常标志(可持续 / 不可持续)
	DWORD nNumberofArguments,		// lpArguments[] 数组长度
	const DWORD* lpArguments		// 传递给异常处理程序的筛选表达式
);
// 具体作用就是将异常信息传递给 EXCEPTION_RECORD 这个结构,然后再调用 RtlRaiseException 函数
/*
	typedef struct _EXCEPTION_RECORD {
	  DWORD                     ExceptionCode;			// 异常状态码
	  DWORD                     ExceptionFlags;			// 异常标志
	  struct _EXCEPTION_RECORD  *ExceptionRecord;		// 指向下一个异常的指针
	  PVOID                     ExceptionAddress;		// 保存异常发生的地址
	  DWORD                     NumberParameters;		// ExceptionInformation[] 数组参数个数
	  ULONG_PTR                 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];		// 异常描述信息
} EXCEPTION_RECORD;
*/

硬件异常

基本可以分为三类,错误异常、陷阱异常、终止异常。

错误异常

处理错误异常时,操作系统首先保护当前环境(上下文),然后然后调用相应的异常处理函数,如果异常处理成功则恢复现场环境继续执行。

陷阱异常

和错误异常不同,陷阱异常发生时会保存要执行的下一条指令的环境(而不是正在执行的指令的环境),调试器的断点就是基于陷阱异常实现的。

终止异常

主要用来处理严重的硬件错误,和上面的异常不同,这种异常不会恢复执行而是直接退出。

异常的分发处理

异常产生时,CPU是通过IDT进入内核来寻找处理函数:

当内核可以处理这个异常时,异常处理程序执行完后会恢复现场并继续执行,这个过程应用程序感知不到。

当内核不能处理这个异常时,如果该异常来自内核,则蓝屏;如果该异常来自应用程序,则异常处理权转交给应用程序的异常处理函数,程序如果处理了该异常则程序继续执行,没有处理则崩溃。

如果程序被调试,则异常处理权限转交给调试器。如果调试器处理了这个异常则程序继续执行,如果没有处理则将异常处理权限转交给应用程序。如果程序没有被调试,则将异常信息连同线程上下文环境送入程序的栈中,并调用 KiUserExceptionDisptcher (由 ntdll 导出,所有的异常分发都会走这个函数),但实际发挥作用的是 RtlExceptionDisptcher,如果该接口返回成功,则调用 ZwContinue 继续执行,否则调用 ZwRaiseException 结束进程。

硬件异常会通过IDT去调用异常处理例程(一般为KiTrap系列函数)。 软件异常则是通过API的层层调用传递异常的信息。

但无论是硬件异常还是软件异常,最后都会走到 KiDispatchException

windows 提供了三种异常处理方式,结构化异常处理(SEH)、向量化异常处理(VEH、VCH)、顶级异常处理(TopLevelEH)。

结构化异常处理(SEH)

SEH是基于线程的一种处理机制,依赖于栈进行存储和查找,所以也被称作是基于栈帧的异常处理机制(只能处理自己线程的异常,而不是像VEH那样可以影响整个进程)。

其实,可以把SEH理解为高级语言的(try-catch),它的作用就是构造一个SEH节点,并将该节点放入SEH链表头部。

在从 C/C++ 中,可以使用 __try 和 __expect 来处理 windows 的 SEH 异常,还可以使用SetUnhandledExceptionFilter注册一个异常处理函数。

当一个异常产生,且__try 和 __expect没有处理处理这个异常时,异常会转交给SetUnhandledExceptionFilter。但是如果存在调试器,则调试器就会接管这个异常,那么异常就不会走SetUnhandledExceptionFilter注册的异常处理函数(调试器默认情况下是接管的)。

except参数的值有以下三种:

EXCEPTION_CONTINUE_EXECUTION,异常被忽略或已被修复,程序控制留跳转到导致异常的那条指令,并尝试重新执行这条指令,继续恢复运行。

EXCEPTION_CONTINUE_SEARCH,异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确异常处理模块,那么系统将继续到上一层的try-except域中,继续查找一个恰当的__except模块;单纯返回常量EXCEPTION_CONTINUE_SEARCH,系统寻找到在它上一层的一个try块,并调用对应的异常过滤程序中的函数(此时若出现异常终止程序,会先忽略)。

EXCEPTION_EXECUTE_HANDLER,异常已经被识别,也即当前的这个异常错误,系统已经找到了并能够确认,这个__except模块就是正确的异常处理模块,控制流将进入到__except模块中。

typedef struct _CONTEXT {
    DWORD   ContextFlags;
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;
    FLOATING_SAVE_AREA FloatSave;
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;
    DWORD   EFlags;
    DWORD   Esp;
    DWORD   SegSs;
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

向量化异常处理(VEH、VCH)

在每个进程的 ntdll.dll 中有一张 VEH 链表, 可以向其中添加一个节点来注册自己的异常处理器,使用AddVectordExceptionHandler函数。

VCH 和 VEH 类似,但是只会在异常被处理的情况下,最后调用。

顶级异常处理(TopLevelEH)

本质上也是 SEH,在最顶层的SEH中,可以注册一个顶层异常处理器(和 SEH 不同的是它可以影响所有的线程)。

当 SEH 链表中的异常处理器都处理不了某个异常,在最顶层的SEH中就会检查是否注册了顶层异常处理,如果注册了顶级异常处理器,就会给 SEH “最后一次处理异常的机会” ,把异常抛给 TopLevelEH(但如果程序被调试时就会忽略 TopLevelEH)。

各异常之间的关系

  1. 当异常交由用户处理时,按照以下顺序调用异常处理方式 VEH -> SEH -> VCH。
  2. 当 VEH 表示处理了异常,就不会传递给 SEH,但是会传递异常给 VCH。
  3. 当 VEH 没有处理了,就会传递给 SEH。
  4. 当 SEH 的所有异常处理函数没能够处理异常,会调用默认的 SEH (就是 UEH,只是方式属于SEH)处理函数。
  5. 当 SEH 处理了异常,从 except 开始执行,就不会再将异常传递给 VCH。
  6. 当 SEH 返回异常产生处执行,在返回之前会调用 VCH。
┌─────┐ unable  ┌─────┐ unable  ┌─────┐  unable  ┌─────┐
│ VEH ├────────►│ SEH ├────────►│ UEH ├─────────►│ VCH │
└──┬──┘ handle  └──┬──┘ handle  └─────┘  handle  └──▲──┘
   │               │            handle              │
   │               └────────────────────────────────┤
   │            handle                              │
   └────────────────────────────────────────────────┘

RtlExceptionDisptcher

其具体工作如下:

首先遍历 VEH 链表,逐个执行异常处理器,一旦某个处理器处理成功则返回成功,线程继续运行。如果 VEH 链表遍历完毕异常仍然没有被处理则遍历 SEH 链表,再逐个执行 SEH 的异常处理器一旦某个处理器处理成功则返回成功,线程继续运行。如果 VEH 和 SEH 都没有处理这个异常,则进程结束。

参考资料:

blog.csdn.net/Simon798/ar…

blog.csdn.net/TCP_321/art…