开发人员主要使用两种异常处理技术,一种是 SEH (结构化异常处理),另一种是 VEH (向量化异常处理,XP 以上)
Intel公司在从386开始的IA-32家族处理器中引人了中断(Interrupt)和异常(Exception)的概念。
中断是由外部硬件设备或异步事件产生的,而异常是由内部事件产生的,又可分为故障、陷阱和终止3类。故障和陷阱,正如其名称所示的,是可恢复的;终止类异常是不可恢复的,如果发生了这种异常,系统必须重启。
CPU 访问无效内存,是硬件异常;操作系统或软件引发的异常,是软件异常。
同时代码可以通过函数 RaiseException ,主动引发一个异常
Windows 正常启动后,会运行在保护模式下,当有中断或异常发生时,CPU 会通过中断描述符(Interrupt Descriptor Table)来寻找处理函数。
IDT (下面主要讨论 32 位模式下的 IDT)
IDT 是一张位于物理内存中的线性表,共有 256 项。在 32 位模式下每个 IDT 项的长度是 8 字节,在 64 位模式下则为 16 字节。
操作系统在启动阶段会初始化这个表,系统中的每个 CPU 都有一份 IDT 的拷贝。IDT 的位置和长度是由 CPU 的 IDTR 寄存器描述的。IDTR 寄存器共有 48 位,其中高 32 位是表的基址,低 16 位是表的长度。尽管可以使用 SIDT 和 LIDT 指令来读写该寄存器,但 LIDT 是特权指令,只能在 Ring 0 特权级下运行。
SIDT 指令的功能 (仅对当前的 CPU): 将中断描述符表寄存器IDTR--64位宽,16~47Bit 存有中断描述符表 IDT 基地址的内容存入指定地址单元。获得 IDT 的基地址后,可以修改 IDT,增加一个中断门安置自己的中断服务。
IDT 的每一项都是一个门结构,它是发生中断或异常时 CPU 转移控制权的必经之路,包括如下
• 任务门(Task-gate) 描述符,主要用于 CPU 的任务切换(TSS功能)。(微软没有采用该方式,内存频繁读写,拖慢系统速度,在 x64 中被废除)
• 中断门( Interrupt-gate)描述符,主要用于描述中断处理程序的入口。
• 陷阱门(Trap-gate)描述符,主要用于描述异常处理程序的入口。(Windows 64 位下,系统本身的运行没有使用任务门)
32 位下有任务门
当有中断或异常发生时,CPU 会根据中断类型号(这里其实把异常也视为一种中断)转而执行对应的中断处理程序,对异常来说就是上面看到的 KiTrapXX 函数。例如,中断号03对应于一个断点异常,当该异常发生时,CPU就会执行 nt!KiTrap03 函数来处理该异常。各个异常处理函数除了针对本异常的特定处理之外,通常会将异常信息进行封装,以便进行后续处理。
封装的内容主要有两部分:一部分是异常记录,包含本次异常的信息,该结构定义如下。
这个结构体其实就是 SEH 处理函数的第一个参数。ExceptionCode 可以自己定义,自定义代码在 RaiseException 函数中使用
另一部分被封装的内容称为陷阱帧,它精确描述了发生异常时线程的状态( Windows 的任务调度是基于线程的)。该结构与处理器高度相关,因此在不同的平台上(Intel x86/x64、MIPS、Alpha 和 PowerPC 处理器等)有不同的定义。在常见的 x86 平台上,该结构定义如下。
typedef struct _KTRAP_FRAME {
// 以下四项仅为调试系统服务 ULONG DbgEbp; //用户EBP指针的拷贝,用于支持栈回溯命令KB ULONG DbgEip; //用于 系统调用时的 EIP 同上,用于 KB 命令 ULONG DbgArgMark; //标记显示这里没有参数 ULONG DbgArgPointer; //指向实际参数
// 当需要调整栈帧时使用以下值作为临时变量 WORD TempSegCs; UCHAR Logging; UCHAR Reserved; 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, *PKTRAP_FRAME;
可以看到,上述结构中包含每个寄存器的状态,但该结构一般仅供系统内核自身或者调试系统使用。
当需要把控制权交给用户注册的异常处理程序时,会将上述结构转换成一个名为 CONTEXT 的结构,它包含线程运行时处理器各主要寄存器的完整镜像,用于保存线程运行环境。
typedef struct _CONTEXT {
// 调试寄存器 DWORD ContextFlags +00h DWORD Dr0 +04h DWORD Dr1 +08h DWORD Dr2 +0Ch DWORD Dr3 +10h DWORD Dr6 +14h DWORD Dr7 +18h FLOATING_SAVE_AREA FloatSave; //浮点寄存器区 +1Ch~~~88h // 段寄存器 DWORD SegGs +8Ch DWORD SegFs +90h DWORD SegEs +94h DWORD SegDs +98h // 通用寄存器 DWORD Edi +9Ch DWORD Esi +A0h DWORD Ebx +A4h DWORD Edx +A8h DWORD Ecx +ACh DWORD Eax +B0h // 控制寄存器 DWORD Ebp +B4h DWORD Eip +B8h DWORD SegCs +BCh DWORD EFlag +C0h DWORD Esp +C4h DWORD SegSs +C8h BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; typedef CONTEXT *PCONTEXT; #define MAXIMUM_SUPPORTED_EXTENSION 512
第一个域 ContexFlags 表示该结构体中的哪些域有效,恢复信息时可有选择的更新数据。
包装完毕,异常处理函数会进一步调用系统内核的 nt!KiDispatchException 函数来处理异常。因此,只有深入分析 KiDispatchException 函数的执行过程,才能理解异常是如何被处理的。该函数原型及各参数的含义如下,其第1个和第3个参数正是上面封装的两个结构。
当异常处理过程在内核态中
当 PreviousMode 为 KernelMode 时,表示是内核模式下产生的异常,此时 KiDispatchException 会按以下步骤分发异常。
① 检测当前系统是否正在被内核调试器调试。如果内核调试器不存在,就跳过本步骤。如果内核调试器存在,系统就会把异常处理的控制权转交给内核调试器,并注明是第1次处理机会( FirstChance )。内核调试器取得控制权之后,会根据用户对异常处理的设置来确定是否要处理该异常。如果无法确定该异常是否需要处理,就会发生中断,把控制权交给用户,由用户决定是否处理。如果调试器正确处理了该异常,那么发生异常的线程就会回到原来产生异常的位置继续执行。
② 如果不存在内核调试器,或者在第1次处理机会出现时调试器选择不处理该异常,系统就会调用 nt!RtIDispatchException 函数,根据线程注册的结构化异常处理(Structured Exception Handling,SEH )过程来处理该异常。
③ 如果 nt!RtIDispatchException 函数没有处理该异常,系统会给调试器第2次处理机会( SecondChance ),此时调试器可以再次取得对异常的处理权。
④如果不存在内核调试器,或者在第2次机会调试器仍不处理,系统就认为在这种情况下不能继续运行了。为了避免引起更加严重的、不可预知的错误,系统会直接调用 KeBugCheckEx 产生一个错误码为“KERNEL_MODE_EXCEPTION_NOT_HANDLED”(其值为 0x0000008E )的 BSOD (俗称蓝屏错误)。
当异常处理过程在用户态中
当 PreviousMode 为 UserMode 时,表示是用户模式下产生的异常。此时 KiDispatchException 函数仍然会检测内核调试器是否存在。如果内核调试器存在,会优先把控制权交给内核调试器进行处理。所以,使用内核调试器调试用户态程序是完全可行的,并且不依赖进程的调试端口。在大多数情况下,内核调试器对用户态的异常不感兴趣,也就不会去处理它,此时 nt!KiDispatchException 函数仍然像处理内核态异常一样按两次处理机会进行分发,主要过程如下。
① 如果发生异常的程序正在被调试,那么将异常信息发送给正在调试它的用户态调试器,给调试器第1次处理机会;如果没有被调试,跳过本步。
② 如果不存在用户态调试器或调试器未处理该异常,那么在栈上放置 EXCEPTION_RECORD 和 CONTEXT 两个结构,并将控制权返回用户态 ntdll.dll 中的 KiUserExceptionDispatcher 函数,由它调用 ntdll!RtIDispatchException 函数进行用户态的异常处理。这一部分涉及 SEH 和 VEH 两种异常处理机制。其中,SEH 部分包括应用程序调用 API 函数 SetUnhandledExceptionFilter 设置的顶级异常处理,但如果有调试器存在,顶级异常处理会被跳过,进入下一阶段的处理,否则将由顶级异常处理程序进行终结处理(通常是显示一个应用程序错误对话框并根据用户的选择决定是终止程序还是附加到调试器)。如果没有调试器能附加于其上或调试器还是处理不了异常,系统就调用 ExitProcess 函数来终结程序。
③ 如果 ntdlRtIDispatchException 函数在调用用户态的异常处理过程中未能处理该异常,那么异常处理过程会再次返回 nt!KiDispatchExoception,它将再次把异常信息发送给用户态的调试器,给调试器第2次处理机会。如果没有调试器存在,则不会进行第2次分发,而是直接结束进程。
④ 如果第2次机会调试器仍不处理, nt!KiDispatchException 会再次尝试把异常分发给进程的异常端口进行处理。该端口通常由子系统进程 csrss.exe 进行监听。子系统监听到该错误后,通常会显示一个“应用程序错误”对话框,如果没有调试器能附加于其上,或者调试器还是处理不了异常,系统就调用 ExitProcess 函数来终结程序。
⑤ 在终结程序之前,系统会再次调用发生异常的线程中的所有异常处理过程,这是线程异常处理过程所获得的清理未释放资源的最后机会,此后程序就终结了。
SEH 相关的数据结构
TIB (Thread Information Block,线程信息块)是保存线程基本信息的数据结构。在用户模式下,它位于 TEB (Thread Environment Block,线程环境块)的头部,而TEB是操作系统为了保存每个线程的私有数据创建的,每个线程都有自己的TEB。在Windows 2000 DDK中、TIB的定义如下。
FS:[0] 即为 Exceptionlist 的地址
__EXCEPTION_POINTERS 结构
当一个异常发生时,在没有调试器干预的情况下,操作系统会将异常信息转交给用户态的异常处理过程。实际上,由于同一个线程在用户态和内核态使用的是两个不同的栈,为了让用户态的异常处理程序能够访问与异常相关的数据,操作系统必须把与本次异常相关联的 EXCEPTION_RECORD 结构和 CONTEXT 结构放到用户态栈中,同时在栈中放置一个 _EXCEPTION_POINTERS 结构,它包含两个指针,一个指向 EXCEPTION_RECORD 结构,另一个指向 CONTEXT 结构,示例如下。
看一下 RtlDispatchException 函数的代码:
总体的流程是:
① 首先调用 VEH 异常处理,返回值不是 EXCEPTION_CONTINUE_SEARCH 就结束异常分发
② 返回值符合要求,则查询 SEHOP 是否启用,未开启则会进行校验(对每个 Record 结构体都会进行验证)
③ 对 Handler 进行增强验证,即 SafeSEH 机制(通过调用函数 RtlIsValidHandler 来实现)
④ 开始依次执行 Handler,并对返回值进行对比(Switch-Case),执行相应的操作
⑤ 必会执行的最后一步,调用 RtlCallVectoredContinueHandlers 函数,然后程序返回
//Exception Flags #define EXCEPTION_NONCONTINUABLE 0x1 // Noncontinuable exception #define EXCEPTION_UNWINDING 0x2 // Unwind is in progress #define EXCEPTION_EXIT_UNWIND 0x4 // Exit unwind is in progress #define EXCEPTION_STACK_INVALID 0x8 // Stack out of limits or unaligned #define EXCEPTION_NESTED_CALL 0x10 // Nested exception handler call #define EXCEPTION_TARGET_UNWIND 0x20 // Target unwind in progress #define EXCEPTION_COLLIDED_UNWIND 0x40 // Collided exception handler call //MmExecutionFlags on Win7 #define MEM_EXECUTE_OPTION_DISABLE 0x1 #define MEM_EXECUTE_OPTION_ENABLE 0x2 #define MEM_EXECUTE_OPTION_DISABLE_THUNK_EMULATION 0x4 #define MEM_EXECUTE_OPTION_PERMANENT 0x8 #define MEM_EXECUTE_OPTION_EXECUTE_DISPATCH_ENABLE 0x10 #define MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE 0x20 #define MEM_EXECUTE_OPTION_DISABLE_EXCEPTIONCHAIN_VALIDATION 0x40 #define MEM_EXECUTE_OPTION_VALID_FLAGS 0x7f //NtGlobalFlag #define FLG_ENABLE_CLOSE_EXCEPTIONS 0x00400000 // kernel mode only #define FLG_ENABLE_EXCEPTION_LOGGING 0x00800000 // kernel mode only //Part of ProcessInformationClass #define ProcessExecuteFlags 34 typedef struct _DISPATCHER_CONTEXT { PEXCEPTION_REGISTRATION_RECORD RegistrationPointer; } DISPATCHER_CONTEXT; // // Execute handler for exception function prototype. // EXCEPTION_DISPOSITION RtlpExecuteHandlerForException ( IN PEXCEPTION_RECORD ExceptionRecord, IN PVOID EstablisherFrame, IN OUT PCONTEXT ContextRecord, IN OUT PVOID DispatcherContext, IN PEXCEPTION_ROUTINE ExceptionRoutine ); VOID RtlpGetStackLimits ( OUT PULONG LowLimit, OUT PULONG HighLimit ); EXCEPTION_DISPOSITION RtlCallVectoredExceptionHandlers ( IN PEXCEPTION_RECORD ExceptionRecord, IN OUT PCONTEXT ContextRecord ); EXCEPTION_DISPOSITION RtlCallVectoredContinueHandlers ( IN PEXCEPTION_RECORD ExceptionRecord, IN OUT PCONTEXT ContextRecord ); PEXCEPTION_REGISTRATION_RECORD RtlpGetRegistrationHead ( VOID ); BOOLEAN RtlIsValidHandler ( IN PEXCEPTION_ROUTINE Handler, IN ULONG ProcessExecuteFlag ); BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD pExcptRec, CONTEXT *pContext) { BOOLEAN Completion; PEXCEPTION_RECORD pExcptRec; EXCEPTION_REGISTRATION_RECORD *RegistrationPointerForCheck; EXCEPTION_REGISTRATION_RECORD *RegistrationPointer; EXCEPTION_REGISTRATION_RECORD *NestedRegistration; EXCEPTION_DISPOSITION Disposition; EXCEPTION_RECORD ExceptionRecord1; DISPATCHER_CONTEXT DispatcherContext; ULONG ProcessExecuteOption; ULONG StackBase,StackLimit; BOOLEAN IsSEHOPEnable; NTSTATUS status; Completion = FALSE; // 首先调用VEH异常处理例程,其返回值包括 EXCEPTION_CONTINUE_EXECUTION (0xffffffff)和 EXCEPTION_CONTINUE_SEARCH (0x0)两种情况 // 这是从Windows XP开始加入的新的异常处理方式 // 返回值不是 EXCEPTION_CONTINUE_SEARCH,那么就结束异常分发过程 if (RtlCallVectoredExceptionHandlers(pExcptRec, pContext) != EXCEPTION_CONTINUE_SEARCH ) { Completion = TRUE; } else { // 获取栈的内存范围 RtlpGetStackLimits(&StackLimit, &StackBase); ProcessExecuteOption = 0; // 从fs:[0]获取SEH链的头节点 RegistrationPointerForCheck = RtlpGetRegistrationHead(); // 默认假设SEHOP机制已经启用,这是一种对SEH链的安全性进行增强验证的机制 IsSEHOPEnable = TRUE; // 查询进程的ProcessExecuteFlags标志,决定是否进行SEHOP验证 status = ZwQueryInformationProcess(NtCurrentProcess(), ProcessExecuteFlags, &ProcessExecuteOption, sizeof(ULONG), NULL) ; // 在查询失败,或者没有设置标志位时,进行SEHOP增强验证 // 也就是说,只有在明确查询到禁用了SEHOP时才不会进行增强验证 if ( NT_SUCCESS(status) && (ProcessExecuteOption & MEM_EXECUTE_OPTION_DISABLE_EXCEPTIONCHAIN_VALIDATION) ) { // 若确实未开启SEHOP增强校验机制,设置此标志 IsSEHOPEnable = FALSE; } else { // 否则,进行开始SEHOP验证 if ( RegistrationPointerForCheck == -1 ) break; //验证SEH链中各个结点的有效性并遍历至最后一个结点 do { // 若发生以下情况,认为栈无效,此时不再执行基于栈的SEH处理 // 1.SEH节点不在栈中 if ( (ULONG)RegistrationPointerForCheck < StackLimit || (ULONG)RegistrationPointerForCheck + 8 > StackBase // 2.SEH节点的位置没有按ULONG对齐 || (ULONG)RegistrationPointerForCheck & 3 // 3.Handler在栈中 || ((ULONG)RegistrationPointerForCheck->Handler < StackLimit || (ULONG)RegistrationPointerForCheck->Handler >= StackBase) ) { pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID; goto DispatchExit; } // 取SEH链的下一个结点 RegistrationPointerForCheck = RegistrationPointerForCheck->Next; } while ( RegistrationPointerForCheck != -1 ); // 此时RegistrationPointerForCheck指向最后一个节点 // 如果TEB->SameTebFlags中的RtlExceptionAttached位(第9位)被设置,但最后一个结点的Handler却不是预设的安全SEH,那么SEHOP校验不通过,不再执行任何SEHHandler if ((NtCurrentTeb()->SameTebFlags & 0x200) && RegistrationPointerForCheck->Handler != FinalExceptionHandler) { goto DispatchExit; } } // 从fs:[0]获取SEH链的头节点 RegistrationPointer = RtlpGetRegistrationHead(); NestedRegistration = NULL; // 遍历SEH链表执行Handler while ( TRUE ) { if ( RegistrationPointer == -1 ) //-1表示SEH链的结束 goto DispatchExit; // 若SEHOP机制未开启,则这里必须进行校验,反之则不需要,因为SEHOP机制已经验证过了 if ( !IsSEHOPEnable ) { if ( (ULONG)RegistrationPointer < StackLimit || (ULONG)RegistrationPointer + 8 > StackBase || (ULONG)RegistrationPointer & 3 || ((ULONG)RegistrationPointer->Handler < StackLimit || (ULONG)RegistrationPointer->Handler >= StackBase) ) { pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID; goto DispatchExit; } } // 调用RtlIsValidHandler对Handler进行增强验证,也就是SafeSEH机制 if (!RtlIsValidHandler(RegistrationPointer->Handler, ProcessExecuteOption)) { pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID; goto DispatchExit; } // 执行SEHHandler Disposition = RtlpExecuteHandlerForException(pExcptRec, RegistrationPointer, pContext, &DispatcherContext, RegistrationPointer->Handler); if ( NestedRegistration == RegistrationPointer ) { pExcptRec->ExceptionFlags &= (~EXCEPTION_NESTED_CALL); NestedRegistration = NULL; } // 检查SEHHandler的执行结果 switch(Disposition) { case ExceptionContinueExecution : if ((ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) != 0) { ExceptionRecord1.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION; ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE; ExceptionRecord1.ExceptionRecord = ExceptionRecord; ExceptionRecord1.NumberParameters = 0; RtlRaiseException(&ExceptionRecord1); } else { Completion = TRUE; goto DispatchExit; } case ExceptionContinueSearch : if (ExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID) goto DispatchExit; break; case ExceptionNestedException : ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL; if (DispatcherContext.RegistrationPointer > NestedRegistration) { NestedRegistration = DispatcherContext.RegistrationPointer; } break; default : ExceptionRecord1.ExceptionCode = STATUS_INVALID_DISPOSITION; ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE; ExceptionRecord1.ExceptionRecord = ExceptionRecord; ExceptionRecord1.NumberParameters = 0; RtlRaiseException(&ExceptionRecord1); break; } // 取SEH链的下一个结点 RegistrationPointer = RegistrationPointer->Next; // Next } } DispatchExit: // 调用VEH的ContinueHandler // 只要RtlDispatchException函数正常返回,那么ContinueHandler总会在SEH执行完毕后被调用 RtlCallVectoredContinueHandlers(pExcptRec, pContext); return Completion; }