( windows 提供的异常处理机制实际上只是一个简单的框架,一般情况下开发人员都不会直接用到。我们通常所用的异常处理(比如 C++ 的 throw、try、catch)都是编译器在系统提供的异常处理机制上进行加工了的增强版本。这里先抛开增强版的不提,主要叙述原始版本。)
0x01 系统如何找到异常链表
结构化异常处理是以线程为基础的。线程的内核数据结构体现是 _ETHREAD,从它开始进入,通过windbg,找到所关注的异常链表(win7 x86):
查看 _KTHREAD结构(dt _KTHREAD):
查看TEB:
查看TIB:
_NT_TIB 的第一个域成员 ExceptionList 就是异常链表头。
但是系统不是这么一步一步找的,而是借助 FS 寄存器来加速寻找。先来说说系统对 FS 的使用。
在应用层,FS 寄存器“指向”当前执行线程的 _TEB 结构体。在内核层,FS 寄存器“指向”另一个跟 CPU 相关的结构体:_KPCR,来看看它的结构,
与 _TEB 一样,它的第一个域成员也是 _NT_TIB,只不过此时是 nt!_NT_TIB,而在应用层是 ntdll!_NT_TIB,但它们的结构是一样的。
这样,不论在应用层还是在内核层,系统都可以使用 FS:[0] 找到异常链表。
0x02 异常处理相关的结构
(1)_EXCEPTION_REGISTRATION_RECORD
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
EXCEPTION_REGISTRATION_RECORD 结构就是登记信息。数据成员:
1. EXCEPTION_REGISTRATION_RECORD::Next 域指向下一个 EXCEPTION_REGISTRATION_RECORD,由此构成一个异常登记信息(从字面上说,应该叫做“异常注册记录”更恰当)链表。链表中的最后一个结点会将 Next 置为 EXCEPTION_CHAIN_END,表示链表到此结束。
2. EXCEPTION_REGISTRATION_RECORD::Handler 指向异常处理函数。
(2)异常的回调函数
EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext);
函数参数:
1..第一个参数ExceptionRecord,是一个指向EXCEPTION_RECORD结构的指针
2.第二个参数是一个指向establisher帧结构的指针。这个异常帧结构就是_EXCEPTION_REGISTRATION_RECORD。
3.第三个参数是一个指向CONTEXT结 构的指针。此结构在WINNT.H中定义,它代表某个特定线程的寄存器值。当用于SEH时,CONTEXT结构表示 异常发生时寄存器的值。顺便说一下,这个CONTEXT结构就是GetThreadContext和SetThreadContext这两个API中使用 的那个CONTEXT结构。
4.第四个参数是_DispatcherContext,用于存放下一个异常帧,全局展开在执行当前异常帧局部展开中发生嵌套展开时使用。
typedef struct _CONTEXT { // // The flags values within this flag control the contents of // a CONTEXT record. // // If the context record is used as an input parameter, then // for each portion of the context record controlled by a flag // whose value is set, it is assumed that that portion of the // context record contains valid context. If the context record // is being used to modify a threads context, then only that // portion of the threads context will be modified. // // If the context record is used as an IN OUT parameter to capture // the context of a thread, then only those portions of the thread's // context corresponding to set flags will be returned. // // The context record is never used as an OUT only parameter. // DWORD ContextFlags; // // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is // set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT // included in CONTEXT_FULL. // DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; // // This section is specified/returned if the // ContextFlags word contians the flag CONTEXT_FLOATING_POINT. // FLOATING_SAVE_AREA FloatSave; // // This section is specified/returned if the // ContextFlags word contians the flag CONTEXT_SEGMENTS. // DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; // // This section is specified/returned if the // ContextFlags word contians the flag CONTEXT_INTEGER. // DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; // // This section is specified/returned if the // ContextFlags word contians the flag CONTEXT_CONTROL. // DWORD Ebp; DWORD Eip; DWORD SegCs; // MUST BE SANITIZED DWORD EFlags; // MUST BE SANITIZED DWORD Esp; DWORD SegSs; // // This section is specified/returned if the ContextFlags word // contains the flag CONTEXT_EXTENDED_REGISTERS. // The format and contexts are processor specific // BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; typedef CONTEXT *PCONTEXT;
(3)EXCEPTION_RECORD结构
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //操作系统提供给异常的一个数,代表异常发生的原因。 DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
参数 ExceptionCode 是操作系统分配给异常的号。在 WINNT.H 文件中查找开头为“STATUS_” 的宏就能找到一大堆这样的异常 代号。例如,大家熟知的 STATUS_ACCESS_VIOLATION 的代号就是 0xC0000005。更为完整的异常代号可以 从 Windows NT DDK 中的 NTSTATUS.H 文件里找到。
EXCEPTION_RECORD 结构体的第四个元素是异常发生处的地 址。其余的 EXCEPTION_RECORD 域目前都可以忽略掉。
0x03 处理过程
当接收到异常后,系统找到当前线程(前面说过异常是线程相关的。系统接收到的异常就是当前正在运行的线程触发的。其实这个说法还不准确,DPC 也会触发异常,而它是线程无关的,这里为了方便理解,当前先只考虑线程)的异常链表,从链表中的第一个结点开始遍历,找到一个 EXCEPTION_REGISTRATION_RECORD 就调用它的 Handler,并把该异常(由第一个类型为 EXCEPTION_RECORD 的参数表示)传递给该 Handler,Handler 处理并返回一个类型为 EXCEPTION_DISPOSITION 的枚举值。该返回值指示系统下一步该做什么:
ExceptionContinueExecution ——已修正了此异常的故障,重新执行异常产生出代码。
ExceptionContinueSearch ——没有处理此异常,请继续搜索其他的解决方案
ExceptionNestedException 和 ExceptionCollidedUnwind 暂不解释。
这样系统根据不同的返回值来继续遍历异常链表或者回到触发点继续执行。
(1)内核处理流程
在windows kernel中,存在一张中断描述符表(IDT, Interupt Descriptor Table). IDT是一张位于内核态物理内存中的线性表,其有256个表项。IDT中的每个表项叫做门描述符(Gate Descriptor)。门描述符的基本作用就是将CPU异常对应的中断号与其对应的异常处理函数KiTrapXX关联起来。
例如,0号中断(即除0错误)对应的处理例程为nt!KiTrap00
可以在windbg中用!idt -a命令查看。
首先,CPU 执行的指令触发了异常,CPU 改执行 IDT 中 KiTrapXX,KiTrapXX 会调用 KiDispatchException。该函数原型如下:
VOID KiDispatchException ( IN PEXCEPTION_RECORD ExceptionRecord, // 指向 ExceptionRecord的指针 IN PKEXCEPTION_FRAME ExceptionFrame, // 对于X86 为空 IN PKTRAP_FRAME TrapFrame, //函数异常记录块指针,由调用者 KiKernelTrapHandler 传递 IN KPROCESSOR_MODE PreviousMode, // 指示内核态还是用户态 IN BOOLEAN FirstChance // 是否是第一次尝试 );