Windows 异常处理机制学习

文章目录
  1. 1. 关于异常
    1. 1.1. 常见异常:
    2. 1.2. 异常处理机制
  2. 2. Windows向量化异常处理
  3. 3. Windows结构化异常处理(SEH)
    1. 3.1. _try _except
    2. 3.2. SEH嵌套使用
    3. 3.3. VS编译器异常处理编译选项
    4. 3.4. SEH顶层异常处理
  4. 4. C++异常处理
  5. 5. 调试时如何找到异常处理函数?
  6. 6. 后续学习
  7. 7. 终结版

参考资料:
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
11
LONG 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函数入口的快速定位

后续学习

异常处理与MiniDump详解(1) 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
HandleWithoutCrash例子:

#include <windows.h>
#include <Dbghelp.h>
using namespace std;

#pragma auto_inline (off)
#pragma comment( lib, "DbgHelp" )

// 为了程序的简洁和集中关注关心的东西,按示例程序的惯例忽略错误检查,实际使用时请注意
LONG WINAPI MyUnhandledExceptionFilter(
struct _EXCEPTION_POINTERS* ExceptionInfo
)
{
HANDLE lhDumpFile = CreateFile(_T("DumpFile.dmp"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL ,NULL);

MINIDUMP_EXCEPTION_INFORMATION loExceptionInfo;
loExceptionInfo.ExceptionPointers = ExceptionInfo;
loExceptionInfo.ThreadId = GetCurrentThreadId();
loExceptionInfo.ClientPointers = TRUE;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),lhDumpFile, MiniDumpNormal, &loExceptionInfo, NULL, NULL);

CloseHandle(lhDumpFile);

return EXCEPTION_EXECUTE_HANDLER;
}


void Fun2()
{
__try
{
static bool b = false;

if(!b)
{
b = true;
int *p = NULL;
*p = 0;
}
else
{
MessageBox(NULL, _T("Here"), _T(""), MB_OK);
}

}
__except(MyUnhandledExceptionFilter(GetExceptionInformation()))
{
}
}

void Fun()
{
Fun2();
}

int main()
{
Fun();
Fun();

return 1;
}

/*
调用SetUnhandledExceptionFilter设置SEH异常处理函数
调用_set_purecall_handler设置处理纯虚函数调用导致的错误的处理函数
调用_set_se_translator()处理C++异常
调用_set_new_handler设置new失败导致的错误的处理函数
调用_set_new_mode设置malloc失败导致的错误的处理函数
调用_set_invalid_parameter_handler设置CRT无效参数导致的错误的处理函数
调用signal设置信号SIGABRT、SIGINT、SIGTERM的处理函数
调用set_terminate设置terminate处理函数,适用于CRT,这个只对当前线程有效
调用set_unexpected设置unexpected处理函数。这个也是只对当前线程有效
调用signal设置信号SIGFPE、SIGILL、SIGSEGV的处理函数。这个也是对当前线程有效
守护进程:https://github.com/mkdym/DaemonSvc
*/
#include <Strsafe.h>
#include <DbgHelp.h>
LONG __stdcall MyExceptionFun( LPEXCEPTION_POINTERS lpExcept)
{


//SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER(MyExceptionFun));

// EXCEPTION_EXECUTE_HANDLER equ 1 表示我已经处理了异常,可以优雅地结束了
// EXCEPTION_CONTINUE_SEARCH equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理程序显示一个错误框,并结束
//EXCEPTION_CONTINUE_EXECUTION equ -1 表示错误已经被修复,请从异常发生处继续执行


//如果存在调试器则继续到上一层的try-except域中继续查找一个恰当的__except模块
if (IsDebuggerPresent())
{
return EXCEPTION_CONTINUE_SEARCH;
}
typedef BOOL (WINAPI * MiniDumpWriteDumpT)(
HANDLE,
DWORD ,
HANDLE ,
MINIDUMP_TYPE ,
PMINIDUMP_EXCEPTION_INFORMATION ,
PMINIDUMP_USER_STREAM_INFORMATION ,
PMINIDUMP_CALLBACK_INFORMATION
);
wchar_t wszCurDir[MAX_PATH]={0},wszMiniDumpPath[MAX_PATH*2]={0},wszTime[60]={0};
SYSTEMTIME sys={0};

MiniDumpWriteDumpT pfnMiniDumpWriteDump = NULL;
HMODULE hDbgHelp = LoadLibraryW(L"DbgHelp.dll");
if (hDbgHelp)
pfnMiniDumpWriteDump = (MiniDumpWriteDumpT)GetProcAddress(hDbgHelp,"MiniDumpWriteDump");

MessageBoxW(NULL,L"Error,请将当前exe文件目录dmp发送给开发者寻求帮助,谢谢!",NULL, MB_ICONERROR);

HANDLE hProcess=GetCurrentProcess();
DWORD dwProcessID=GetProcessId(hProcess);
GetCurrentDirectory(sizeof(wszCurDir),wszCurDir);
GetLocalTime( &sys );
StringCchPrintf(wszTime,60,L"%4d%02d%02d%02d%02d%02d", sys.wYear,sys.wMonth,sys.wDay,sys.wHour,sys.wMinute,sys.wSecond);
StringCchPrintf(wszMiniDumpPath,MAX_PATH*2,L"%ws\\MiniDump%ws.dmp",wszCurDir,wszTime);

HANDLE hFile=CreateFileW(wszMiniDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if ( INVALID_HANDLE_VALUE!=hFile && GetLastError()!=ERROR_ALREADY_EXISTS &&pfnMiniDumpWriteDump!=NULL)
{
MINIDUMP_EXCEPTION_INFORMATION mei;
mei.ThreadId=GetCurrentThreadId();
mei.ExceptionPointers=lpExcept;
mei.ClientPointers=FALSE;
pfnMiniDumpWriteDump(hProcess, dwProcessID, hFile, MiniDumpWithProcessThreadData, &mei, NULL, NULL);
CloseHandle(hFile);
}
return EXCEPTION_EXECUTE_HANDLER;
}

终结版

大家想学习Windows的异常处理直接就看这个吧Effective Exception Handling in Visual C++,没有比这更好的了。中文版,都和我没关系……….
更多详细信息
……
CrashRpt
异常处理与MiniDump详解(1) C++异常
软件调试-第24章 异常处理代码的编译
SEH学习报告