为了程序的健壮性,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++的异常处理捕获到了这个异常并成功输出了一条信息。

windows 异常处理相关推荐

  1. 逆向与破解-windows异常处理机制

    以前看到过的很棒的一个讲解SEH的,非常的详细和简单易懂,不需要特别纠结具体的结构和处理的方法,初期对过程有一定的掌握就可以.以下为原文 深入解析结构化异常处理(SEH) - by Matt Piet ...

  2. 攻击windows异常处理机制SEH

    转载自个人博客0pt1mus 0x00 简介 本文主要有两个部分.第一部分介绍windows异常处理机制中的SEH,详细介绍SEH的工作原理.第二部分介绍如何通过栈溢出实现利用SEH来绕过GS. 0x ...

  3. C++及Windows异常处理(try,catch; __try,__finally; __try, __except)——一道笔试题引起的探究

    题目: int* p = 0x00000000; // pointer to NULL puts( "hello "); __try{ puts( "in try &qu ...

  4. windows异常处理

    https://github.com/junligong/WinCrashHandler.git 1.创建MiniDump 2.获取到dump信息 3.自定义完成(上报功能,需要自定义完成) 4.相关 ...

  5. Windows系统程序设计之结构化异常处理

    标 题: [原创]Windows系统程序设计之结构化异常处理 作 者: 北极星2003 时 间: 2006-09-20,20:21:28 链 接: http://bbs.pediy.com/showt ...

  6. Windows异常学习笔记(五)—— 未处理异常

    Windows异常学习笔记(五)-- 未处理异常 要点回顾 最后一道防线 实验一:理解最后一道防线 实验二:新线程的最后一道防线 总结 UnhandledExceptionFilter 实验三:理解U ...

  7. 深入解析结构化异常处理(SEH) - by Matt Pietrek

    目录 1.浅析SEH 2.移向更深处 3.编译器层面的SEH 4.扩展的异常处理帧 5.ShowSEHFrames程序 6.展开 7.未处理异常 8.进入地狱 9.结论 ​尽管以前写过一篇SEH相关的 ...

  8. windows pwn 基础知识

    环境搭建 checksec winchecksec winchecksec 是 windows 版的 checksec ,不过有时候结果不太准确. checksec(x64dbg) x64dbg 的插 ...

  9. 利用Win32 Debug API打造自己的调试器Debugger

    很多朋友都梦想有自己的Debugger程序,今天我们就来自己制作一个.作为一个Debugger程序,其最基本的功能框架其实就是完成2件事情:  启动目标程序.  实时监控目标程序的运行,并做出相应 ...

最新文章

  1. Mac解决中文matplotlib乱码问题
  2. 用REDIS实现分布式缓存
  3. Linux系统时间\硬件时间(date、tzselect、clock、hwclock、ntpdate)
  4. synchronized和ReentrantLock区别,用新的lock有什么好处?举例说说
  5. oracle查询本月第一天_oracle获取本月第一天和最后一天及Oracle trunc()函数的用法...
  6. 十年生死轮回,看国产手机发展四个阶段
  7. 简单总结手机app测试,弱网测试
  8. 【z变换】2. z变换的性质
  9. 【C/C++学习】之内存分配(new,operator new,placement new)详解
  10. 国际结算三大方式之一—信用证 Letter of Credit(L/C)
  11. 计算机用户名起什么好,如何随机取名计算机名-如何改计算机用户名
  12. nagios监控安装配置
  13. PDF文件格式转换攻略:PDF格式转换图片格式
  14. 红黑树(Red-Black Tree,RBT)
  15. IDEA连接数据库自动生成model(get set方法)
  16. 从全球最大同性交友网站抄了一份不一样的2048小游戏
  17. Vim查找、替换与删除常用命令
  18. excel中DATE(year,month,day)函数——oracle自定义函数
  19. 2012总结--第2篇--读书篇
  20. Win10 蓝屏CRITICAL_PROCESS_DIED值为 0x000000EF

热门文章

  1. 120幅中国近现代国画大师真迹长沙展出
  2. XSS跨站攻击解决办法--AntiSamy的配置及使用
  3. OpenWrt挂载U盘
  4. 脉冲淀粉的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  5. 32位怎么兼容64位java_Java 32位与64位兼容
  6. 计算机网络西安邮电大学,计算机网络实验报告西安邮电大学.doc
  7. struts2action无法使用通配符解决办法
  8. 使用Process Explorer和Dependency Walker定位dll库动态启动失败的问题(常用分析工具)
  9. 钧道博物馆——022号藏品元早期钧窑红斑长颈瓶
  10. Apache配置+php配置