SEH结构化异常处理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SEH结构化异常处理相关的知识,希望对你有一定的参考价值。

23.1 基础知识

23.1.1 Windows下的软件异常

(1)中断和异常

  ①中断是由外部硬件设备或异步事件产生的

  ②异常是由内部事件产生的,可分为故障、陷阱和终止三类。

(2)两种异常处理机制:SEH和VEH(WindowsXP以上新引进)

(3)结构化异常处理(SEH)是Windows操作系统提供的强大异常处理功能。而Visual C++中的__try{} __finally{}和__try{} __except{}结构本质上是对Windows提供的SEH的封装。

23.1.2 SEH的分类

(1)Per-Thread类型SEH(也称为线程异常处理),用来监视某线程代码是否发生异常。

(2)Final类型SEH(也称为进程异常处理、筛选器或顶层异常处理),用于监视整个进程中所有线程是否发生异常。在整个进程中,该类型异常处理过程只有一个,可通过SetUnhandledExceptionFilter设置。

23.1.3 SEH相关的数据结构

(1) 线程信息块TIB(Thread Information Block或TEB)

typedef struct _NT_TIB {
     struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常的链表

     PVOID StackBase;
     PVOID StackLimit;
     PVOID SubSystemTib;

     union {
         PVOID FiberData;
         DWORD Version;
     };
 
     PVOID ArbitraryUserPointer;
     struct _NT_TIB *Self;
} NT_TIB;

    Fs:[0]总是指向当前线程的TIB,其中0偏移的指向线程的异常链表,即ExceptionList是指向异常处理链表(EXCEPTION_REGISTRATION结构)的一个指针。

(2)EXCEPTION_REGISTRATION结构

typedef struct _EXCEPTION_REGISTRATION_RECORD {
     struct _EXCEPTION_REGISTRATION_RECORD *Prev; //指向前一个EXCEPTION_REGISTRATION的指针
     PEXCEPTION_ROUTINE Handler; //当前异常处理回调函数的地址
} EXCEPTION_REGISTRATION_RECORD;

(3)EXCEPTION_RECORD结构

typedef struct _EXCEPTION_RECORD {
    DWORD    ExceptionCode;  //异常码,以STATUS_或EXCEPTION_开头,可自定义。(sehdef.inc)
    DWORD ExceptionFlags;    //异常标志。0可修复;1不可修复;2正在展开,不要试图修复
    struct _EXCEPTION_RECORD *ExceptionRecord; //指向嵌套的异常结构,通常是异常中又引发异常
    PVOID ExceptionAddress;  //异常发生的地址
    DWORD NumberParameters;  //下面ExceptionInformation所含有的dword数目
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //附加消息,如读或写冲突
} EXCEPTION_RECORD;

(4)CONTEXT结构(可通过GetThreadContext和SetThreadContext来读写)

typedef struct _CONTEXT {
     DWORD ContextFlags; //用来表示该结构中的哪些域有效
     DWORD   Dr0, Dr2, Dr3, Dr4, Dr5, Dr6, Dr7; //调试寄存器
     FLOATING_SAVE_AREA FloatSave; //浮点寄存器区
     DWORD   SegGs, SegFs, SegEs, Seg Ds; //段寄存器
     DWORD   Edi, Esi, Ebx, Edx, Ecx, Eax; //通用寄存器组
     DWORD   Ebp, Eip, SegCs, EFlags, Esp, SegSs; //控制寄存器组

     //扩展寄存器,只有特定的处理器才有
     BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

(5)EXCEPTION_POINTERS结构(传递给顶层型异常处理回调函数的参数)

typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord; //指向ExceptionRecord的指针
    PCONTEXT ContextRecord; //指向Context结构的指针
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

23.1.4 异常处理相关API

(1)SetErrorMode:控制错误模式,如是否出现错误对话框

(2)SetUnhandledExceptionFilter:设定顶层异常处理回调函数

(3)RaiseException:用于引发异常,可指定自己的异常代码及相关信息

(4)GetThreadContext/SetThreadContext:获取或设置线程环境

(5)RtlUnwind:栈展开操作(后面会详细介绍)

23.1.5 顶层异常处理

(1)顶层异常处理是进程相关的,只要没有由线程处理过程或调试器处理掉,最终均要交由顶层异常回调函数处理

(2)注册顶层异常处理:SetUnhandledExceptionFilter,每个进程只能注册一个。返回值为前一个异常回调函数的地址。

(3)回调函数

LONG UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *ExceptionInfo){

   //三种返回值,决定系统下一步的动作
   //1.EXCEPTION_EXECUTE_HANDLE(1):表示异常己被处理。程序可以优雅结束
   //2.EXCEPTION_CONTINUE_SEARCH(0):表示顶层异常处理不能处理,需交给其他异常处理过程
   //3.EXCEPTION_CONTINUE_EXECUTION(-1):表示顶层异常过程处理了异常,且程序应从原异常发生的指令重新继续执行一下。(可以通过改变CONTEXT内容来达到改变程序执行环境)
}

23.1.6 线程异常处理(局部的,仅仅监视进程中某特定线程是否发生异常)

(1)线程异常处理特点

  ①Windows系统为每个线程单独提供了一种异常处理的方法,当一个线程出现错误时,操作系统调用用户定义的一系列回调函数,在这些回调函数中,可以进行修复错误或其它的一些操作,最后的返回值告系统系统下一步的动作(如继续搜索异常处理程序或终止程序等)。

  ②SEH是基于线程的,使用SEH可以为每个线程设置不同的异常处理程序(回调函数)而且可以为每个线程设置多个异常处理程序。

  ③ 由于SEH使用了与硬件平台相关的数据指针,所以不同硬件平台使用SHE的方法有所不同。

(2)回调函数原型

EXCEPTION_DISPOSITION __cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,//指向包含异常信息的EXCEPTION_RECORD结构 void* EstablisherFrame,//指向该异常相关的EXCEPTION_REGISTRATION结构 struct _CONTEXT *ContextRecord,//指向线程环境CONTEXT结构的指针 void* DispatcherContext){ //该域暂无意义 …… //4种返回值及含义 //1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。 //2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。 //3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常 //4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作 return … }

(3)线程异常处理的注册

push _exception_handler //异常回调函数_exception_handler的地址,即handler
push fs:[0]             //保存前一个异常回调函数的地址,即prev
mov fs:[0],esp          //安装新的EXCEPTION_REGISTRATION结构(两个成员:prev,handler)。
                        //此时栈顶分别是prev和handler,为新的EXCEPTION_REGISTRATION结
                        //构,mov fs:[0],esp,就可以让fs:[0]指向该指构。

(4)异常回调函数的调用过程

技术分享 

  ①线程信息块(TIB),永远放在fs段选择器指定的数据段的0偏移处,即fs:[0]的地方就是TIB结构。对不同的线程fs寄存器的内容有所有不同,但fs:[0]都是指向当前线程的TIB结构体,所以fs:[0]是一个EXCEPTION_REGISTRATION结构体的指针。

  ②当异常发生时,系统从fs:[0]指向的内存地址处取出ExceptionList字段,然后从ExceptionList字段指向的EXCEPTION_REGISTRATION结构中取出handler字段,并根据其中的地址去调用异常处理程序(回调函数)。

【MySeh程序】安装线程异常处理程序

技术分享

/************************************************************************
MYSEH - Matt Pietrek 1997
Microsoft Systems Journal,January 1997
cl.exe myseh.cpp /link -SAFESEH:NO
用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置)
用命令行CL MYSHE.CPP编译里可参考下面的MySeh.bat批处理的设置。
************************************************************************/
//MySeh.bat
//set PATH = %PATH%; E:\读书笔记\Windows核心编程\SEH\SEH; D:\VisualStudio\VC\bin
//set include = C:\Program Files\Windows Kits\8.1\Include\um; C:\Program Files\Windows Kits\8.1\Include\shared; D:\VisualStudio\VC\include
//set lib = C:\Program Files\Windows Kits\8.1\Lib\winv6.3\um\x86; D:\VisualStudio\VC\lib
//cl.exe E : \读书笔记\Windows核心编程\SEH\SEH\myseh.cpp / link - SAFESEH:NO
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

DWORD scratch;

EXCEPTION_DISPOSITION __cdecl _except_handler(struct _EXCEPTION_RECORD *ExceptionRecord,
                                              void* EstablisherFrame,
                                              struct _CONTEXT *ContextRecord,
                                              void* DispatcherContext){

    //指明是我们让流程转到我们的异常处理程序的
    printf("进入Seh异常处理程序!\n");

    //改变CONTEXT结构体EAX的值,以便它指向可以成功进行写操作的位置
    ContextRecord->Eax = (DWORD)&scratch;

    printf("异常处理完毕!\n");

    //告诉操作系统重新执行出错的指令
    return ExceptionContinueExecution;
}

int main(){

    DWORD handler = (DWORD)_except_handler;

    printf("安装seh异常回调函数!\n");
    __asm{
    
        //创建EXCEPTION_REGISTRATION结构:
        push handler  //handler函数的地址
        push fs:[0]   //保存前一个handler函数地址,即EXCEPTION_REGISTRATION结构的prev

        mov fs:[0],ESP  //安装新的EXCEPTION_REGISTRATION结构

    }
    printf("安装完毕,向0地址写入数据,引发异常!\n");
    __asm
    {
        xor eax, eax                    //EAX = 0
        mov dword ptr[eax], 1234h    //写EAX指向的内存从而故意引发一个异常!
    }
    printf("写入数据完毕,scratch=0x%x!\n",scratch);
    printf("卸载seh自定义的异常回调函数!\n");
    __asm{
        // 移去我们的EXECEPTION_REGISTRATION结构
        mov eax, [ESP]    // 此时栈顶为prev,后进先出原理。获取前一个EXECEPTION_REGISTRATION结构的指针
        mov fs:[0], EAX // 安装前一个结构
        add esp, 8       // 将我们的EXECEPTION_REGISTRATION弹出堆栈
    }

    system("Pause");
    return 0;
}

(5)SEH链及异常的传递(通知调试器→SEH链→顶层异常处理→系统默认处理)

技术分享 

(1)系统查看产生异常的进程是否被正在被调试,如果正在被调试,那么向调试器发送EXCEPTION_DEBUG_EVENT事件。

(2)如果进程没有没有被调试或者调试器不去处理这个异常,那么系统检查异常所处的线程并在这个线程环境中查看fs:[0]来确定是否安装SEH异常处理回调函数,如果有则调用它。

(3)回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。

(4)如果回调函数返回ExceptionContinueSearch,相当于告诉系统它无法处理这个异常,系统将根据SEH链中的prev字段得到前一个回调函数地址并重复步骤3,直至链中的某个回调函数返回ExceptionContinueExection为止,查找结束。

(5)如果到了SEH链的尾部却没有一个回调函数愿意处理这个异常,那么系统会再被检查进程是否正在被调试,如果被调试的话,则再一次通知调试器。

(6)如果调试器还是不去处理这个异常或进程没有被调试,那么系统检查有没有Final型的异常处理回调函数,如果有,就去调用它,当这个回调函数返回时,系统会根据这个函数的返回值做相应的动作。

(7)如果没有安装Final型回调函数,系统直接调用默认的异常处理程序终止进程,但在终止之前,系统再次调用发生异常的线程中的所有异常处理过程,目的是让线程异常处理过程获得最后清理未释放资源的机会,其后程序终止。

 【MySEH2】SEH链及异常传递

 

技术分享

/************************************************************************
MYSEH2 - Matt pietrek 1997
Microsoft Systems Journal,january 1997
File: MySEH2.cpp
用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置)
************************************************************************/
#include <windows.h>
#include <stdio.h>

//异常处理回调函数
EXCEPTION_DISPOSITION __cdecl _except_handler(
       struct _EXCEPTION_RECORD *ExceptionRecord,
       void * EstablisherFrame,
       struct _CONTEXT *ContextRecord,
       void * DispatcherContext){
    //显示异常信息
    printf("HomeGrown异常处理回调函数:异常码(%08X) 标志(%X)", 
           ExceptionRecord->ExceptionCode,ExceptionRecord->ExceptionFlags);
    if (ExceptionRecord->ExceptionFlags & 1)
        printf(" EH_NONCONTINUABLE");

    if (ExceptionRecord->ExceptionFlags & 2)
        printf(" EH_UNWINDING");

    if (ExceptionRecord->ExceptionFlags & 4)
        printf(" EH_EXIT_UNWIND");

    if (ExceptionRecord->ExceptionFlags & 8) //
        printf(" EH_STACK_INVALID");

    if (ExceptionRecord->ExceptionFlags & 0x10) //
        printf(" EH_NESTED_CALL");
    
    printf("\n");

    //我们不想处理这个异常,让其它SEH链的其他回调函数去处理
    return ExceptionContinueSearch;
}

void HomeGrownFrame(void){
    DWORD  handler = (DWORD)_except_handler;

    __asm{
        //创建EXCEPTION_REGISTRATION结构
        push handler //handler函数的地址
        push FS :[0] //保存前一个handler地址
        mov FS :[0],ESP //安装新的EXCEPTION_REGISTRATION结构
    }

    *(PDWORD)0 = 0; //向地址0数据,从而引发一个异常

    printf("_except_handle函数不处理异常,此句永远不会被执行!\n");

    __asm{
        //移去我个的EXCEPTION_REGISTRATION结构
        mov eax, [ESP]   //获取前一个结构
        mov FS : [0],eax //安装前一个结构
        add esp,8        //把我们的EXCEPTION_REGISTRATION结构弹出
    }
}

int main(){
    //为了简化代码,使用编译器层面的异常处理,即__try{} __excetp{}
    __try{
        HomeGrownFrame();
    }
    __except (EXCEPTION_EXECUTE_HANDLER){  //这里处理了异常,不再向外层传递
        printf("主函数main中的异常处理过程\n");
    }

    return 0;
}

 

以上是关于SEH结构化异常处理的主要内容,如果未能解决你的问题,请参考以下文章

SEH结构化异常处理

结构化异常SEH处理机制详细介绍(二)

windows 异常处理中VEHSEHUEHVCH 之间的关系

深入解析结构化异常处理

第24章 SEH结构化异常处理—异常处理及软件异常

第25章 SEH结构化异常处理_未处理异常及向量化异常