一种通用跨平台实现SEH的解决方案

一. 前言

众所周知,在软件的代码中,处理软件本身的逻辑只要大约1/3的代码,另外2/3的代码实际上是在处理各种各样的异常情况。

这些异常情况一方面是因为不同用户之间不同的硬件软件环境要处理。另一方面是程序中可能出现的bug。比较典型的情况就是因为读取不到特定的信息,导致计算结果错误最后访问非法地址引起异常。

对于这些情况我们可以增加代码中的条件检查,增加大量的if判断来确定有没有出错,但是最会导致如果出错在一个比较深的子函数中,需要从多层函数返回,才能回到合适的错误处理流程中,而且大量额外的判断也会影响程序的性能。对此c++编译器和操作系统各自提供了自己的解决方案。

c++编译器提出的方案是提供了c++异常,它的本质上还是上面提到的条件判断然后在代码里边手动抛出异常,但是它的好处是异常可以自动跨函数回到用户设定的,合适的异常处理代码;但是如果出现了判断之外的异常比如说除零或者是访问非法地址之类的CPU异常只能引起程序崩溃。

操作系统的异常处理方案(Windows/MACOS/Linux这样的现代操作系统都有)能够处理CPU异常,但是它的问题是异常发生之后会跳转到注册的异常处理函数,这个函数是独立于产生异常代码的,无法精确定位异常到底出现在什么函数的什么位置,也无法处理资源回收清理事宜。

对此,VC编译器提出了结构化异常的解决方案。通过结构化异常,用户代码不需要增加条件判断就能处理包括CPU定义的异常之类的各种异常情况。还可以象C++异常那样,跳转到合适的堆栈环境去处理回收清理事务。可以说是完美的平衡了代码的简洁和安全性。

但是,事情总是有但是,不论是Linux/MACOS还是Mingw环境的编译器都不支持结构化异常。程序员必须自己维护大量的环境来妥善处理异常事务。而对于Visual LVM和Blue Print这种后台需要跨平台运行的软件更是一场灾难。明明在Windows平台上使用VC编译器有很简单的方案可以解决问题,但是,其他平台的部分却需要增加大量的代码去处理,实在是大大增加了开发的成本。

出于降低开发成本,优化代码的目的。本文就来讨论一下如何在使用非VC编译器的情况下模拟实现VC编译器自带的结构化异常处理方案。这样可以将各个平台上的异常处理代码统一起来,实现不同平台上的源代码级兼容。

二. 准备工作

我们首先考虑的是Linux平台。在Linux平台上可以注册异常处理函数,去截获各种异常,那么要解决的就是当异常发生的时候,如何跳转到用户定义的异常处理函数中,甚至最好的情况就是在出现异常的函数体内去做处理。

这种时候就需要用到所有操作系统都提供的两个系统函数setjmp和longjmp。

cpp 复制代码
int setjmp(jmp_buf env);           // 保存当前执行环境

void longjmp(jmp_buf env,int val); // 跳转到 setjmp 保存的位置

. jmp_buf: 一个特殊的数据结构,用于保存程序当前的执行环境(如寄存器、栈指针等)。

. setjmp: 保存调用函数时的代码位置及堆栈环境,首次调用时返回0。

当通过longjmp跳转到env中保存的代码位置时,返回longjmp提供的val。

. longjmp: 跳转到从setjmp(env)返回时的位置,并且返回值是val。

查看操作系统提供的系统函数说明,我们可以看到setjmp函数会保存当前运行环境(指令位置,寄存器信息和堆栈环境信息),而longjmp函数可以跨函数跳转到setjmp保存的代码位置,并且恢复那一时刻的运行环境

下面我们写一个简单的使用setjmp/longjmp函数的例子。

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>

jmp_buf jump_buffer;

void foo()
{
    printf("准备触发跳转...\n");
    longjmp(jump_buffer, 42);  // 跳转到 setjmp,并返回 42
}

int main()
{
    int ret=setjmp(jump_buffer);  // 首次调用返回 0,在调用longjmp跳转后返回 42
    if(!ret)
    {
        printf("首次调用 setjmp\n");
        foo();
    }
    else
    {
        printf("从 longjmp 返回,返回码: %d\n", ret);
    }

    return 0;
}

运行结果如下:

首次调用 setjmp

准备触发跳转...

从 longjmp 返回,返回码: 42

从运行结果我们可以看到,代码的运行顺序是

ret=setjmp(jump_buffer);

...

foo()

ret=setjmp(jump_buffer); //注意这里其实是从foo()里的longjmp调过来的。

printf("从 longjmp 返回,返回码: %d\n", ret);

现在,基于异常处理函数和上述的两个API来模拟结构化异常处理的逻辑就很清晰了。

在开始模拟之前,先对各个区域作一个简要定义

cpp 复制代码
__try                        ----------------------------+
{                            ------------+               |
                                         |               |
    //User code                          +异常监测块      |
                                         |               |
}                            ------------+               |
                                                         |
__except(ExceptionFilter)    ------------+异常过滤函数    |异常处理块
                                                         |
{                            ------------+               |
                                         |               |
    //Do clean work                      +清理回收块      |
                                         |               |
}                            ------------+               |
                             ----------------------------+

三.第一次模拟

在进入异常监测块的时候,执行setjmp保存当前运行环境和位置,再注册异常处理信息,然后运行代码。如果代码运行正常,在异常监测块结束的时候,注销记录的异常处理信息;如果出现异常,在异常处理函数中间使用longjmp跳转到我们记录的运行位置,即回到出现异常的函数(也可能是出现异常的函数的某个适合做清理工作的父函数)在处理完清理回收信息之后,清除之前记录的运行信息,并继续运行。

下面我们可以写出以下的代码:

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>

jmp_buf SectionEntry; //用于保存异常监视区入口以及运行环境

struct sigaction SignalHandler; //用于保存当前异常处理记录

struct sigaction OldHandler; //用于保存旧异常处理记录

static void _ExceptionHandler(int iSignal,siginfo_t *pSignalInfo,void *pContext)
//异常处理函数,在本例中是直接跳转到异常监视区开始位置,并利用longjmp函数跳转到异常环境回收例程
{
    printf("    Got SIGSEGV at address: %lXH, %p\n",(long) pSignalInfo->si_addr,pContext);
    siglongjmp(SectionEntry,1); //跳转到记录的异常监视块开始处
}

int StartSEHService(void)
//异常处理初始化函数,应该程序启动之后运行
{
    SignalHandler.sa_sigaction=_ExceptionHandler;//指向我们定义的异常处理函数
    if(-1==sigaction(SIGSEGV,&SignalHandler,&OldHandler)) //注册自己的异常处理函数并保存原来的异常处理函数
    {
        perror("Register sigaction fail");
        return 0;
    }
    else
    {
        return 1;
    }
}

int main(void)
{
    char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";//用于展示异常清理块跟异常监测块拥有相同环境的字符串

    StartSEHService();//初始化异常处理服务,在程序开始时调用一次

    printf("--------------Virtual SEH Test Start--------------\n");
    {
        printf("  +Enter critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区
        
        if(!sigsetjmp(SectionEntry,1)) //保存当前运行位置以及环境信息
        { //setjmp返回0表示刚刚完成了环境保存工作,下面运行可能产生异常的代码
            //------------------------------- Exception monitor block start -------------------------------

            printf("Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);

            printf("  Let's do some bad thing\n");

            *((char*)0)=0;//产生异常

            //------------------------------- Exception monitor block end -------------------------------

            printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
        }
        else
        { //非0表示是从longjmp函数中跳转过来(在注册的系统异常处理程序中我们设置longjmp的参数是1),在本例中表明发生了异常并无法恢复运行,执行环境清理工作
            //------------------------------- Exception clean block start -------------------------------

            printf("  Exception occur! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);

            //------------------------------- Exception clean block end -------------------------------
        }
    }

    sigaction(SIGSEGV,&OldHandler,&OldHandler); //程序结束,恢复原有异常处理函数
}

运行结果如下:

--------------Virtual SEH Test Start--------------

+Enter critical section @ test1.cpp 37

Hello, we go into exception monitor section

This is the local string :'Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.'

Let's do some bad thing

Got SIGSEGV at address: 0H, 0x7fff3b6627c0

Exception occur! do clean work

and we can access local data in the same environment of exception critical section:

'Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.'

恭喜!你进行了第一次在产生异常的函数内处理异常后环境清理的工作。

我们理一次上述代码的基本逻辑:

  1. 注册系统异常处理函数
  2. 调用sigsetjmp保存代码运行位置和运行环境
  3. 触发异常
  4. 系统调用注册的异常处理函数
  5. 异常处理函数调用siglongjmp回到保存的代码位置
  6. main函数中的异常处理例程显示与触发异常的代码运行在相同环境

四.渐入佳境

既然基本逻辑走通了,那么我们就可以准备模拟SEH的工作了。

众所周知,SEH的基本代码结构是

cpp 复制代码
__try
{
    //Exception critical section
}
__except(ExceptionFilter)
{
    //Clean work section
}

异常监测块

注册异常拦过滤函数(独立函数)

环境清理块

再看看我们上面写的代码

大致结构是

异常处理机制初始化(程序启动时运行一次)

保存异常监测块/清理块入口位置和环境

进入异常监测块

产生异常

异常清理块

既然我们是要模拟SEH以实现源代码级跨平台兼容,而SEH是微软编译器内嵌支持的,因此它最大,只能是别的平台迁就它。

所以第一步我们需要增加独立的异常过滤函数

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>

//SEH中异常过滤函数有3个返回值,这里我们先把它们的定义放上来

#define EXCEPTION_EXECUTE_HANDLER       1 //异常无法修复,运行环境清理回收例程
#define EXCEPTION_CONTINUE_SEARCH       0 //异常无法修复,尝试搜索上一级异常处理块
#define EXCEPTION_CONTINUE_EXECUTION    -1 //异常修复,恢复运行产生异常的代码

//既然跟异常相关的数据多起来了,我们定义一个结构用于存储异常处理块相关的信息

typedef struct _EXCEPTION_NODE
{
    jmp_buf SectionEntry;//保存异常监视区入口以及运行环境
    int     (*FilterRoutine)(int iSignal,siginfo_t *pSignalInfo,void *pContext);//异常过滤函数指针
}EXCEPTION_NODE,*PEXCEPTION_NODE;

EXCEPTION_NODE ExceptionNode;//异常监视区信息块

struct sigaction SignalHandler; //当前异常处理块

struct sigaction OldHandler; //旧异常处理块

static void _ExceptionHandler(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{ //异常处理函数,在本例中是直接跳转到异常监视区开始位置,并利用setjmp/longjmp函数的特性跳转到异常环境回收例程
    printf("    Got SIGSEGV at address: %lXH, %p\n",(long) pSignalInfo->si_addr,pContext);
    ExceptionNode.FilterRoutine(iSignal,pSignalInfo,pContext);
    siglongjmp(ExceptionNode.SectionEntry,1); //跳转到记录的异常监视块开始处
}

int StartSEHService(void)
{ //异常处理初始化函数,应该程序启动之后运行
    SignalHandler.sa_sigaction=_ExceptionHandler;

    if(-1==sigaction(SIGSEGV,&SignalHandler,&OldHandler))
    { //注册自己的异常处理函数
        perror("Register sigaction fail");
        return 0;
    }
    else
    {
        return 1;
    }
}

//异常过滤函数
int ExceptionFilter(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
    printf("Exception occur! We cannot fix it now.\n");
    return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

int main(void)
{
    int  iResult;

    char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";//用于展示异常清理块跟异常颢块拥有相同环境的客串

    StartSEHService();//初始化异常处理服务,在程序开始时调用一次

    printf("--------------Virtual SEH Test Start--------------\n");
    {
        printf("  +Enter critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区

        iResult=sigsetjmp(ExceptionNode.SectionEntry,1); //保存当前运行位置以及环境信息
        if(2==iResult)
        { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
            //------------------------------- Exception monitor block start -------------------------------
            printf("Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
            printf("  Let's do some bad thing\n");

            *((char*)0)=0;

            //------------------------------- Exception monitor block end -------------------------------
            printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
        }
        else if(!iResult)
        { //setjmp返回0表示刚刚完成了环境保存工作,此时我们需要注册异常过滤例程
            printf("  *Register exception filter @ %s %d\n",__FILE__,__LINE__);
            ExceptionNode.FilterRoutine=ExceptionFilter;//注册异常过滤函数
            siglongjmp(ExceptionNode.SectionEntry,2); //跳转到异常监视块开始处    
        }
        else
        { //非0/2表示是从系统异常处理程序中的longjmp函数跳转过来,在本例中表明发生了异常并无法恢复运行,处理善后工作
            //------------------------------- Exception clean block start -------------------------------

            printf("  Exception occur! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);

            //------------------------------- Exception clean block end -------------------------------
        }
    }
    sigaction(SIGSEGV,&OldHandler,&OldHandler); //恢复原有异常处理函数
}

上述代码的基本结构是

信号(异常)处理函数

异常过滤函数

异常监测块

注册异常拦过滤函数(独立函数)

异常清理块

这里我们可以看到,已经跟SEH一样了。

既然基本结构已经相同了,那么我们把异常维护相关的代码放进宏定义里,现在代码变成这个样子了:

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>

//SEH中异常过滤函数有3个返回值,这里我们先把它们的定义放上来

#define EXCEPTION_EXECUTE_HANDLER       1

#define EXCEPTION_CONTINUE_SEARCH       0

#define EXCEPTION_CONTINUE_EXECUTION    -1

//既然跟异常相关的数据多起来了,我们定义一个结构用于存储异常块相关的信息
typedef struct _EXCEPTION_NODE
{
    jmp_buf SectionEntry;//保存异常监视区入口以及运行环境

    int     (*FilterRoutine)(int iSignal,siginfo_t *pSignalInfo,void *pContext);//异常过滤函数指针
}EXCEPTION_NODE,*PEXCEPTION_NODE;

EXCEPTION_NODE ExceptionNode;//异常监视区信息块

struct sigaction SignalHandler; //当前异常处理块

struct sigaction OldHandler; //旧异常处理块

static void _ExceptionHandler(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{ //异常处理函数,在本例中是直接跳转到异常监视区开始位置,并利用setjmp/longjmp函数的特性跳转到异常环境回收例程
    printf("    Got SIGSEGV at address: %lXH, %p\n",(long) pSignalInfo->si_addr,pContext);
    ExceptionNode.FilterRoutine(iSignal,pSignalInfo,pContext);
    siglongjmp(ExceptionNode.SectionEntry,1); //跳转到记录的异常监视块开始处
}

int StartSEHService(void)
{ //异常处理初始化函数,应该程序启动之后运行
    SignalHandler.sa_sigaction=_ExceptionHandler;

    if(-1==sigaction(SIGSEGV,&SignalHandler,&OldHandler))
    { //注册自己的异常处理函数
        perror("Register sigaction fail");

        return 0;
    }
    else
    {
        return 1;
    }
}

//异常过滤函数
int ExceptionFilter(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
    printf("Exception occur! We cannot fix it now.\n");
    return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

#define TRY_START \
    int iResult; \
    iResult=sigsetjmp(ExceptionNode.SectionEntry,1); /*保存当前运行位置以及环境信息*/ \
    if(2==iResult) \
    /*setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码*/

#define TRY_EXCEPT(filter) \
    else if(!iResult) \
    { /*setjmp返回0表示刚刚完成了环境保存工作,此时我们需要注册异常过滤例程*/ \
        printf("  *Register exception filter @ %s %d\n",__FILE__,__LINE__); \
        ExceptionNode.FilterRoutine=filter;/*注册异常过滤函数*/ \
        siglongjmp(ExceptionNode.SectionEntry,2); /*跳转到异常监视块开始处*/ \
    } \
    else \
    /*非0/2表示是从系统异常处理程序中的longjmp函数跳转过来,在本例中表明发生了异常并无法恢复运行,处理善后工作*/

int main(void)
{
    char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";//用于展示异常清理块跟异常颢块拥有相同环境的客串

    StartSEHService();

    printf("--------------Virtual SEH Test Start--------------\n");

    TRY_START
    {
        printf("  +Enter critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区

        //------------------------------- Exception monitor block start -------------------------------

        printf("Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
        printf("  Let's do some bad thing\n");

        *((char*)0)=0;

        //------------------------------- Exception monitor block end -------------------------------
        printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
    }
    TRY_EXCEPT(ExceptionFilter)
    {
        //------------------------------- Exception clean block start -------------------------------
        printf("  Exception occur! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
        //------------------------------- Exception clean block end -------------------------------
    }

    sigaction(SIGSEGV,&OldHandler,&OldHandler); //恢复原有异常处理函数
}

现在除开初始化异常服务和相关的函数,程序主体结构看上去跟SEH几乎一模一样了。

但是,事情并没有这么简单。

首先,在前面我们提到SEH支持在异常过滤例程中有3种返回信息。我们需要在系统异常处理程序中增加对异常过滤函数返回不同值之后的相应处理。其次,SEH是支持嵌套的,所以还需要支持向后搜索合适的异常处理块,因此我们需要为注册的SEH信息建立一个链表。同时,在退出异常处理块(修复失败并清理或正常结束)之后,还要注销当前异常处理块的信息。

所以我们还需要增加很多内容:

修改后的代码如下

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>

//3种SEH中异常过滤函数返回值

#define EXCEPTION_EXECUTE_HANDLER       1

#define EXCEPTION_CONTINUE_SEARCH       0

#define EXCEPTION_CONTINUE_EXECUTION    -1

//既然跟异常相关的数据多起来了,我们定义一个结构用于存储异常块相关的信息
typedef struct _EXCEPTION_NODE
{
    struct _EXCEPTION_NODE *Prev;//异常处理块链
    int                    RunStatus;//前述例子里的运行状态(0:完成运行环境记录,1:异常无法修复,转到清理块运行,2:异常信息块注册成功,跳转到异常监测区运行
    jmp_buf                SectionEntry;//保存异常监视区入口以及运行环境
    int                    (*FilterRoutine)(int iSignal,siginfo_t *pSignalInfo,void *pContext);//异常过滤函数指针
}EXCEPTION_NODE,*PEXCEPTION_NODE;

static struct _SEH_SET
{
    PEXCEPTION_NODE  Chain;
    struct sigaction SignalHandler,OldHandler;
    int              Initialized;
}s_Maintain={0};//SEH管理块

static void _ExceptionHandler(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{ //异常处理函数,在本例中是直接跳转到异常监视区开始位置,并利用setjmp/longjmp函数的特性跳转到异常环境回收例程
    int             iResult;
    PEXCEPTION_NODE pEntry,pPrev;

    printf("    Got SIGSEGV at address: %lXH, %p\n",(long) pSignalInfo->si_addr,pContext);
    pEntry=s_Maintain.Chain;//准备遍历注册的异常处理块

    do
    {
        iResult=pEntry->FilterRoutine(iSignal,pSignalInfo,pContext);//调当前异常块的异常过滤函数
        switch(iResult)
        {
            case EXCEPTION_EXECUTE_HANDLER://当前异常无法解决,执行善后清理工作
                s_Maintain.Chain=pEntry->Prev;//注销当前异常处理块
                siglongjmp(pEntry->SectionEntry,1);//跳转到注册异常块的善后清理入口
                break;
            case EXCEPTION_CONTINUE_SEARCH://注册异常块无法处理当前异常,尝试上一级异常处理
                pPrev=pEntry->Prev;
                s_Maintain.Chain=pEntry->Prev;//既然需要到上一级异常块处理,那么当前异常块已经无用了,注销当前异常块
                pEntry=pPrev;//转到上一级异常处理块
                break;
            case EXCEPTION_CONTINUE_EXECUTION://异常修复完成,返回异常发生处继续运行
                return;
                break;
            default://异常过滤程序返回了错误的值
                printf("Bad exception filter result %d\n",iResult);
                break;
        }
    }while(pEntry);

    printf("    No more handler\n");
    sigaction(SIGSEGV,&s_Maintain.OldHandler,NULL);
}

int StartSEHService(void)
{ //异常处理初始化函数,应该程序启动之后运行
    s_Maintain.SignalHandler.sa_sigaction=_ExceptionHandler;
    if(-1==sigaction(SIGSEGV,&s_Maintain.SignalHandler,&s_Maintain.OldHandler))
    { //注册自己的异常处理函数
        perror("Register sigaction fail");

        return 0;
    }
    else
    {
        s_Maintain.Initialized=1;
        return 1;
    }
}

//异常过滤函数
int ExceptionFilter(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
    printf("Exception occur! We cannot fix it now.\n");
    return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

int main(void)
{
    char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";//用于展示异常清理块跟异常颢块拥有相同环境的客串

    printf("--------------Virtual SEH Test Start--------------\n");
    printf("  +Enter critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区

/* TRY_START */
    {
        EXCEPTION_NODE __weLees_ExceptionNode;
        __weLees_ExceptionNode.Prev=NULL;
        if(!s_Maintain.Initialized)
        {
            StartSEHService();
        }
        __weLees_ExceptionNode.Prev=s_Maintain.Chain;
        s_Maintain.Chain=&__weLees_ExceptionNode;
        __weLees_ExceptionNode.RunStatus=sigsetjmp(__weLees_ExceptionNode.SectionEntry,1); //保存当前运行位置以及环境信息
        if(2==__weLees_ExceptionNode.RunStatus)
/* TRY_START end */
        { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
            //------------------------------- Exception monitor block start -------------------------------
            printf("Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
            printf("  Let's do some bad thing\n");

            *((char*)0)=0;

            //------------------------------- Exception monitor block end -------------------------------
            printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
            s_Maintain.Chain=__weLees_ExceptionNode.Prev;//注销异常块
        }
/* TRY_EXCEPT(filter) */
        else if(!__weLees_ExceptionNode.RunStatus)
        { //setjmp返回0表示刚刚完成了环境保存工作,此时我们需要注册异常过滤例程
            printf("  *Register exception filter @ %s %d\n",__FILE__,__LINE__);
            __weLees_ExceptionNode.FilterRoutine=ExceptionFilter;//注册异常过滤函数
            siglongjmp(__weLees_ExceptionNode.SectionEntry,2); //跳转到异常监视块开始处
        }
        else//非0/2表示是从系统异常处理程序中的longjmp函数跳转过来,在本例中表明发生了异常并无法恢复运行,处理善后工作
/* TRY_EXCEPT(filter) end */
        {
            //------------------------------- Exception clean block start -------------------------------
            printf("  Exception occur! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
            //------------------------------- Exception clean block end -------------------------------
        }
/* TRY_END */
    }
/* TRY_END end */

    sigaction(SIGSEGV,&s_Maintain.OldHandler,&s_Maintain.OldHandler); //恢复原有异常处理函数
}

这一次的代码中我们为了更直观地展示代码结构,没有把异常块注册/注销部分提取到宏里,而是用注释标记出了它们的内容和在代码中的位置。

其中异常区要注意以下几点

  1. 首先,每个异常处理块需要定义一个EXCEPTION_NODE结构用于记录相关信息。既然这个结构只在异常处理块范围内使用,那么完全不需要额外为它分配空间,只需要把它作为一个局部变量放到函数栈里。
  2. 首先用花括号把整个异常处理块包起来是有必要的,为了注册异常处理块,需要函数非开头位置定义异常登记块变量,使用花括号包起来可以避免在在某些编译器上编译失败,以及确定异常块的作用域,即在异常嵌套时,内部异常处理块定义的异常登记块只属于内部异常处理块,不会影响外层代码。
  3. 为此增加了一个额外的TRY_END块以保证代码正确,它的内容就是一个右花括号。
  4. 异常注册块定义一个比较长的,用户不容易使用的变量名,避免跟用户定义的变量发生冲突。
  5. 上述代码中用于识别异常监视块初始化状态的iResult变量移动到异常注册块中,同样是为了减少变量名冲突。

除了对异常监视区大改动之外,对系统异常处理函数也做了大改。

  1. 为了支持异常处理链,增加了遍历注册的异常处理链操作
  2. 调用异常过滤例程之后,分别按不同的返回值做出处理。

异常过滤例程返回的合法返回值有3种

EXCEPTION_EXECUTE_HANDLER(1)

当前异常无法解决,需要跳转到当前异常处理块的异常清理块,执行清理回收工作。

对这种情况,异常处理程序要做的是:

注销当前异常块

跳转到注册异常处理块的异常清理块入口

EXCEPTION_CONTINUE_SEARCH(0)

注册异常块无法处理当前异常,尝试上一级异常处理

对这种情况,异常处理程序要做的是:

因为既然要跳转到上一级异常处理块,那么当前块所在的运行环境都会失效,所以注销当前异常块

把上一级异常处理块设置为当前异常处理块

循环,继续调用(上一层异常处理块的)异常过滤函数

EXCEPTION_CONTINUE_EXECUTION(-1)

异常修复完成,返回异常发生处继续运行,通常用于调试器

对这种情况,异常处理程序要做的是:

直接返回,让系统继续运行产生异常的代码

五.完全状态

经过上述改动,SEH的基本要点都已经实现了。

下面就让我们把相关的代码移动到宏里面,来进行实战环境中的异常处理测试。

以下是最终的模拟SEH服务和测试代码

cpp 复制代码
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>

//SEH中异常过滤函数有3个返回值,这里我们先把它们的定义放上来

#define EXCEPTION_EXECUTE_HANDLER       1

#define EXCEPTION_CONTINUE_SEARCH       0

#define EXCEPTION_CONTINUE_EXECUTION    -1

//既然跟异常相关的数据多起来了,我们定义一个结构用于存储异常块相关的信息
typedef struct _EXCEPTION_NODE
{
    struct _EXCEPTION_NODE *Prev;
    int                    RunStatus;
    jmp_buf                SectionEntry;//保存异常监视区入口以及运行环境
    int                    (*FilterRoutine)(int iSignal,siginfo_t *pSignalInfo,void *pContext);//异常过滤函数指针
}EXCEPTION_NODE,*PEXCEPTION_NODE;

static __thread struct _SEH_SET
{
    PEXCEPTION_NODE  Chain;
    struct sigaction SignalHandler,OldHandler;
    int              Initialized;
}s_Maintain={0};//SEH管理块

static void _ExceptionHandler(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{ //异常处理函数,在本例中是直接跳转到异常监视区开始位置,并利用setjmp/longjmp函数的特性跳转到异常环境回收例程
    int             iResult;
    PEXCEPTION_NODE pEntry,pPrev;
    
    printf("    Got SIGSEGV at address: %lXH, %p\n",(long) pSignalInfo->si_addr,pContext);
   
    pEntry=s_Maintain.Chain;//准备遍历注册的异常处理块
    do
    {
        iResult=pEntry->FilterRoutine(iSignal,pSignalInfo,pContext);//调当前异常块的异常过滤函数
        
        switch(iResult)
        {
            case EXCEPTION_EXECUTE_HANDLER://当前异常无法解决,执行善后清理工作
                s_Maintain.Chain=pEntry->Prev;//注销当前异常块
                siglongjmp(pEntry->SectionEntry,1);//跳转到注册异常块的善后清理入口
                break;
            case EXCEPTION_CONTINUE_SEARCH://注册异常块无法处理当前异常,尝试上一级异常处理
                pPrev=pEntry->Prev;
                s_Maintain.Chain=pEntry->Prev;//既然需要到上一级异常块处理,那么当前异常块已经无用了,注销当前异常块
                pEntry=pPrev;//转到上一级异常处理块
                break;
            case EXCEPTION_CONTINUE_EXECUTION://异常修复完成,返回异常发生处继续运行
                return;
                break;
            default://异常过滤程序返回了错误的值
                printf("Bad exception filter result %d\n",iResult);
                break;
        }
    }while(pEntry);

    printf("    No more handler\n");
    sigaction(SIGSEGV,&s_Maintain.OldHandler,NULL);
}

int StartSEHService(void)
{ //异常处理初始化函数,应该程序启动之后运行

    s_Maintain.SignalHandler.sa_sigaction=_ExceptionHandler;

    if(-1==sigaction(SIGSEGV,&s_Maintain.SignalHandler,&s_Maintain.OldHandler))
    { //注册自己的异常处理函数
         perror("Register sigaction fail");

         return 0;
    }
    else
    {
         s_Maintain.Initialized=1;
         return 1;
    }
}

#define TRY_START \
{ \
    EXCEPTION_NODE __weLees_ExceptionNode; \
    __weLees_ExceptionNode.Prev=NULL; \
    if(!s_Maintain.Initialized) \
    { \
        StartSEHService(); \
    } \
    __weLees_ExceptionNode.Prev=s_Maintain.Chain; \
    s_Maintain.Chain=&__weLees_ExceptionNode; \
    __weLees_ExceptionNode.RunStatus=sigsetjmp(__weLees_ExceptionNode.SectionEntry,1); /*保存当前运行位置以及环境信息*/ \
   if(2==__weLees_ExceptionNode.RunStatus) \
   {

#define TRY_EXCEPT(filter) \
       s_Maintain.Chain=__weLees_ExceptionNode.Prev; \
   } \
   else if(!__weLees_ExceptionNode.RunStatus) \
   {/*setjmp返回0表示刚刚完成了环境保存工作,此时我们需要注册异常过滤例程*/ \
       printf("  *Register exception filter @ %s %d\n",__FILE__,__LINE__); \
       __weLees_ExceptionNode.FilterRoutine=filter;/*注册异常过滤函数*/ \
       siglongjmp(__weLees_ExceptionNode.SectionEntry,2);/*跳转到异常监视块开始处*/ \
   } \
   else/*非0/2表示是从系统异常处理程序中的longjmp函数跳转过来,在本例中表明发生了异常并无法恢复运行,处理善后工作*/

#define TRY_END }

//异常过滤函数
int ExceptionFilter1(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
   printf("Test1 : Exception occur! We cannot fix it now.\n");
   return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

void Test1(void)
{
   char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";
   printf("--------------Virtual SEH Test1 : simple exception handling--------------\n");
   printf("  +Enter critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区

   TRY_START
   { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
       //------------------------------- Exception monitor block start -------------------------------
       printf("Test1 : Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
       printf("Test1 :   Let's do some bad thing\n");

       *((char*)0)=0;

       //------------------------------- Exception monitor block end -------------------------------
       printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
   }
   TRY_EXCEPT(ExceptionFilter1)
   {
       //------------------------------- Exception clean block start -------------------------------
       printf("Test1 :   Exception occur! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
       //------------------------------- Exception clean block end -------------------------------
   }
   TRY_END
}

//异常过滤函数
int ExceptionFilter2(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
   printf("Test2 : Exception occur! We cannot fix it now.\n");
   return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

void Test2(void)
{
   char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";
   printf("\n--------------Virtual SEH Test2 : nested exception handling--------------\n");

   TRY_START
   { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
       //------------------------------- Exception monitor block start -------------------------------
       printf("Test2 : +Enter outer critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区
       TRY_START
       { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
          //------------------------------- Exception monitor block start -------------------------------
          printf("Test2 : +Enter inner critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区
          printf("Test2 : Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
          printf("Test2 :   Let's do some bad thing\n");

          *((char*)0)=0;

          //------------------------------- Exception monitor block end -------------------------------
          printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
       }
       TRY_EXCEPT(ExceptionFilter2)
       {
          //------------------------------- Exception clean block start -------------------------------
          printf("Test2 :   Exception occur in INNER level! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
          //------------------------------- Exception clean block end -------------------------------
       }
       TRY_END
   }
   TRY_EXCEPT(ExceptionFilter2)
   {
       //------------------------------- Exception clean block start -------------------------------
       printf("Test2 :   Exception occur in outer level! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
       //------------------------------- Exception clean block end -------------------------------
   }
   TRY_END
}

//异常过滤函数
int ExceptionFilter3(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
   printf("Test3 : Exception occur! We cannot fix it now.\n");
   return EXCEPTION_EXECUTE_HANDLER;//这里我们先强制返回这个值
}

//异常过滤函数
int ExceptionFilter3_1(int iSignal,siginfo_t *pSignalInfo,void *pContext)
{
   printf("Test3 : Exception occur! We cannot fix it now, try upper level fixing.\n");
   return EXCEPTION_CONTINUE_SEARCH;//这里我们先强制返回这个值
}

void Test3(void)
{
   char sz[]="Exception critical section & exception clean up section was run in THE SAME FUNCTION, I show the same information as proof.";
   printf("\n--------------Virtual SEH Test3 : nested exception handling-try outer exception handler --------------\n");

   TRY_START
   { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码
       //------------------------------- Exception monitor block start -------------------------------
       printf("Test3 : +Enter outer critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区
       TRY_START
       { //setjmp返回2表示已经完成了异常过滤例程注册工作,下面运行可能产生异常的高危代码

          //------------------------------- Exception monitor block start -------------------------------
          printf("Test3 : +Enter inner critical section @ %s %d\n",__FILE__,__LINE__); //进入异常监视区
          printf("Test3 : Hello, we go into exception monitor section\n  This is the local string :'%s'\n",sz);
          printf("Test3 :   Let's do some bad thing\n");

          *((char*)0)=0;

          //------------------------------- Exception monitor block end -------------------------------
          printf("  -Leave critical section @ %s %d\n",__FILE__,__LINE__); //这一行其实不会运行
       }
       TRY_EXCEPT(ExceptionFilter3_1)
       {
          //------------------------------- Exception clean block start -------------------------------
          printf("Test3 :   Exception occur in INNER level! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
          //------------------------------- Exception clean block end -------------------------------
       }
       TRY_END
   }
   TRY_EXCEPT(ExceptionFilter3)
   {
       //------------------------------- Exception clean block start -------------------------------
       printf("Test3 :   Exception occur in outer level! do clean work\n  and we can access local data in the same environment of exception critical section:\n'%s'\n",sz);
       //------------------------------- Exception clean block end -------------------------------
   }
   TRY_END
}

int main(void)
{
    Test1();
    Test2();
    Test3();

    sigaction(SIGSEGV,&s_Maintain.OldHandler,&s_Maintain.OldHandler); //恢复原有异常处理函数
}

代码中一共有三个测试例子。

第一个是简单的异常测试。

第二个是嵌套异常,内层代码触发异常并处理。

第三个是嵌套异常,内层代码触发异常后,异常过滤例程无法处理异常,请求使用外层异常处理。当然这里没有测试异常修复并重新运行的情况,在Linux/OSX取得异常详细信息比较复杂,本文就不就此展开了。

以上代码在Ubuntu和OSX/x86_64上测试运行通过。理论上该代码能兼容FreeBSD。只需要在FreeBSD上重新编译即可。

至此,我们在非Windows平台上模拟SEH实现基本完成了。对于Windows平台和VC编译器,只需要定义以下宏:

cpp 复制代码
#define TRY_START __try

#define TRY_EXCEPT(filter) __except(filter(GetExceptionInformation()))

#define TRY_END

即可实现在所有平台中以统一格式

cpp 复制代码
TRY_START
{

   //work code

}
TRY_EXCEPT(filter)
{

   //clean up code

}
TRY_END

实现跟SEH相同的异常处理流程。

当然,各个平台上异常过滤函数的声明和参数各不相同,需要分别处理。

六.一点补充

即可实现Windows/Linux/OSX等平台代码的源代码级兼容。接下来要注意的是三点:

  1. SEH服务是线程独立的,因此在定义异常处理头结构s_Maintain时需要加上__thread/thread_local(C++11)关键字,以保证这个结构是线程独立的。

  2. 上述代码中没有考虑到从异常监视区中退出循环,即break指令和goto指令。

  3. 上述代码中没有考虑到从异常监视区中退出函数,即return指令。

因为VC在实现SEH时有编译器支持,它会在任意退出指令前加上注销异常处理块的代码,而我们没有编译器支持,只能从代码上想办法,因此对于goto指令,在反复考虑过各种方法之后,只能放弃支持,毕竟c/c++规则中也建议不使用该指令。

对于break指令,执行前需要注销循环中所有要退出的try/except异常处理块,因此需要提前知晓循环中到底有几级异常监测块。不过考虑到同一函数中的单个循环中,通常不会嵌套过多的异常监测块,因此我们提供了以下几个宏,基本能应对绝大部分情况。

cpp 复制代码
#define TRY_BREAK s_Maintain.Chain=s_Maintain.Chain->Prev;break //循环中只有一层异常监视块

#define TRY_BREAK2 \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    break /*循环中有二层异常监视块*/

#define TRY_BREAK3 \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    break /*循环中有三层异常监视块*/

用户视循环中有几层异常块来决定使用哪一个宏来替代break指令。

要实现从异常块中直接退出函数(return指令),同样需要在真正退出前注销函数中所有注册的异常监视块。因此我们也可以象实现TRY_BREAK那样做,定义几个宏:

cpp 复制代码
#define TRY_RETURN s_Maintain.Chain=s_Maintain.Chain->Prev; \
    return //代码在一层异常监视块中

#define TRY_RETURN2 \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    return /*代码位于两层异常监视块中*/

#define TRY_RETURN3 \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    s_Maintain.Chain=s_Maintain.Chain->Prev; \
    return /*代码位于三层异常监视块中*/

看上去十分不优雅,而且一旦用户忘记使用宏替代break/return指令就容易出错。

但是!那是针对C语言,如果是C++语言,那就好办了。在C++中,结构/类在离开作用域的时候,会调用它的析构函数,所以我们可以在析构函数里注销异常块!而break和return指令肯定会退出异常块的作用域,这下编译器就会帮我们解决这个问题了。

首先,我们修改为每一个注销异常处理动作增加一行:

cpp 复制代码
(Node)->Prev=NULL;

然后,为EXCEPTION_NODE增加析构函数:

cpp 复制代码
    ~_EXCEPTION_NODE(void)
    {
        if(Prev)
        {
            printf("~~~~ ExceptionNode %p, Status %d\n",this,RunStatus);
            s_Maintain.Chain=s_Maintain.Chain->Prev;
            Prev=NULL;
        }
    }

行了,现在代码就非常优雅了。我们完成了非Windows平台上的SEH功能的模拟。

当然还有unwind问题,这个在VC编译器上会报错,而非Windows平台上整个都是我们模拟的,这个问题只能由用户自己解决了。

相关推荐
2301_14725836926 分钟前
7月2日作业
java·linux·服务器
xuanzdhc5 小时前
Linux 基础IO
linux·运维·服务器
愚润求学5 小时前
【Linux】网络基础
linux·运维·网络
bantinghy5 小时前
Linux进程单例模式运行
linux·服务器·单例模式
小和尚同志6 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
帽儿山的枪手6 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
shadon1789 天前
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
linux
小米里的大麦9 天前
014 Linux 2.6内核进程调度队列(了解)
linux·运维·驱动开发
算法练习生9 天前
Linux文件元信息完全指南:权限、链接与时间属性
linux·运维·服务器
忘了ʷºᵇₐ9 天前
Linux系统能ping通ip但无法ping通域名的解决方法
linux·服务器·tcp/ip