Windows 异常处理机制学习
参考资料:
1.Windows核心编程
2.Windows系统程序设计之结构化异常处理
3.C++处理异常技巧-try,catch,throw,finally
4.深入理解C++中的异常处理机制
5.读windows核心编程,结构化异常部分,理解摘要
6.【原创】白话windows之四 异常处理机制(VEH、SEH、TopLevelEH…)
关于异常
常见异常:
- 程序访问一个不可用的内存地址(例如,NULL指针);
- 无限递归导致的栈溢出;
- 向一个较小的缓冲区写入较大块的数据;
- 类的纯虚函数被调用;
- 申请内存失败(内存空间不足);
- 一个非法的参数被传递给C++函数;
- C运行时库检测到一个错误并且需要程序终止执行。
- 访问的对象(文件等)或则地址不存在
- 等等。。。。。
根据异常作用或则产生后后果可以将异常分为错误(重新执行产生异常的指令如页面错误)、陷阱(执行下一条指令如调试断点)和终止(进程终止或则系统崩溃)。 当异常发生后我们可以通过GetExceptionCode获得异常号[其实就和GetLasterro效果相同],通过这个异常号可以查询到异常发生的相关信息。(这里有个调试时候的小技巧当调试时在调试选项将监视窗口调出,然后在监视窗口设置 名称“$ERR,hr” 当动态调试时不管是Getlasterr的值还是GetExceptionCode值都将显示在后面值中。这样就不用导出Getlasterrno了)。
异常处理机制
Windows中主要两种异常处理机制,Windows异常处理(VEH、SEH)和C++异常处理。
Windows异常处理结构未公开的,包含向量化结构异常VEH及结构化异常处理SEH。由操作系统提供的服务,当一个线程出现错误时,操作系统调用用户定义的一个回调函数_exept_handler。回调函数接收到操作系统传递过来的许多有价值的信息,例如异常的类型和发生的地址。使用这些信息,异常回调函数就能决定下一步做什么。
C++异常处理是C++语言的特性,在Windows平台上由系统提供支持(我是这么理解的)。
Windows异常处理顺序流程
- 终止当前程序的执行
- 调试器(进程必须被调试,向调试器发送EXCEPTION_DEBUG_EVENT消息)
- 执行VEH
- 执行SEH
- TopLevelEH(进程被调试时不会被执行)
- 执行VEH
- 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
- 调用异常端口通知csrss.exe
Windows向量化异常处理
向量异常处理(Vectored Exception Handling (VEH))是结构化异常处理的扩展,它是在XP中被引进的。VEH优先权高于SEH,只有所有VEH全不处理某个异常的时候,异常处理权才会到达SEH。每当操作系统发生异常的时候,系统会检测进程的PEB结构中的EnvironmentUpdateCount元素,当PEB. EnvironmentUpdateCount符合要求的时候,系统将会遍历VEH异常链表,VEH链表中的异常处理函数会得到执行,各个VEH中的异常处理函数顺序执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//AddVectoredExceptionHandler添加的函数会在SEH异常函数之前执行
PVOID WINAPI AddVectoredExceptionHandler(
_In_ ULONG FirstHandler, 是否将VEH函数插入到VEH链表头,插入到链表头的函数先执行 1表示头部 0表示尾部
_In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler 异常处理函数指针
);
//AddVectoredContinueHandler添加的函数,会在SEH异常函数之后执行
PVOID WINAPI AddVectoredContinueHandler(
_In_ ULONG FirstHandler,
_In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
LONG NTAPI FirstVectExcepHandler( PEXCEPTION_POINTERS pExcepInfo )
{
if( ... )
{
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
//参数1=1表示插入Veh链的头部,=0表示插入到VEH链的尾部
AddVectoredExceptionHandler( 1, &FirstVectExcepHandler );
VEH处理函数可以返回的值:EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTION。
返回EXCEPTION_EXECUTE_HANDLER是无效的,等同于EXCEPTION_CONTINUE_SEARCH。当一个Veh返回EXCEPTION_CONTINUE_SEARCH,则把异常交给下一个VEH处理。如果返回EXCEPTION_CONTINUE_EXECUTION,认为已经被处理,退出异常处理器在异常指令处继续执行。
VEH对本进程中的任意一个线程有效。
Windows结构化异常处理(SEH)
SEH使用_try、_except、_finally和_leave关键字和RaiseException API EXCEPTION_EXECUTE_HANDLER,SEH是基于线程栈的异常处理机制,所以它只能处理自己线程的异常(但是顶层异常处理对所有线程有效)。
###_try _finally
基本结构1
2
3
4
5
6
7
8__try
{
// 受保护的代码
}
__finally
{
// 结束处理程序
}
这个结构可能是SEH机制中我使用最多的,加上__leave关键字使用起来太方便了。try块可能会因为return,goto,异常等非自然退出,也可能会因为成功执行而自然退出。但不论try块是如何退出的,finally块的内容都会被执行【注意:对于使用ExitProcess或则进程被其他进程终止的情况finally块将不会被执行】。这样我们就可以在finally块中做一些清理工作,不用每次都判断或则使用goto语句。有多方便呢,看下面的图就知道了。图片出处【这篇文章写得非常好,推荐看这篇这里我只是总结哈自己所学。。。】

_try _except
基本结构1
2
3
4
5
6
7
8
9__try
{
// 受保护的代码
}
__except ( /*异常过滤器exception filter*/ )
{
// 异常处理程序exception handler,
//这里的代码只有当try块中发生异常才执行
}
_except ( /异常过滤器exception filter/ )中的异常过滤器取值:
EXCEPTION_EXECUTE_HANDLER(1): 代码执行__except块中的代码,该异常被处理,一般我都写这个
EXCEPTION_CONTINUE_SEARCH(0): 表示继续搜索上一个异常过滤器,由上一级来处理.
EXCEPTION_CONTINUE_EXECUTION(-1):程序试图重新执行引发异常的代码,因为exception可以用一个函数代替,函数的返回值就是exception的值,所以可以在函数中进行一些操作使用原语句可以正确执行,但是并不推荐这样做.
通俗语言版:
EXCEPTION_EXECUTE_HANDLER(1): 这是告诉系统, 我认识这个异常,请执行我的异常处理代码,然后从接下来的第一行代码开始继续执行
EXCEPTION_CONTINUE_SEARCH(0): 这个是告诉系统, 我不认识这个异常, 请继续往外抛异常,让别人处理
EXCEPTION_CONTINUE_EXECUTE(-1): 这个是告诉系统, 我已经在调用filter时修正了这个异常, 请从发生异常的地方继续执行
这个值我们也可以根据具体情况选择,比如下面这个例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15__try {
……
}
__except ( MyFilter( GetExceptionCode() ) )
{
……
}
LONG MyFilter ( DWORD dwExceptionCode )
{
if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION )
return EXCEPTION_EXECUTE_HANDLER ;
else
return EXCEPTION_CONTINUE_SEARCH ;
}
例子中根据传递过来的不同异常结果选择不同的异常过滤器。
这三个不同的过滤处理器处理流程如下图

try finally主要还是编译器做工作,而exception主要是操作系统的工作。
SEH嵌套使用
当程序中存在多个异常处理结构嵌套时就涉及到全局展开、局部展开的问题了,根据不同的异常过滤值有不同的展开方式。具体有多复杂请看参考资料
VS编译器异常处理编译选项
在VC中,你可能会发现一个怪异的现象,就是try-catch块无法捕获像“除0”、“空指针访问”之类的异常。原来,在VC中一般的错误和异常都是用SEH来处理的,不等同于throw抛出的异常。而try-catch对结构化异常的处理,是由编译参数EH来控制的。
参数 | 无EH参数 | EHs(EHsc) | EHa(Ehac) |
---|---|---|---|
try-catch | 不处理异常 | 只处理C++标准异常,代码优化较好 | 处理C++标准异常和结构化异常,代码优化较差 |
_try _except(vs2005及以后) | 处理C++标准异常和结构化异常 | 处理C++标准异常和结构化异常 | 处理C++标准异常和结构化异常 |
_try _except(vs2005之前) | 只处理结构化异常 | 只处理结构化异常 | 只处理结构化异常 |
SEH顶层异常处理
顶层的SEH异常处理函数对这个进程中所有线程都是有效的,所以只用在main()函数开始的地方设置一次就够了。1
2
3
4
5
6
7
8
9
10
11LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionPtrs)
{
// Do something, for example generate error report
//..
// Execute default exception handler next return EXCEPTION_EXECUTE_HANDLER;
}
void main()
{
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
// .. some unsafe code here
}
顶层异常处理函数也可以返回三个值:EXCEPTION_CONTINUE_SEARCH、EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_EXECUTION。
返回EXCEPTION_CONTINUE_EXECUTION时,和SEH一样。
返回EXCEPTION_EXECUTE_HANDLER时,则直接杀死该进程。
返回EXCEPTION_CONTINUE_SEARCH时,会查注册表,检查是否存在实时调试器。注册表路径:KLM\software\microsoft\windows nt\currentvsrsion\aedebug。如果Auto==1,Debugger!=NULL则根据Debugger中指示的参数启动实时调试器,让调试器处理该异常。(如果不存在顶层异常且进程没被调试,也会检查并启动实时调试器)
C++异常处理
先看一个C++未处理异常的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55#include <exception>
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Up{};
class Fit{};
void g();
//异常规格说明,f函数只能抛出Up 和Fit类型的异常
void f(int i)throw(Up,Fit) {
switch(i) {
case 1: throw Up();
case 2: throw Fit();
}
g();
}
void g() {throw 47;}
void my_ternminate() {
cout << "I am a ternminate" << endl;
exit(0);
}
void my_unexpected() {
cout << "unexpected exception thrown" << endl;
// throw Up();
throw 8;
//如果在unexpected中继续抛出异常,抛出的是规格说明中的 则会被捕捉程序继续执行
//如果抛出的异常不在异常规格说明中分两种情况
//1.异常规格说明中有bad_exception ,那么会导致抛出一个bad_exception
//2.异常规格说明中没有bad_exception 那么会导致程序调用ternminate函数
// exit(0);
}
int main() {
set_terminate(my_ternminate);
set_unexpected(my_unexpected);
for(int i = 1;i <=3;i++)
{
//当抛出的异常,并不是异常规格说明中的异常时
//会导致最终调用系统的unexpected函数,通过set_unexpected可以
//用来设置自己的unexpected汗函数
try {
f(i);
}catch(Up) {
cout << "Up caught" << endl;
}catch(Fit) {
cout << "Fit caught" << endl;
}catch(bad_exception) {
cout << "bad exception" << endl;
}
}
}
调试时如何找到异常处理函数?
- SEH头部保存在TEB中,可以通过TEB(fs:[0])找到seh头部然后找到异常处理函数(还没深入研究)
- VEH函数入口的快速定位
后续学习
1 | HandleWithoutCrash例子: |
终结版
大家想学习Windows的异常处理直接就看这个吧Effective Exception Handling in Visual C++,没有比这更好的了。中文版,都和我没关系……….
更多详细信息
……
CrashRpt
异常处理与MiniDump详解(1) C++异常
软件调试-第24章 异常处理代码的编译
SEH学习报告