第8章:Windows 下的异常处理-SEH
开发人员主要使用两种异常处理技术,一种是 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; }
函数 RtlExecuteHandlerForException 的返回值 _Exception_Disposition 有:
(1) ExceptionContinueExecution
异常已处理,根据 Contex 结构体的相关信息恢复线程的执行
(2) ExceptionContinueSearch
回调函数不能处理该异常,需要交由链表中的下一个函数来处理
(3) ExceptionNestedException
回调函数在处理该异常时也发生了异常,即嵌套异常。在内核中发生将会停止系统的运行
(4) ExceptionCollidedUnwind
回调函数在进行异常展开操作时发生了异常。展开操作可以理解为恢复发生异常的第一现场,并在恢复过程中的系统资源进行回收。展开操作由系统在处理异常时进行,用户自定义的回调函数通常不返回这个值。
后两个返回值一般只见于系统内部的处理过程,用户自定义的回调函数只返回前两个值。
由书作者给出的程序,放入 X64 Dbg 中可知,之前在 ReverseCore 中看到的 SEH 的例子也是由汇编代码写出来,即从系统层面执行 SEH,并没有使用高级语言。
SEH 链的最后一个函数是系统设置的终结处理函数。
异常处理函数的栈展开(RtlUnwind函数)
ExceptionRecord 结构的 ExceptionFlags 成员有三个值 0,1,2,分别代表 可修复异常,不可修复异常,栈展开
栈展开:所有回调函数都不处理异常时,系统在终结程序之前会调用发生异常的线程中所有注册的回调函数(被调用的函数执行自定义的操作,调用顺序是:从Fs:[0]开始依次到目前正在执行的异常处理函数之前,不包括当前回调函数)。在开始调用前,ExceptionFlags 会被置为 2,ExceptionCode 会被置为 STATUS_UNWIND(0x0C0000027),回调的目的是给程序一个清理占用的资源以及保存关键变量的值等等操作。利用汇编代码写 SEH 时可以不执行这个函数。
栈展开的另一个目的是,如果在调用了多重函数并在内层函数遇到异常,返回到最外层函数后,经过一系列的 Push 操作,而再次遇到异常时,由于 Fs:[0] 的值指向的还是原来的栈地址,会造成程序出现问题。
MSC 编译器对线程异常处理的增强
上面那些例子都是用汇编语言写的。但是,这个操作过程是极其不便的,尤其对高级语言来说,直接操作寄存器、读写fs[0]并不合适且非常烦琐。为此,各主流编译器都对 SEH 机制进行了扩充和增强,使程序员能更简便地使用异常处理机制。所以,在现实程序设计中,除了保护壳、反调试等特殊用途,基本上没有直接使用系统的SEH 机制,而是使用编译器提供的增强版本。C语言是Windows操作系统的开发语言,下面就看看微软的 C 编译器(MSC)提供的增强版本异常处理机制。
typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;
//异常回调函数原型 typedef EXCEPTION_DISPOSITION
(__cdecl *PEXCEPTION_ROUTINE)( struct _EXCEPTION_RECORD *__ExceptionRecord,
void *_EstablisherFrame, struct _cONTEXT *_ContextRecord,
void *_DispatcherContext );
//C/C++运行库使用的_SCOPETABLE_ENTRY 结构
typdef struct_SCOPETABLE_ ENTRY { DWORD EnclosingLevel; //上一层__try块 PVOID FilterFunc; //过滤表达式 PVOID HandlerFunc; //_except 块代码或 _finally 块代码 }SCOPETABLE_ENTRY, *PSCOPETABLE_ENTRY;
struct _EH3_EXCEPTION_REGISTRATION {
struct _EH3_EXCEPTION_REGISTRATION *Next;
PVOID ExceptionHandler;
PSCOPETABLE_ENTRY scopeTable;
DWORD TryLevel;
};
// C/C++编译器扩展 SEH 的异常帧结构
struct CPPEH_RECORD
{
DWORD old_esp:
EXCEPTION_POINTERS *exc_ptr;
struct_EH3_EXCEPTION_REGISTRATION registration;
};
编译器真正使用的结构是 CPPEH_RECORD,其成员 _EH3_EXCEPTION_RECISTRATION 结构是对原始的 SEH结构 _EXCEPTION_RECISTRATION_RECORD 的扩充。该结构在原始版本的基础上增加了4个域成员(ScopeTable、Trylevel、old_esp、exc_ptr )来支持它的增强功能。
__try { /*可能产生异常的代码*/ } __except( /*异常筛选代码*/ FilterFunc() } { /*异常处理代码*/ ExceptionHandler(); }
__try
{
/*可能产生异常的代码*/
}
__fina1ly
{
/*终结处理代码*/
FinallyHandler();
}
__try / __finally 暂时可以看成 __try /__except 模型的一个特例,它本身不能处理异常,但是不管有没有发生异常,__finally 块总会被执行,通常用来进行一些收尾和清理工作。
现在程序员再进行异常处理就非常简单了,只要把可能产生异常的代码用 __try 包裹起来,然后在 FilterFunc 中判断是不是预料之中的异常即可。如果是,在 ExceptionHandler 中进行处理就可以了。而且,__try / __except ( __finally ) 结构可以进行多层嵌套,每层相应地只处理自己关心的异常。
这里的 FilterFunc 称为异常筛选器,它实际上是一个逗号表达式(注意,逗号表达式最后的值取决于最右边的表达式值,因为逗号表达式从左至右计算)。我们可以在这里完成任何工作——哪怕是计算圆周率,只要最后返回符合要求的结果就可以了。
正常来说__except 的表达式内的函数通过调用 GetExceptionCode 或 GetExceptionInformation 函数获取异常的详细信息,对特定的异常进行处理返回不同的值。通常有3种返回值,示例如下。
#define EXCEPTION_EXECUTE_HANDLER 1 表示该异常在预料之中,请直接执行下面的 Exception #define EXCEPTION_CONTINUE_SEARCH 0 表示不处理该异常,请继续寻找其他处理程序 #define EXCEPTION_CONTINUE_EXECUTION -1 表示该异常已被修复,请回到异常现场再次执行
后面给的示例代码直接在 Printf 函数后面跟上了返回值,省去了进一步的调用和返回,_except 根据表达式的值判断进行什么操作,所以有点不太好理解。
编译器的 SEH 增强设计
按照原始版本的设计,每一对“触发异常/处理异常”都有一个注册信息,即 EXCEPTION_REGISTRATION_RECORD (以下简称“ERR结构”)。也就是说,按照原始设计,每一个__tryl_except(_finally)都应该对应于一个 ERR 结构。但是,而 MSC 编译器的实现是:每个使用_tryl_except (_finally)的函数,不管其内部嵌套或反复使用多少_tryl_exeept ( _finally),都只注册1遍,即只将1个ERR 结构挂入当前线程的异常链表中。每对 __try / _except(_finally) 称为一个 Try 块。对于递归函数,每次调用都会创建一个ERR结构,并将其挂入线程的异常链表。
对于多个 Try 块而只有一个 Handler, MSC 提供一个代理函数,即 _EH3_EXCEPTION_RECISTRATION:ExceptionHandler 被设置为 MSC 的某个库函数。开发人员提供的多个 __exeept 块被存储在 _EH3_EXCEPTION_REGISTRATION::ScopeTable 数组中。
在由 VC6.0 编译生成的程序中,这个由编译器提供的异常处理函数叫作 _except_handler3 ,它实际上是公共库的一部分,系统的每个 DLL 里都有这个函数的实现。根据编译设置的不同,它的具体实现可能出现在 msvcrt.dl、kernel32.dll 这样的公共库中,也可能直接内联到 exe 本身,但具体代码都是一样的。这样,当异常发生时,系统会根据 SEH 链表找到_except_handler3 函数并执行它,_except_handler3 再根据编译时生成的 ScopeTable 执行开发人员提供的 FilterFunc 和 HandlerFunc ,也就是说,编译器提供的异常处理函数实现了一层代理的工作。
编译器的实际工作
① 把 FilterFunc 和 HandlerFunc 编译成单独的函数(FilterFunc 代码块和 Finally 代码块的尾部都有 retn 指令;而 HandlerFunc 尾部有 Jmp 指令,它跳转到 Try 块的结束;它们都是以函数的形式被调用的),并按照 Try 块出现的顺序及嵌套关系生成正确的 ScopeTable。
编译器对 SCOPETABLE_ENTRY 是这样定义的:对 __try / __except 组合来说,其中的 FilterFunc 就是过滤表达式代码块,HandlerFunc 就是异常处理代码块;对 __try / __finally 组合来说,FilterFunc 被置为 “NULL”, HandlerFunc 就是终结处理代码块。
对于函数中的每一个 Try 块,编译器都会生成一个 SCOPETABLE_ENTRY ,并按照 Try 块的出现顺序确定各个 Try 块的索引,索引值遵从C语言的习惯(从0开始)。在执行对应的 Try 块之前,编译器会把当前 Try 块的索引保存到 _EH3_EXCEPTION_RECISTRATION 结构的 TryLevel 成员中,它是函数中当前 Try 块的索引。每个 SCOPETABLE_ENTRY 结构中的 EnclosingLevel 则表示:如果当前 Try 块未能处理异常,那么要寻找下一级 Try 块(也就是包含当前 Try 块的父 Try 块)的索引。如果它的值是 -1,表示当前块已经没有父块了,当前函数已经不能处理该异常了。同时,-1也是 TryLevel 的默认值。
② 在函数开头布置 CPPEH_RECORD 结构,并安装 SEH 异常处理函数。在由 VC 6.0 编译的程序中,这个函数的名字是 _except_handler3。如果想详细分析 _except_handler3 函数,了解栈中的数据布局是非常有必要的。
③ 在进入每个 Try 块之后,先设置当前 Try 块的 TryLevel 值,再执行 Try 块内的代码。退出 Try 块保护区域后,恢复为原来的 TryLevel.
④ 在函数返回前,卸载 SEH 异常处理函数。
VOID TwoException() { int *pValue = NULL ; __try { printf("In Try0.\n"); __try { printf("In Try1.\n"); *pValue = 0x55555555; } __except(printf("In Filter1\n"),EXCEPTION_CONTINUE_SEARCH) { printf("In Handler1.\n"); } } __except(printf("In Filter0\n"),EXCEPTION_EXECUTE_HANDLER) { printf("In Handler0.\n"); } printf("After All Trys.\n"); }
.text:00401200 sub_401200 proc near ; CODE XREF: _main+F↑p .text:00401200 .text:00401200 ms_exc= CPPEH_RECORD ptr -18h .text:00401200 .text:00401200 ; __unwind { // __except_handler3 .text:00401200 55 push ebp .text:00401201 8B EC mov ebp, esp .text:00401203 6A FF push 0FFFFFFFFh .text:00401205 68 10 71 40 00 push offset stru_407110 .text:0040120A 68 30 17 40 00 push offset __except_handler3 .text:0040120F 64 A1 00 00 00 00 mov eax, large fs:0 .text:00401215 50 push eax .text:00401216 64 89 25 00 00 00+mov large fs:0, esp .text:0040121D 83 EC 0C sub esp, 0Ch .text:00401220 53 push ebx .text:00401221 56 push esi .text:00401222 57 push edi .text:00401223 89 65 E8 mov [ebp+ms_exc.old_esp], esp .text:00401226 33 F6 xor esi, esi .text:00401228 ; __try { // __except at loc_401293 .text:00401228 89 75 FC mov [ebp+ms_exc.registration.TryLevel], esi .text:0040122B 68 B0 82 40 00 push offset aInTry0 ; "In Try0.\n" .text:00401230 E8 D1 03 00 00 call sub_401606 .text:00401235 83 C4 04 add esp, 4 .text:00401235 ; } // starts at 401228 .text:00401238 ; __try { // __except at loc_401267 .text:00401238 ; __try { // __except at loc_401293 .text:00401238 C7 45 FC 01 00 00+mov [ebp+ms_exc.registration.TryLevel], 1 .text:0040123F 68 A4 82 40 00 push offset aInTry1 ; "In Try1.\n" .text:00401244 E8 BD 03 00 00 call sub_401606 .text:00401249 83 C4 04 add esp, 4 .text:0040124C C7 06 55 55 55 55 mov dword ptr [esi], 55555555h .text:0040124C ; } // starts at 401238 .text:0040124C ; } // starts at 401238 .text:00401252 ; __try { // __except at loc_401293 .text:00401252 89 75 FC mov [ebp+ms_exc.registration.TryLevel], esi .text:00401255 EB 4C jmp short loc_4012A3 .text:00401257 ; --------------------------------------------------------------------------- .text:00401257 .text:00401257 loc_401257: ; DATA XREF: .rdata:stru_407110↓o .text:00401257 ; __except filter // owned by 401238 .text:00401257 68 98 82 40 00 push offset aInFilter1 ; "In Filter1\n" .text:0040125C E8 A5 03 00 00 call sub_401606 .text:00401261 83 C4 04 add esp, 4 .text:00401264 33 C0 xor eax, eax .text:00401266 C3 retn .text:00401267 ; --------------------------------------------------------------------------- .text:00401267 .text:00401267 loc_401267: ; DATA XREF: .rdata:stru_407110↓o .text:00401267 ; __except(loc_401257) // owned by 401238 .text:00401267 8B 65 E8 mov esp, [ebp+ms_exc.old_esp] .text:0040126A 68 88 82 40 00 push offset aInHandler1 ; "In Handler1.\n" .text:0040126F E8 92 03 00 00 call sub_401606 .text:00401274 83 C4 04 add esp, 4 .text:00401274 ; } // starts at 401252 .text:00401277 ; __try { // __except at loc_401293 .text:00401277 C7 45 FC 00 00 00+mov [ebp+ms_exc.registration.TryLevel], 0 .text:0040127E EB 23 jmp short loc_4012A3 .text:00401280 ; --------------------------------------------------------------------------- .text:00401280 .text:00401280 loc_401280: ; DATA XREF: .rdata:stru_407110↓o .text:00401280 ; __except filter // owned by 401228 .text:00401280 ; __except filter // owned by 401238 .text:00401280 ; __except filter // owned by 401252 .text:00401280 ; __except filter // owned by 401277 .text:00401280 68 7C 82 40 00 push offset aInFilter0 ; "In Filter0\n" .text:00401285 E8 7C 03 00 00 call sub_401606 .text:0040128A 83 C4 04 add esp, 4 .text:0040128D B8 01 00 00 00 mov eax, 1 .text:00401292 C3 retn .text:00401293 ; --------------------------------------------------------------------------- .text:00401293 .text:00401293 loc_401293: ; DATA XREF: .rdata:stru_407110↓o .text:00401293 ; __except(loc_401280) // owned by 401228 .text:00401293 ; __except(loc_401280) // owned by 401238 .text:00401293 ; __except(loc_401280) // owned by 401252 .text:00401293 ; __except(loc_401280) // owned by 401277 .text:00401293 8B 65 E8 mov esp, [ebp+ms_exc.old_esp] .text:00401296 68 6C 82 40 00 push offset aInHandler0 ; "In Handler0.\n" .text:0040129B E8 66 03 00 00 call sub_401606 .text:004012A0 83 C4 04 add esp, 4 .text:004012A0 ; } // starts at 401277 .text:004012A3 .text:004012A3 loc_4012A3: ; CODE XREF: sub_401200+55↑j .text:004012A3 ; sub_401200+7E↑j .text:004012A3 C7 45 FC FF FF FF+mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh .text:004012AA 68 58 82 40 00 push offset aAfterAllTrys ; "After All Trys.\n" .text:004012AF E8 52 03 00 00 call sub_401606 .text:004012B4 83 C4 04 add esp, 4 .text:004012B7 8B 4D F0 mov ecx, [ebp+ms_exc.registration.Next] .text:004012BA 64 89 0D 00 00 00+mov large fs:0, ecx .text:004012C1 5F pop edi .text:004012C2 5E pop esi .text:004012C3 5B pop ebx .text:004012C4 8B E5 mov esp, ebp .text:004012C6 5D pop ebp .text:004012C7 C3 retn .text:004012C7 ; } // starts at 401200 .text:004012C7 sub_401200 endp
需要注意,最外层的异常筛选器以及处理函数从属的代码段
他们分别覆盖了__except 代码段和编译器自己加的代码,对 TryLevel 进行赋值时也会检测其是否产生异常
而内层的异常筛选器以及处理函数:
只包含了 __except 代码段:
__except_handler3 函数流程解析
__except_handler3函数主要按以下流程工作。
① 在栈上生成一个EXCEPTION_POINTES结构,并将其保存到 [ebp-10] 处。
② 获取当前的 TryLevel,判断其值是否等于 -1。若等于,则表示当前不在 Try 块中(嵌套的里面也没有能处理的),返回 ExceptionContinueSearch,继续寻找其他异常处理程序。
③ 若 TryLevel 的值不等于-1,并根据 TryLevel 在 ScopeTable 中找到相应的 SCOPTETABLE_ENTRY,判断 FilterFunc 是否为“NULL”。若为“NULL”,说明是 __try / __finally组合。因为该组合不直接处理异常,所以也返回 ExceptionContinueSearch。
④ 若FilterFunc 不为“NULL”,说明是 __try / __except 组合,那么执行 FilterFunc,然后判断其返回值(也就是前面讲到的3种返回值),根据返回值的不同执行不同的动作。EXCEPTION_CONTINUE_SEARCH 和 EXCEPTION_CONTINUE_EXECUTION 的意义比较明确,就不多介绍了。若返回值是 EXCEPTION_EXECUTE_HANDLER,就是去执行 HandlerFunc,执行完毕会跳转到当前 Try 块的结束位置,同时表示本次异常处理结束,此时 _except_handler3 将不返回。
⑤ 如果异常没有被处理,最后会由系统默认的异常处理函数进行处理,它在展开时会调用 finally 块的代码。
因为几乎所有由 MSC 编译生成的 sys、dll 、exe文件都需要使用 _except_handler3 异常处理函数,并且都需要进行 SEH 的安装和卸载,所以编译器把这部分代码提取出来,形成了两个独立的函数,分别叫作 _SEH_prolog 和 _SEH_epilog。它们的主要作用就是把 _except_handler3 安装为 SEH 处理函数及卸载,这也是在反汇编那些使用了 SEH 的系统 API 时总会看到如下代码的原因。
更新一点版本的编译器增加了 SecurityCookie ,防止缓冲区溢出而设置的栈验证机制(即GS 保护机制),在函数开头会对栈中的 ScopeTable 使用 Cookie 进行加密,异常函数也变成了 _except_handler4。
C++ 的异常处理
多个 Catch 用于捕获多个异常
上面代码中的 stru_40DE48 实际指向了一个非常复杂的结构,它主要包含各个 try 块、 catch 块的位置信息、异常类型信息等,而 _CxxFrameHandler 的工作与 _except_handler3有很多相似之处,也需要定位发生异常的 try 块、匹配异常类型并执行相应的 catch 块。C++ 涉及各种对象的操作,复杂度比 C 要高很多。
顶层异常处理
顶层(Top-level)异常处理是系统设置的一个默认异常处理程序,所有在线程中发生的异常,只要没有被线程异常处理过程或调试器处理,最终均交由顶层异常回调函数处理。
Win XP 系统中:
进程的实际启动位置是 kemel32!BaseProcessStartTunk ,然后才跳转到 kernel32!BaseProcessStart ,它的反汇编结果如下。
.text:70817054 ; int __stdcall BaseProcessstart (PVOID ThreadStartAddress) .text:7C817054 uExitCode dword ptr -1Ch .text:7C817054 ms_exc = CPPEH_RECORD ptr -18h .text:7C817054 ThreadStartAddress = dword ptr 8 .text:7C817054 push 0ch .text:7C817056 push offset stru_7C817080 .text:7C81705B call__SEH_prolog .text:7C817060 and [ebp+ ms_exc.registration.TryLevel],0 .text:7C817064 push 4 ; ThreadInformationLength .text:7C817066 lea eax, [ebp+ ThreadstartAddress] .text:7C817069 push eax ; ThreadInformation .text:7C81706A push 9 ; ThreadInformationclass .text:7C81706C push 0FFFFFFEEh ; ThreadHandle .text:7C81706E call ds: NtSetInformationThread(x, x, x, x) .text:7C817074 call [ebp+ThreadStartAddress] .text:7C817077 push eax ; dwExitcode .text:7C817078 call ExitThread (x) .text:7C817078 __stdcall BaseProcessStart(x) endp
等同于下面的 C 代码
在使用 CreatThread 函数创建线程时,线程运行的起点是 kernel!BaseStartThunk ,而后跳转到 kernel32!BaseThreadStart,并由该函数执行 ThreadProc 。BaseThreadStart 也包含上述的异常处理代码,并且二者的 FilterFunc 都是 kernel!UnhandledExceptionFilter 。操作系统在执行任意一个用户线程(不管是不是主线程)之前,都已经为它安装了一个默认的 SEH 处理程序,这是该线程的第1个 SEH 处理程序,即顶层异常处理程序。
(1)对预定错误的预处理
①检测当前异常中是否是嵌套了异常,即异常处理的过程中是否又产生了异常。由于在这种情况下已经很难恢复现场和执行后续的异常处理过程了,UEF 函数会直接调用 NtTerminateProcess 结束当前进程。这大概解释了为什么明明设置了错误报告但是某些程序在出错退出时却依然悄无声息这个问题。
② 检测异常代码是不是 EXCEPTION_ACCESS_VIOLATION (0xc0000005),以及引起异常的操作是不是写操作。如果是,会进一步检测要写入的内存位置是否在资源段中,然后通过改变页属性来尝试修复该错误。
③ 检测当前进程是否正在被调试,这是通过查询当前进程的 DebugPort 实现的。如果进程正在被调试,那么 UEF 函数会打印一些调试信息并返回 EXCEPTION_CONTINUE_ SEARCH,也就是不进行后续的终结处理。由于这已经是最后一个异常处理程序了,该返回值会导致异常进行第2次分发。如果想调试后面的代码,在这里必须通过调试器干预 UEF 的查询结果,使它认为调试器不存在。例如,使 NtQueryInformationProcess 函数返回失败,或者使查询到的 ProcessDebugPort 值为0。
(2)调用用户设置的回调函数
为了在 UEF 阶段给用户一个干预的机会、微软提供了一个API函数 SetUnhandledExceptionFilter 。用户设置一个顶层异常过滤回调函数,在 kernel32!UnhandledExceptionFilter 中会调用它并根据它的返回值进行相应的操作,平时所说的“顶层异常回调函数”指的就是这个回调函数,而不是 UEF 函数。该API原型及参数类型定义如下。
API 函数 kernel32!SetUnhandledExceptionFilter 实际上把用户设置的回调函数地址加密并保存在一个全局变量 kernel32!BasepCurrentTopLevelFilter 中,因此:
① 不管调用这个 API 多少次,只有最后一次设置的结果才是有效的,所以在同一时刻每个进程只可能有一个有效的顶层回调函数。有些程序为了保证自己设置的异常处理过滤函数不会被其他模块覆盖,会在调用该函数后对其入口进行Patch,使它不再执行实际功能,这样就保证了不会有其他模块能够修改这个回调函数。
② 因为系统在创建用户线程时总会安装顶层异常处理过程,并把 UEF 函数作为异常过滤函数,所以该全局变量不仅对所有已经创建了的线程有效,对那些尚未“出生”的线程同样有效。这就为什么顶层异常处理是基于 SEH 和线程的,而它的有效范围却是整个进程。
UEF 函数会判断用户有没有设置回调函数,如果设置了就会进行调用。由于实际的异常过滤函数是 UEF 函数,用户设置的回调函数只是它的一个子函数调用,回调函数的返回值只在某些情况下等于UEF 函数的返回值。异常过滤函数有3种有效的返回值。
• EXCEPTION_EXECUTE_HANDLER:表示异常已经被顶层异常处理过程处理了,这会使异常处理程序执行 HandlerFunc,也就是退出当前线程(服务程序)或进程(非服务进程),操作系统不会出现非法操作框。如果在回调函数中已经做了必要的收尾工作,可利用返回该值来优雅地结束程序。
• EXCEPTION_CONTINUE_EXECUTION:表示顶层异常处理过程处理了异常,程序应该从原异常发生的指令处继续执行(Contex 结构体)。如果回调函数要这么做,那么在返回之前应该做出必要的修复异常现场的动作,这一点与普通的SEH处理程序是一样的。
• EXCEPTION_CONTINUE_SEARCH:表示顶层异常处理过程不能处理异常,需要将异常交给其他异常处理过程继续处理,这一般会导致调用操作系统默认的异常处理过程,也就是第3阶段的终结处理过程。
(3)终结处理如何进行严重依赖用户的设置
① 检查应用程序是否使用 API SetErrorMode() 设置了 SEM_NOGPFAULTERRORBOX 标志。如果设置了,就不会出现任何错误提示,直接返回 EXCEPTION_EXECUTE_HANDLER 以结束进程。
② 判断当前进程是否在 Job 中。如果在且设置了有未处理异常时结束,将直接结束进程
③ 读取用户关于JIT调试器(Just-In-Time,即时调试器)的设置,它保存在注册表中。设置相对应的值可以带命令行参数的将调试器附加到出错的进程上
④ 如果经过查询不需要,会加载 faultrep.dll,以异常信息为参数调用 ReportFault 函数,根据组策略的设置的不同,弹出不同类型的提示窗口。如果根据设置,不需要启动错误报告程序,ReportFault 会直接返回。这时,会调用系统服务 NtRaiseHardError,由子系统进程 csrss.exe 进行相关操作
看下作者写的测试代码:
// NoSEH.cpp : Defines the entry point for the console application. // /*----------------------------------------------------------------------- 第8章 Windows下的异常处理 《加密与解密(第四版)》 (c) 看雪学院 www.kanxue.com 2000-2018 -----------------------------------------------------------------------*/ #include "stdafx.h" #pragma comment(linker,"/Entry:main") #pragma comment(linker,"/subsystem:windows") #pragma comment(linker,"/entry:main") // 需要手动添加这三个库文件,本来在 代码生成 -> 运行库中可以选择 // 但经过测试,发现都不行,手动添加反而可以 #pragma comment(lib, "msvcrtd.lib") #pragma comment(lib, "vcruntimed.lib") #pragma comment(lib, "ucrtd.lib") __declspec(naked) void main(void) { __asm { mov dword ptr fs:[0],-1 xor eax,eax mov [eax],5 //向0地址写入数据,引发内存访问异常 retn } }
注意:若没有连接微软的符号服务器,将无法显示出正确的符号。
我的 Win XP sp3 中显示出如下的代码
下面的调试代码在 Win7 x64 中得到:
可以看到,程序会先将异常筛选器指针地址解码,然后通过 call eax 调用此函数。从 Windows Vista 开始,线程的实际入口点变成了 ntdll!RtlUserThreadStart (不再位于 kernel32.dlI 中)。该函数直接跳转到了 ntdll!_RtIUserThreadStart,其内部调用了 RtIInitializeExceptionChain 函数(W),该函数与 SEHOP 保护机制有关。
从以上代码可以看到:从Windows Vista开始,UEF 的设置变成了两级设置。相应地就有两级接口,分别是 kernel32!SetUnhandledExceptionFilter 和ntdlI!RtlSetUnhandledExceptionFilter。当然,ntdll.dll 的接口不是公开的,用户仍然只能使用 kernel32!SetUnhandledExceptionFilter 这个 API 来设置顶层回调函数。但是, kernel32.dll 在加载的时候会调用 ntdll!RtlSetUnhandledExceptionFilter 把回调函数设置成自己模块内的 UnhandledExceptionFilter 函数。因此,异常到达顶层异常处理程序后,会先执行ntdll.dll 中的FilterFunc,而它会继续调用 kernel32!UnhandledExceptionFilter (到这里就与 Windows XP 中一样了), kernel32!UnhandledExceptionFilter 会再调用用户设置的顶层异常回调函数。有一种特殊的情况是,如果某个 Native 程序并没有加载 kernel32.dll,那么 ntdll.dll 本身仍然会提供一个与 kernel32!UnhandledExceptionFilter 功能相似的 ntdll!RtlUnhandledExceptionFilter 函数来完成终结处理功能。
顶层异常处理的典型应用模式
在程序设计中,开发人员普遍使用 SEH 机制来捕获可能产生的异常。但 SEH 不是万能的,总会有一些无法预料的情况发生,却不能被 SEH 处理。所以,一般的使用模式是:使用 SEH 捕获异常,并对那些预料之中的异常进行处理,其他无法处理的异常都会到达 UEF 函数处,由用户设置的回调函数进行收尾处理。
于是可以把异常现场的所有信息保存下来,形成一个快照文件,作为分析异常的依据(Dump文件)。用户可以手动把这个文件发送给开发人员,或者将文件自动上传到专门用于收集 Dump 的服务器中(通常还会生成一个文本类的文件用于保存本次异常的一些概要信息和那些无法保存到 Dump 文件中的信息)。“拍照”时的参数不同,生成的 Dump 文件所包含信息的丰富。
// WriteDump.cpp : Defines the entry point for the console application. // Author:achillis // 本程序用于演示顶层异常处理的使用及生成Dump技术 // /*----------------------------------------------------------------------- 第8章 Windows下的异常处理 《加密与解密(第四版)》 (c) 看雪学院 www.kanxue.com 2000-2018 -----------------------------------------------------------------------*/ #include "stdafx.h" #include <WINDOWS.H> #include <DbgHelp.h> #pragma comment(lib,"Dbghelp.lib") LONG WINAPI TopLevelExceptionFilter( struct _EXCEPTION_POINTERS* ExceptionInfo ); int main(int argc, char* argv[]) { //安装顶层异常处理回调 SetUnhandledExceptionFilter(TopLevelExceptionFilter); int *pValue = NULL; *pValue = 5 ; //引发内存访问异常 return 0; } LONG WINAPI TopLevelExceptionFilter( struct _EXCEPTION_POINTERS* ExceptionInfo ) { printf("Exception Catched, Code = 0x%08X EIP = 0x%p\n", ExceptionInfo->ExceptionRecord->ExceptionCode, ExceptionInfo->ExceptionRecord->ExceptionAddress); printf(".exr = 0x%p\n",ExceptionInfo->ExceptionRecord); printf(".cxr = 0x%p\n",ExceptionInfo->ContextRecord); HANDLE hDumpFile = CreateFile("Dump.dmp", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hDumpFile == INVALID_HANDLE_VALUE) { return EXCEPTION_CONTINUE_SEARCH; } MINIDUMP_EXCEPTION_INFORMATION MinidumpExpInfo; ZeroMemory(&MinidumpExpInfo,sizeof(MINIDUMP_EXCEPTION_INFORMATION)); MinidumpExpInfo.ThreadId = GetCurrentThreadId(); MinidumpExpInfo.ExceptionPointers = ExceptionInfo; MinidumpExpInfo.ClientPointers = TRUE ; BOOL bResult = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithProcessThreadData, &MinidumpExpInfo, NULL, NULL ); printf("Write Dump File %s .\n",bResult ? "Success":"Failed"); CloseHandle(hDumpFile); //Dump文件生成完毕,可以结束进程了 return EXCEPTION_EXECUTE_HANDLER; }