Loading... # Windows核心编程-学习笔记13 SEH:**结构化异常处理**(structed exception handling),包含两方面的功能: 1. 终止处理 2. 异常处理 ## 终止处理程序 终止处理程序确保不管一个代码块(被保护代码)是如何退出的,另一个代码块(终止处理程序),总会得以调用和执行 ~~~c++ __try { // Guarded body } __finally { // Termination handler } ~~~ 注意在循环里的try中提前退出包括continue、break等,也会进入finally,之后循环内尚余的代码就不会执行了。所以最好在try、finally中不要包含return、continue、break、goto语句 ## 异常处理程序与软件异常 异常处理程序如下,只要没有异常,except里面的都不会执行 ~~~zhu __try { // Guarded body } __except (exception filter) { // Exception handler } ~~~ 注意try后不能同时有finally块和except块,也不能有多个finally块或except块,但可以互相嵌套 ### EXCEPTION_EXECUTE_HANDLER(1) EXCEPTION_EXECUTE_HANDLER告诉系统程序知道这个异常,并预计这个异常会在某种情况下发生,可以执行代码来处理它 try-except很容易造成漏洞,比如在try中加入stcpy,加入抛出异常却不知道什么原因,比如可能是参数为NULL或非法地址,或者缓冲区大小不够等等,这时候如果处理了异常,进程继续执行,但却是在有受损状态下执行,可能会导致程序稍后因为不明原因崩溃,更有可能造成容易被利用的安全漏洞。 **全局展开**:try抛出后,检查异常处理者;找不到就继续找外面一层的try代码块 ### EXCEPTION_CONTINUE_EXECUTION(-1) EXCEPTION_CONTINUE_EXECUTION表示从异常发生点继续执行 实际上可以调用一个函数来决定返回异常标识符中的哪一个,但如果选择EXCEPTION_CONTINUE_EXECUTION要谨慎,因为不知道从哪条指令重新执行,可能会造成新的异常。例如下面汇编是`*pchBuffer=TEXT('J')`的汇编,pchBuffer没有初始化而是指向了NULL ~~~assembly MOV EAX, DWORD PTR[pchBuffer] MOV WORD PTR[EAX], 'J' ~~~ 这时第二条指令抛出异常,pchBuffer指向的是NULL,就算经过except调用函数的处理修复,pchBuffer虽然不再指向NULL,但寄存器EAX依然是NULL,所以陷入死循环。 ### EXCEPTION_CONTINUE_SEARCH(0) EXCEPTION_CONTINUE_SEARCH表示不处理,继续向上找别的try处理块 ### GetExceptionCode 该函数可以返回发生异常的类型 下面列举一些异常 **与内存相关的异常**: * EXCEPTION_ACCESS_VIOLATION:线程试图读写一个虚拟内存地址,但在这个地址并不具备相应权限 * EXCEPTION_DATATYPE_MISALIGNMENT:线程试图从没有提供自动对齐机制的硬件中读写没有对齐的数据 * EXCEPTION_ARRAY_BOUNDS_EXCEEDED:线程在支持边界检查的硬件上访问越界的数组元素 * EXCEPTION_IN_PAGE_ERROR:因文件系统或设备驱动返回一个读取错误,造成页面错误不能满足 * EXCEPTION_GUARD_PAGE:线程试图访问具有PAGE_GUARD保护属性的内存页。该内存页是可访问的,但同时引发了EXCEPTION_GUARD_PAGE异常 * EXCEPTION_STACK_OVERFLOW:线程耗尽了系统分配给它的栈空间 * EXCEPTION_ILLEGAL_INSTRUCTION:线程执行了一条无效指令。这个异常由具体的CPU架构所定义,在不同CPU上执行一条无效指令可能会导致陷阱错误 * EXCEPTION_PRIV_INSTRUCTION:线程试图执行在当前机器模式下不允许的指令 **与异常本身相关的异常**: * EXCEPTION_INVALID_DISPOSITION:异常过滤程序返回EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_EXECUTION、EXCEPTION_CONTINUE_SEARCH之外的值 * EXCEPTION_NONCONTINUABLE_EXCEPTION:异常过滤程序返回EXCEPTION_CONTINUE_EXECUTION以响应不可继续的异常 **与调试相关的异常**: * EXCEPTION_BREAKPOINT:执行到一个断点 * EXCEPTION_SINGLE_STEP:由跟踪中断或者其他单步执行机制发出的一条指令已经执行完的信号 * EXCEPTION_INVALID_HANDLE:向函数传入了一个无效句柄 **与整型相关的异常**: * EXCEPTION_INT_DIVIDE_BY_ZERO:线程试图在整数除法运算中除以0 * EXCEPTION_INT_OVERFLOW:整型运算的结果超过了该类型规定的范围 **与浮点类型相关的异常**: * EXCEPTION_FLT_DENORMAL_OPERAND:浮点运算的其中一个操作数是非规格化值,太小以至于不能作为一个标准浮点值 * EXCEPTION_FLT_DIVIDE_BY_ZERO:线程试图在浮点除法运算中以浮点数0作为除数 * EXCEPTION_FLT_INEXACT_RESULT:浮点运算的结果不能精确地表示为十进制小数 * EXCEPTION_FLT_INVALID_OPERATION:表示任何没有在此列出的其他浮点数异常 * EXCEPTION_FLT_OVERFLOW:浮点运算结果的指数部分超过了该类型允许的最大值 * EXCEPTION_FLT_STACK_CHECK:浮点运算造成栈上溢或下溢 * EXCEPTION_FLT_UNDERFLOW:浮点运算结果的指数部分小于该类型允许的最小值 注意,GetExceptionCode只能在异常过滤程序中(__except之后的圆括号中)或者异常处理程序的代码中调用,但不能在异常过滤程序的函数中调用 ### GetExceptionInformation 异常发生时,操作系统向发生异常的线程的栈中压入三个结构:**EXCEPTION_RECORD、CONTEXT、EXCEPTION_POINTERS** * **EXCEPTION_RECORD**:包含与引发的异常有关的信息,这些信息的内容与CPU无关 * **CONTEXT**:包含关于异常但与CPU也有关的信息 * **EXCEPTION_POINTERS**:仅包含两个指针数据成员,分别指向被压入栈中的EXCEPTION_RECORD和CONTEXT结构 可以使用GetExceptionInformation来获取这些信息,该函数会返回指向一个EXCEPTION_POINTERS结构的指针 和GetExceptionCode限制类似,**GetExceptionInformation只能在异常过滤程序中调用**,一旦程序控制流被转移到异常处理程序或者别的地方,栈上的数据就被销毁了 ### 软件异常 之前讨论的都是硬件异常,即CPU捕获事件并引发异常。其实也可以在代码中强制引发异常 ~~~c++ VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, CONST ULONG_PTR *pArguments); ~~~ * dwExceptionCode:要抛出异常的标识符,最好按照标准Windows错误码格式 * 位31和位30描述严重性 * 位29是1,0为Microsoft创建的异常保留 * 位28是0 * 位27~位16是Microsoft预定义的设备代码 * 位15~位0是选择用来标识引发异常的程序段的任意值 * dwExceptionFlags:必须设为0或EXCEPTION_NONCONTINUABLE,用来指出异常过滤程序在响应这个异常时能否返回EXCEPTION_CONTINUE_EXECUTION。如果不向RaiseException传递EXCEPTION_NONCONTINUABLE标志,过滤程序就能返回EXCEPTION_CONTINUE_EXECUTION。正常情况下会导致线程重新执行引发软件异常的那条CPU指令。但Microsoft做了修改,使得程序**从RaiseException函数调用的后面继续执行**。如果传递EXCEPTION_NONCONTINUABLE,相当于告诉系统出现异常程序便不能继续,这个标志在OS内部用来传递极为严重的出错信号 * nNumberOfArguments、pArguments:传递和引发的异常有关的附加信息,可以传递NULL ## 未处理异常 如果每个异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH,会发生**未处理异常**。Windows提供了SetUnhandledExceptionFilter函数,提供了处理异常的最后机会。 ~~~c++ PTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(PTOP_LEVEL_EXCEPTION_FILTER pTopLevelExceptionFilter); ~~~ 通常应在进程初始化阶段调用该函数。一旦调用了这个函数,进程中任意线程抛出未处理异常,都会通过SetUnhandledExceptionFilter里的异常过滤函数,该函数必须符合以下原型: ~~~c++ LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo); ~~~ 返回值是三个异常标识符之一即可 #### UnhandledExceptionFilter ~~~c++ LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo); ~~~ 同样返回值是三个异常标识符之一。该函数式系统在“异常最终没人处理”时会调用的默认入口,它内部会去调用 `SetUnhandledExceptionFilter` 设置进去的那个回调(如果有) UnhandledExceptionFilter处理异常时会按照顺序执行5个步骤: 1. **允许向资源写入并继续执行**:如果是因为线程的写操作而引起访问违例,UnhandledExceptionFilter会检查该线程是不是试图修改exe或DLL模块中的资源。这些资源默认只读,试图修改他们会引发访问违例。UnhandledExceptionFilter调用VirtualProtect将资源页的保护属性设为PAGE_READWRITE,并返回EXCEPTION_CONTINUE_EXECUTION,以允许失败的指令再次执行 2. **将未处理异常报告给调试器**:UnhandledExceptionFilter首先检查应用程序是否在调试器控制之下,是则返回EXCEPTION_CONTINUE_SEARCH。此时,由于异常未处理,Windows会通知当前连接的调试器。调试器接收为这个异常生成的EXCEPTION_RECORD结构的ExceptionInformation成员,并通过这个信息来定位代码中引发异常的指令,并通知你引发了什么样的异常 3. **通知设定的全局异常过滤函数**:如果已调用SetUnhandledExceptionFilter设定了一个全局异常过滤程序,UnhandledExceptionFilter将调用它。如果过滤函数返回EXCEPTION_CONTINUE_EXECUTION、EXCEPTION_EXECUTE_HANDLER,UnhandledExceptionFilter直接返回该值给系统。如果过滤函数返回EXCEPTION_CONTINUE_SEARCH,则转到步骤4 4. **将未处理异常报告给调试器(再次)** 5. **静默终止进程** 最后修改:2026 年 01 月 20 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏