windows 异常处理
为了程序的健壮性,windows 中提供了异常处理机制,称为结构化异常,异常一般分为硬件异常和软件异常,硬件异常一般是指在执行机器指令时发生的异常,比如试图向一个拥有只读保护的页面写入内容,或者是硬件的除0错误等等,而软件异常则是由程序员,调用RaiseException显示的抛出的异常。对于一场处理windows封装了一整套的API,平台上提供的异常处理机制被叫做结构化异常处理(SEH)。不同于C++的异常处理,SEH拥有更为强大的功能,并且采用C风给的代码编写方式。
异常处理机制的流程简介
一般当程序发生异常时,用户代码停止执行,并将CPU的控制权转交给操作系统,操作系统接到控制权后,将当前线程的环境保存到结构体CONTEXT中,然后查找针对此异常的处理函数。系统利用结构EXCEPTION_RECORD保存了异常描述信息,它与CONTEXT一同构成了结构体EXCEPTION_POINTERS,一般在异常处理中经常使用这个结构体。
异常信息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, *PEXCEPTION_RECORD;
当系统在用户程序中查找异常处理代码时主要通过查找当前的这个链表。
下面详细说明异常发生时操作系统是如何处理的:
1. 如果程序是被调试运行的(比如我们在VS编译器中调试运行程序),当异常发生时,系统首先将异常信息交给调试程序,如果调试程序处理了那么程序继续运行,否则系统便在发生异常的线程栈中查找可能的处理代码。若找到则处理异常,并继续运行程序
2. 如果在线程栈中没有找到,则再次通知调试程序,如果这个时候仍然不能处理这个异常,那么操作系统会对异常进程默认处理,比如强制终止程序。
SEH的基本框架
结构化异常处理一般有下面3个部分组成:
1. 保护代码体
2. 过滤表达式
3. 异常处理块
其中保护代码体:是指有可能发生异常的代码,一般在SEH中是用__try{}包含的那部分
过滤表达式:是在__except表达式的括号中的部分,一般可以是函数或者表达式,过滤表达式一般只能返回3个值:EXCEPTION_CONTINUE_SEARCH表示继续向下寻找异常处理的程序,也就是说本__exception不能处理这个异常;EXCEPTION_CONTINUE_EXECUTION表示异常已被处理,继续执行当初发生异常的代码;EXCEPTION_EXECUTE_HANDLER:表示异常已被处理,直接跳转到__exception(){}代码块中执行,这个时候就有点像C++中的异常处理了。一般一个__try块可以跟随多个__except块
异常处理块:是指__except大括号中的代码块
另外可以在过滤表达式中调用GetExceptionCode和GetExceptionInformagtion函数取得正在处理的异常信息,这两个函数不能再过滤表达式中使用,但是可以作为过滤表达式中的函数参数。
下面是一个异常处理的简单的例子:
#define PAGELIMIT 1024
DWORD dwPageCnt = 0;
LPVOID lpPrePage = NULL;
DWORD dwPageSize = 0;
INT FilterFunction(DWORD dwExceptCode)
{
if(EXCEPTION_ACCESS_VIOLATION != dwExceptCode)
{
return EXCEPTION_EXECUTE_HANDLER;
}
if(dwPageCnt >= PAGELIMIT)
{
return EXCEPTION_EXECUTE_HANDLER;
}
if(NULL == VirtualAlloc(lpPrePage, dwPageSize, MEM_COMMIT, PAGE_READWRITE))
{
return EXCEPTION_EXECUTE_HANDLER;
}
lpPrePage = (char*)lpPrePage + dwPageSize;
dwPageCnt++;
return EXCEPTION_CONTINUE_EXECUTION;
}
int _tmain(int argc, TCHAR *argv[])
{
SYSTEM_INFO si = {0};
GetSystemInfo(&si);
dwPageSize = si.dwPageSize;
char* lpBuffer = (char*)VirtualAlloc(NULL, dwPageSize * PAGELIMIT, MEM_RESERVE, PAGE_READWRITE);
lpPrePage = lpBuffer;
for(int i = 0; i < PAGELIMIT * dwPageSize; i++)
{
__try
{
lpBuffer[i] = 'a';
}
__except(FilterFunction(GetExceptionCode()))
{
ExitProcess(0);
}
}
VirtualFree(lpBuffer, dwPageSize * PAGELIMIT, MEM_FREE);
return 0;
}
这段代码我们通过结构化异常处理实现了内存的按需分配,首先程序保留了4M的地址空间,但是并没有映射到具体的物理内存,接着向这4M的空间中写入内容,这个时候会造成非法的内存访问异常,系统会执行过滤表达式中调用的函数,在函数中校验异常的异常码,如果不等于EXCEPTION_ACCESS_VIOLATION,也就是说这个异常并不是读写非法内存造成的,那么直接返回EXCEPTION_EXECUTE_HANDLER,这个时候会执行__exception块中的代码,也就是结束程序,如果是由于访问非法内存造成的,并且读写的范围没有超过4M那么就提交一个物理页面供程序使用,并且返回EXCEPTION_CONTINUE_EXECUTION,让程序接着从刚才的位置执行也就是说再次执行写入操作,这样保证了程序需要多少就提交多少,节约了物理内存。
终止处理块
终止处理块是结构化异常处理特有的模块,它保证了当__try块执行完成后总会执行终止处理块中的代码。一般位于__finally块中。只有当线程在__try中结束,也就是在__try块中调用ExitProcess或者ExitThread。由于系统为了保证__try块结束后总会调用__finally所以某些跳转语句如:goto return break等等就会添加额外的机器码以便能够跳入到__try块中,所以为了效率可以用__leave语句代替这些跳转语句。另外需要注意的一点是一个__try只能跟一个__finally块但是可以跟多个__except块。同时__try块后面要么跟__except要么跟__finally这两个二选一,不能同时跟他们两个。
抛出异常
在SEH中抛出异常需要使用函数:RaiseException,它的原型如下:
void WINAPI RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, const ULONG_PTR* lpArguments);
第一个是异常代码,第二个参数是异常标志,第三个是异常参数个数,第四个是参数列表,这个函数主要是为了填充EXCEPTION_RECORD结构体并将这个节点添加到链表中,当发生异常时系统会查找这个链表,下面是一个简单的例子:
DWORD FilterException()
{
wprintf(_T("1\n"));
return EXCEPTION_EXECUTE_HANDLER;
}
int _tmain(int argc, TCHAR *argv[])
{
__try
{
__try
{
RaiseException(1, 0, 0, NULL);
}
__finally
{
wprintf(_T("2\n"));
}
}
__except(FilterException())
{
wprintf(_T("3\n"));
}
_tsystem(_T("PAUSE"));
return 0;
}
上面的程序使用RaiseException抛出一个异常,按照异常处理的流程,程序首先会试着执行FilterException,以便处理这个异常,所以首先会输出1,然后根据返回值EXCEPTION_EXECUTE_HANDLER决定下一步会执行异常处理块__except中的内容,这个时候也就表示最里面的__try块执行完了,在前面说过,不管遇到什么情况,执行完__try块,都会接着执行它对应的__finally块,所以这个时候会首先执行__finally块,最后执行外层的__except块,最终程序输出结果为1 2 3
win32下的向量化异常处理
为什么向量化异常要强调是win32下的呢,因为64位windows不支持这个特性
理解这个特性还是回到之前说的操作系统处理异常的顺序上面,首先会交给调试程序,然后再由用户程序处理,根据过滤表达式返回的值决定这个异常是否被处理,而这个向量化异常处理,就是将异常处理的代码添加到这个之前,它的代码会先于过滤表达式之前执行。
我们知道异常是由内层向外层一层一层的查找,如果在内层已经处理完成,那么外层是永远没有机会处理的,这种情况在我们使用第三方库开发应用程序,而这个库又不提供源码,并且当发生异常时这个库只是简单的将线程终止,而我们想处理这个异常,但是由于内部处理了,外层的try根本捕获不到,这个时候就可以使用向量化异常处理了。这样我们可以编写异常处理代码先行处理并返回继续执行,这样库中就没有机会处理这个异常了。
使用这个机制通过AddVectoredExceptionHandler函数可以添加向量化异常处理过滤函数,而调用RemoveVectoredExceptionHandler可以移除一个已添加的向量化异常处理过滤函数。下面是一个简单的例子:
int g_nVal = 0;
void Func(int nVal)
{
__try
{
nVal /= g_nVal;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("正在执行Func中的__try __except块\n");
ExitProcess(0);
}
}
LONG CALLBACK VH1(PEXCEPTION_POINTERS pExceptionInfo)
{
printf("正在执行VH1()函数\n");
return EXCEPTION_CONTINUE_SEARCH;
}
LONG CALLBACK VH2(PEXCEPTION_POINTERS pExceptionInfo)
{
printf("正在执行VH2()函数\n");
if (EXCEPTION_INT_DIVIDE_BY_ZERO == pExceptionInfo->ExceptionRecord->ExceptionCode)
{
g_nVal = 25;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
LONG CALLBACK VH3(PEXCEPTION_POINTERS pExceptionInfo)
{
printf("正在执行VH3()函数\n");
return EXCEPTION_CONTINUE_SEARCH;
}
LONG SEH1(EXCEPTION_POINTERS *pEP)
{
//除零错误
if (EXCEPTION_INT_DIVIDE_BY_ZERO == pEP->ExceptionRecord->ExceptionCode)
{
g_nVal = 34;
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int _tmain(int argc, TCHAR *argv[])
{
LPVOID lp1 = AddVectoredExceptionHandler(0, VH1);
LPVOID lp2 = AddVectoredExceptionHandler(0, VH2);
LPVOID lp3 = AddVectoredExceptionHandler(1, VH3);
__try
{
Func(g_nVal);
printf("Func()函数执行完成后g_nVal = %d\n", g_nVal);
}
__except(SEH1(GetExceptionInformation()))
{
printf("正在执行main()中的__try __except块");
}
RemoveVectoredExceptionHandler(lp1);
RemoveVectoredExceptionHandler(lp2);
RemoveVectoredExceptionHandler(lp3);
return 0;
}
上述的程序模拟了调用第三方库的情况,比如我们调用了第三方库Func进行某项操作,我们在外层进行了异常处理,但是由于在Func函数中有异常捕获的代码,所以不管外层如何处理,总不能捕获到异常,外层的异常处理代码总是不能执行,这个时候我们注册了3个向量处理函数,由于VH1返回的是EXCEPTION_CONTINUE_SEARCH,这个时候会在继续执行后面注册的向量函数——VH2,VH2返回EXCEPTION_CONTINUE_SEARCH,会继续执行VH3,VH3还是返回EXCEPTION_CONTINUE_SEARCH,那么它会继续执行库函数内层的异常处理,内层的过滤表达式返回EXCEPTION_EXECUTE_HANDLER,这个时候会继续执行异常处理块中的内容,结束程序,如果我们将3个向量函数中的任何一个的返回值改为EXCEPTION_CONTINUE_EXECUTION,那么库中的异常处理块中的内容将不会被执行。
函数AddVectoredExceptionHandler中填入的处理函数也就是上述代码中的VH1 VH2 VH3只能返回EXCEPTION_CONTINUE_EXECUTION和EXCEPTION_CONTINUE_SEARCH,对于其他的值操作系统不认。
将SEH转化为C++异常
C++异常处理并不能处理所有类型的异常而将SEH和C++异常混用,可以达到使用C++异常处理处理所有异常的目的
要混用二者需要在项目属性->C/C++->代码生成->启动C++异常的选项中打开SEH开关。
在混用时可以在SEH的过滤表达式的函数中使用C++异常,当然最好的方式是将SEH转化为C++异常。
通过调用_set_se_translator这个函数指定一个规定格式的回调函数指针就可以利用标准C++风格的关键字处理SEH了。下面是它们的定义:
_set_se_translator(_se_translator_function seTransFunction);
typedef void (*_se_translator_function)(unsigned int, struct _EXCEPTION_POINTERS* );
使用时,需要自定义实现_se_translator_function函数,在这个函数中通常可以通过throw一个C++异常的方式将捕获的SEH以标准C++EH的方式抛出
下面是一个使用的例子:
class SE_Exception
{
public:
SE_Exception(){};
SE_Exception(DWORD dwErrCode) : dwExceptionCode(dwErrCode){};
~SE_Exception(){};
private:
DWORD dwExceptionCode;
};
void STF(unsigned int ui, PEXCEPTION_POINTERS pEp)
{
printf("执行STF函数\n");
throw SE_Exception();
}
void Func(int i)
{
int x = 0;
int y = 5;
x = y / i;
}
int _tmain(int argc, TCHAR *argv[])
{
try
{
_set_se_translator(STF);
Func(0);
}
catch(SE_Exception &e)
{
printf("main 函数中捕获到异常 \n");
}
return 0;
}
程序首先调用_set_se_translator函数定义了一个回掉函数,当异常发生时,系统调用回掉函数,在函数中抛出一个自定义的异常类,在主函数中使用C++的异常处理捕获到了这个异常并成功输出了一条信息。