Windows 异常处理
C++异常处理概述
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况。C++和Java/C#类似,提供了
try
、
catch
、
throw
这一组关键字来捕获异常:
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字: try、catch、throw 。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在想要处理问题的地方,通过异常处理程序捕获异常。 catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
例如:
#include <windows.h>
#include <iostream>
int main() {
try {
throw std::exception("An exception was thrown.");
return -1;
} catch (std::exception e) {
std::cout << "Exception handled here." << std::endl;
return 0;
上面的例子中,
try
块中抛出了一个
std::exception
异常,导致流程直接跑到了
catch
块中,那么
return -1;
将永远不会执行到。
使用
throw;
,可以将异常抛给上一个
try
块:
#include <windows.h>
#include <iostream>
void foo() {
try {
throw std::exception("An exception was thrown.");
} catch (std::exception) {
throw;
int main() {
try {
foo();
return -1;
} catch (std::exception e) {
std::cout << "Exception handled here." << std::endl;
return 0;
foo()
发现自己无法处理这个异常,因此将
std::exception
原封不动交给其上一层调用者
main()
函数。
需要注意的是,如果出现多个
catch
块,
catch
块会按照从上到下的顺序进行匹配:
#include <windows.h>
#include <iostream>
int main() {
try {
throw std::invalid_argument("An invalid argument exception was thrown.");
return -1;
} catch (std::exception e) {
std::cout << "Exception handled here." << std::endl;
} catch (std::invalid_argument e) {
std::cout << "An invalid argument exception was thrown." << std::endl;
return -2;
return 0;
在这种情况,
std::invalid_argument
将不会被捕获到,因为它一定会匹配到它的基类
std::exception
,这种情况下MSVC会给出一个警告:
warning C4286: “std::invalid_argument”: 由基类(“std::exception”)在行 9 上捕获
如果一个异常始终都没有被捕获,那么它最终会被内核捕获。在调试下,它会通知调试器出错的位置:
结构化异常 (SEH)
Windows下的C++异常,底层实现是Windows的结构化异常(Structured Exception Handling)。结构化异常分为两部分,一个叫做终止处理器(Termination Handlers),一个叫做异常处理器(Exception Handlers)。
终止处理器(Termination Handlers)
终止处理器的使用方法
终止处理器由
__try
和
__finally
组成。在流程离开
__try
的时候,
__finally
会被调用,例如:
void foo() {
int* i = nullptr;
__try {
i = new int[5];
return;
} __finally {
delete[] i;
在这个函数中,我们无论如何都希望在
return
之前能够释放掉申请在堆上的内存
i
,所以我们在
__finally
中进行了释放工作。如果不使用
__try
、
__finally
,那么我们会使用一个智能指针或者RAII机制来保证这块内存会被释放。
__finally
块被执行,有下列4种情况:
-
程序流程走完了整个
__try
块。 -
使用
__leave
关键字,离开一个__try
块。 -
程序在
__try
块中遇到了如return
、goto
、continue
、break
这样的语句,导致退出__try
。这个就是上面的例子,被称为“局部展开(local unwind)”。 -
程序在
__try
中遇到了异常,例如访问非法内存等,触发了一个全局展开(global unwind)。
下面是一个触发全局展开的例子:
void foo() {
int* i = nullptr;
__try {
*i = 5;
return;
} __finally {
MessageBoxA(NULL, "An exception occurs.", "Warning", MB_OK);
delete[] i;
由于
i
为空,因此
*i=5
触发了一个内存访问异常,造成了全局展开,最终
__finally
块被执行,在非调试模式下,一个MessageBox被弹出。
流程是否正常走到中止处理器
有时候,我们需要知道执行到
__finally
中的代码,是在正常执行完之后进来的,还是因为全局展开或局部展开而进入的
__finally
的。
为此,Windows提供了一个intrinsic function,来进行判断这个中断流程是否异常:
BOOL AbnormalTermination(void);
AbnormalTermination()
只能在
__finally
块中被使用,它类似于一个关键字,将会被展开为若干汇编代码。当我们通过上述4种情况中的1、2进入
__finally
中止处理器时,
AbnormalTermination()
返回
FALSE
表明是正常进入;否则,这表明发生了一些展开,函数返回
TRUE
,表示异常进入中止处理流程。
例如:
void foo() {
__try {
__leave; // 情况2
} __finally {
BOOL bAbnormal = AbnormalTermination(); // bAbnormal == 0
std::cout << bAbnormal ? "Yes" : "No";
void foo() {
__try {
return; // 情况3
} __finally {
BOOL bAbnormal = AbnormalTermination(); // bAbnormal == 1
std::cout << bAbnormal ? "Yes" : "No";
异常处理器
异常处理器使用方法
异常处理器是由
__try
和
__except
组成的一个处理器:
__try {
// Guarded body
__except (exception filter) {
// Exception handler
}
其中,
__try
块中是表示被保护的代码,而
__except
块中,表示的是出现异常时需要执行的一些操作。exception filter是一个表达式,返回值只能是以下三种:
-
EXCEPTION_EXECUTE_HANDLER
-
EXCEPTION_CONTINUE_EXECUTION
-
EXCEPTION_CONTINUE_SEARCH
需要注意的是,和C++的
try...catch...catch...
不同,SEH中
__try
只能要么和一个
__except
搭配,要么和一个
__finally
搭配,不能同时出现多个
__except
或同时出现
__except
和
__finally
。
当
__try
块中的代码发生异常(如除0、内存访问异常等)
EXCEPTION_EXECUTE_HANDLER
如果exception filter表达式返回的是
EXCEPTION_EXECUTE_HANDLER
,那么说明一旦发生了异常,流程将跳转到
__except
块,而不会继续执行
__try
块中的内容。
例如:
void foo() {
int* i = nullptr;
__try {
*i = 5;
} __except(EXCEPTION_EXECUTE_HANDLER) {
std::cout << "An exception occurs.";
上面的例子中,我们尝试为一个空指针赋值立即数5,此时引起了一个内存访问异常。在执行
*i=5;
时,流程会马上跳到
__except
块中,然后打印一行
An exception occurs.
,接着,离开
__except
块,并结束
foo()
函数。
Exception filter可以是一个函数,而不一定只是一个常数,例如:
int EvaluateFilter(void*) {
return EXCEPTION_EXECUTE_HANDLER;
void foo() {
int* i = nullptr; // 1. 访问发生异常
__try {
*i = 5;
} __except (EvaluateFilter(i)) { // 2. 执行EvaluateFilter
std::cout << "An exception occurs."; // 3. 执行异常处理
这样做的好处是,我们可以根据参数的值,来选择返回哪种返回值。在上面的例子中,我们返回了
EXCEPTION_EXECUTE_HANDLER
,但是我们也可以根据参数传入的不同,来返回
EXCEPTION_CONTINUE_EXECUTION
或
EXCEPTION_CONTINUE_SEARCH
。
注释中的数字表明了函数执行的顺序,Exception filter一定是在出现了异常的时候才被执行。
全局展开流程
下面看一个比较复杂的例子:
void bar() {
int* i = nullptr;
__try {
*i = 5; // 3. 赋值,出现内存访问异常
} __finally {
std::cout << "Finally." << std::endl; // 5. 中止处理器被调用
int EvaluateFilter() {
return EXCEPTION_EXECUTE_HANDLER;
void foo() {
__try {
bar(); // 2. bar()被调用
} __except (EvaluateFilter()) { // 4. EvaluateFilter()被调用
std::cout << "An exception occurs." << std::endl; // 6. 异常处理器被调用
void main() {
foo(); // 1. foo被调用
-
主函数从
main()
进入,foo()
被调用。 -
foo()
中的__try
模块中的bar()
被调用。 -
*i=5
赋值时,触发了一个内存访问异常,此时触发了一个全局展开。 -
全局展开会找最接近目前流程中的
__except
块,并执行它的exception filter,也就是EvaluateFilter()
。 -
由于即将进入
__except
块,在离开bar()
之前,bar()
的__finally
块将会被调用。 -
foo()
中的__except
块异常处理器将会被调用。
停止全局展开
只要
__finally
块中执行到了一个
return
语句,那么全局展开将会被中止,异常处理器不会被调用:
void bar() {
int* i = nullptr;
__try {
*i = 5;
} __finally {
return;
int EvaluateFilter() {
return EXCEPTION_EXECUTE_HANDLER;
void foo() {
__try {
bar();
} __except (EvaluateFilter()) {
// 下面这个语句不会被调用
std::cout << "An exception occurs." << std::endl;
void main() {
foo();
EXCEPTION_CONTINUE_EXECUTION
如果exception filter的返回值是
EXCEPTION_CONTINUE_EXECUTION
,那么在发生异常后,代码流程将会从发生异常的那一句开始:
int global;
int FixNullptr(int** i) {
if (!*i) {
*i = &global; // 2. i的地址赋值为一个新的值,指向一个全局地址(&global)
return EXCEPTION_CONTINUE_EXECUTION; // 3. 重新执行语句 *i = 5
return EXCEPTION_EXECUTE_HANDLER;
void foo() {
int* i = nullptr;
__try {
*i = 5; // 1. i为空,触发内存访问异常
} __except (FixNullptr(&i)) {
std::cout << "An exception occurs";
void main() {
foo();
上面的代码执行过程如下:
-
程序从
main()
开始进入,foo()
被调用,*i = 5
触发访问异常。 -
程序进入
FixNullptr()
函数,发现原来是空指针问题,所以将i
的值修改为指向一个全局变量global
的地址。 -
返回
EXCEPTION_CONTINUE_EXECUTION
,接下来重新执行*i = 5
。
在EXCEPTION_CONTINUE_EXECUTION这样的情况下,__except块中的内容不会被调用,每当__try块出现了异常时,则会调用exception filter(即FixNullptr())尝试进行筛选或修复。
看上去好像我们是解决了空指针的问题,不过,事情还没有结束。我们需要谨慎使用EXCEPTION_CONTINUE_EXECUTION,在上面的例子中,即便是重新赋值*i=5,还是会出现异常。那么接下来的故事是这样的:
- 程序重新执行*i = 5,此时重新执行下面的指令 :
需要注意的是,5被直接赋值到了
eax
所指向的地址中。不过,虽然我们是重新赋值了
i
,但是
eax
我们并没有更新!那么,此时又会触发一个内存访问异常。
5. 流程再次进入
FixNullptr()
中,但是,此时
i
所指向的地址已经不为空了,所以返回
EXCEPTION_EXECUTION_HANDLER
。
6.
foo()
中的
__except
块异常处理器会被调用,程序输出
An exception occurs.
总结以下,我们需要谨慎使用
EXCEPTION_CONTINUE_EXECUTION
,原因有以下两点:
-
如果exception filter返回的是
EXCEPTION_CONTINUE_EXECUTION
,那么一旦没有解决异常,exception filter会无限被调用,导致程序卡死。 -
EXCEPTION_CONTINUE_EXECUTION
总是重新执行出错的那一条指令,所以我们无法保证是否总能准确地解决异常。
EXCEPTION_CONTINUE_SEARCH
如果exception filter返回
EXCEPTION_CONTINUE_SEARCH
,那么类似于C++异常中的
throw
,将这个异常抛给上一个
__try
__except
块去处理。
请看下面的情况:
void bar() {
__try {
int* i = nullptr;
*i = 5;
__except(EXCEPTION_CONTINUE_SEARCH) {
// Never goes here
std::cout << "An EXCEPTION_CONTINUE_SEARCH occurs";
void foo() {
__try {
bar();
} __except (EXCEPTION_EXECUTE_HANDLER) {
std::cout << "An exception occurs";
void main() {
foo();
当
bar()
被调用时,里面触发了一个访问异常,由于其exception filter返回了
EXCEPTION_CONTINUE_SEARCH
,因此
__except
块中的代码不会被执行,而是将异常抛给了上一个
__except
块执行,上一个
__except
块正好为
EXCEPTION_EXECUTE_HANDLER
,所以
An exception occurs
被打印了出来。
获取异常原因
GetExceptionCode
Windows提供了一个intrinsic function
GetExceptionCode()
来获取当前的异常类型。
GetExceptionCode()
只能在异常处理上下文中被调用。例如:
void foo() {
__try {
int x = 0;
int y = 4 / x; // y is used later so this statement is not optimized away
} __except (
((GetExceptionCode()) == EXCEPTION_INT_DIVIDE_BY_ZERO)
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH) {
// Handle divide by zero exception.
std::cout << GetExceptionCode() << std::endl;
// GetExceptionCode(); 直接这么调用会触发一个编译错误
GetExceptionCode()
返回值表示各种各样的异常类型,如
GetExceptionCode macro - Win32 apps
所示,最常用的有
EXCEPTION_ACCESS_VIOLATION
,除此以外还有
EXCEPTION_INT_DIVIDE_BY_ZERO
,表示一个除0错误。如上面的例子,如果是一个除0错误,则交给异常处理器来处理,否则就通过
EXCEPTION_CONTINUE_SEARCH
交给上一层
__except
来处理。
GetExceptionInformation
Windows提供了另外一个intrinsic function来获取异常错误的详细信息:
LPEXCEPTION_POINTERS GetExceptionInformation(void);
其中:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
CONTEXT
结构记录了目前寄存器的上下文信息,例如每一个寄存器的值等,和机器相关。
EXCEPTION_RECORD
记录了和当前异常相关的一些信息,解释如下:
typedef struct _EXCEPTION_RECORD {
// 异常码,和GetExceptionCode()获得的值一样
DWORD ExceptionCode;
// 0表示这个异常可以继续,EXCEPTION_NONCONTINUABLE 表示这个异常不可以继续
// 如果尝试继续一个EXCEPTION_NONCONTINUABLE 异常,则会抛出一个新的
// EXCEPTION_NONCONTINUABLE_EXCEPTION 异常。
DWORD ExceptionFlags;
// 嵌套异常。如果一个异常处理函数引起了这个异常,
// 那么ExceptionRecord表示上一个异常信息的地址。
// 如果不是嵌套异常则为NULL。
struct _EXCEPTION_RECORD *ExceptionRecord;
// 异常指令发生的地址
PVOID ExceptionAddress;
// 下面两个参数表示异常和业务相关的上下文
// 异常所携带的参数个数
DWORD NumberParameters;
// 异常所携带的参数指针
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
特别需要注意的是,
GetExceptionInformation()
只能在exception filter中被调用,而不能在
__except
块中被调用,否则会得到一个编译错误。如果需要在
__except
块中使用异常信息,我们需要先在栈上面分配空间,然后在exception filter中进行赋值:
void foo() {
EXCEPTION_RECORD record;
CONTEXT context;
__try {
int* i = nullptr;
*i = 5;
} __except (record = *((GetExceptionInformation())->ExceptionRecord),
context = *((GetExceptionInformation())->ContextRecord),
EXCEPTION_EXECUTE_HANDLER) {
return;
Exception filter是一个非常骚气的逗号表达式的写法,完成了对
EXCEPTION_RECORD
和
CONTEXT
的赋值,并且返回一个
EXCEPTION_EXECUTE_HANDLER
。
通过执行上面的代码,我们可以看到一个内存访问异常的
EXCEPTION_RECORD
如下:
ExceptionCode
的0xc0000005表示
EXCEPTION_ACCESS_VIOLATION
,
ExceptionAddress
表示产生异常的那一条指令:
当我们的exception filter返回
EXCEPTION_CONTINUE_EXECUTION
时,就会重新执行这一条指令。
ExceptionInformation
和
NumberParameters
则表明了这个异常所携带的参数,可以看到一共携带了2个参数,分别时
ExceptionInformation[0]
和
ExceptionInformation[1]
,这些参数都是和异常类型相关的。
软件异常
上面所说的各种异常,都是CPU在执行指令过程中产生的异常。不过,我们也可以通过调用一个函数来产生上面的异常:
void RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
const ULONG_PTR *lpArguments
当某一个函数调用了
RaiseException()
之后,内核会根据里面4个参数产生一个
EXCEPTION_RECORD
,并抛出一个异常。
如果
dwExceptionFlags
为0,那么这个异常可以用
EXCEPTION_CONTINUE_EXECUTION
来继续执行,如果
dwExceptionFlags
为
EXCEPTION_NONCONTINUABLE
,若尝试继续执行则会得到一个
EXCEPTION_NONCONTINUABLE_EXCEPTION
。
未捕获的异常
自定义未捕获异常Exception Filter
如果一个异常从产生开始到你的用户代码都未捕获(即一直都是
EXCEPTION_CONTINUE_SEARCH
),那么它最终会被内核捕获。
一个未捕获的异常被称为 未处理异常 (Unhandled exception)。Windows提供了一个方法,能够有最后一次处理未处理异常的机会:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
这个函数指定了一个新的异常处理函数,并返回旧的异常处理函数。
一个异常处理函数签名如下:
typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
_In_ struct _EXCEPTION_POINTERS *ExceptionInfo
异常处理函数可能有3种返回值:
-
EXCEPTION_EXECUTE_HANDLER
:这种情况下程序将悄无声息地被中止,不会产生任何通知,全局展开会被执行,__finally
块也会被执行。 -
EXCEPTION_CONTINUE_EXECUTION
:产生异常的那一条指令将会被重新执行。如果你没有修复异常,那么这一条指令以及这个处理函数将会被无限执行。 -
EXCEPTION_CONTINUE_SEARCH
:这个异常真的是没有在用户态被捕获,那么它将会被移交到内核去处理。
MSVC默认的Exception Filter
在MSVC下,一个程序启动时,会注册一个默认的exception filter:
exe_common.inl
:
static __declspec(noinline) int __cdecl __scrt_common_main_seh()
if (!__scrt_initialize_crt(__scrt_module_type::exe))
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
bool has_cctor = false;
__try
bool const is_nested = __scrt_acquire_startup_lock();
if (__scrt_current_native_startup_state == __scrt_native_startup_state::initializing)
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
else if (__scrt_current_native_startup_state == __scrt_native_startup_state::uninitialized)
_initterm(__xc_a, __xc_z);
has_cctor = true;
//.....
__except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
// Note: We should never reach this except clause.
int const main_result = GetExceptionCode();
if (!__scrt_is_managed_app())
_exit(main_result);
if (!has_cctor)
_c_exit();
return main_result;
_initterm()
会调用
pre_cpp_initialization()
:
static void __cdecl pre_cpp_initialization()
// Before we begin C++ initialization, set the unhandled exception
// filter so that unhandled C++ exceptions result in std::terminate
// being called:
__scrt_set_unhandled_exception_filter();
_set_new_mode(_get_startup_new_mode());
extern "C" void __cdecl __scrt_set_unhandled_exception_filter()
SetUnhandledExceptionFilter(__scrt_unhandled_exception_filter);
__scrt_unhandled_exception_filter()
实现如下:
extern "C" LONG WINAPI __scrt_unhandled_exception_filter(LPEXCEPTION_POINTERS const pointers)
auto const exception_record = reinterpret_cast<EHExceptionRecord*>(pointers->ExceptionRecord);
if (PER_IS_MSVC_PURE_OR_NATIVE_EH(exception_record))
_pCurrentException = reinterpret_cast<EHExceptionRecord*>(exception_record);
_pCurrentExContext = pointers->ContextRecord;
terminate();
return EXCEPTION_CONTINUE_SEARCH;
宏
PER_IS_MSVC_PURE_OR_NATIVE_EH
表示,这个异常是否为一个C++异常,如果是,则调用
terminate()
,其中它会调用
abort()
,弹出一个错误提示,并且结束进程:
void main() {
// throw了一个int,这是一个C++异常,在ExceptionRecord有特别的标记
// 可以被PER_IS_MSVC_PURE_OR_NATIVE_EH识别
throw 5;
如果不是一个C++异常,则会返回
EXCEPTION_CONTINUE_SEARCH
继续交给上层处理:
void main() {
int* i = nullptr;
*i = 5; // 内存非法访问,继续向上交给内核处理
上层exception filter函数是
_seh_filter_exe()
或
_seh_filter_dll()
,它在MSDN上有对应的介绍:
主要是用于产生对应的信号。
Unhandled Exception Filter调用过程
我们可以通过
SetUnhandledExceptionFilter()
注册自己的exception filter函数,从异常产生到这个函数被调用,堆栈如下:
自己的exception filter
MyFilter
,通过KernelBase.dll中的
_UnhandledExceptionFilter()
调用进来,其上层是
__RtlUserThreadStart()
,所有的线程都通过
BaseThreadStart()
启动,代码类似如下:
VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
__except (_UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
// NOTE: We never get here
_UnhandledExceptionFilter()
处理异常的流程如下:
-
允许资源访问,并且返回EXCEPTION_CONTINUE_EXECUTION
如果是访问exe或者dll资源非法,将页面设置为PAGE_READWRITE
并返回EXCEPTION_CONTINUE_EXECUTION
重新执行指令。
-
检查是否有调试器
如果当前存在调试器(::IsDebuggerPresent()
),那么_UnhandledExceptionFilter()
返回EXCEPTION_CONTINUE_SEARCH
,异常交给上一层来处理,此时这个异常会被调试器捕获。调试器生成ExceptionInformation
,进入调试状态。
-
调用你设置TopLevel的Exception Filter
如果不存在调试器,并且你已经调用过SetUnhandledExceptionFilter()
来设置一个顶层的Exception filter,那么你的Exception filter就会被调用。
-
再次通知调试器
在第3步中,如果你的Exception filter仍然返回EXCEPTION_CONTINUE_SEARCH
,那么异常没有被捕获,将继续交给上一层处理。在这一层会继续通知调试器来进行调试。如果没有调试器,那么将进入步骤5。
-
中止程序
当异常到底这一层的时候,程序会读取SetErrorMode()
的值,来决定是否需要弹出错误提示框。
例如,当你的进程通过
SetErrorMode()
设置了
SEM_NOGPFAULTERRORBOX
,那么在这个阶段,
_UnhandledExceptionFilter()
将会返回
EXECEPTION_EXECUTE_HANDLER
,全局展开继续,
__finally
块被执行,然后程序悄无声息地结束,而不会弹出Windows Error Reporting (WER) 错误提示框。
向量化异常(VEH)
在上面的介绍中,我们通过
__try
、
__except
结构化语句来捕获异常,这种方式叫做结构化异常。除此之外,我们可以为每一个异常设置若干处理函数,而这些处理函数会被放到一个链表中,当异常到达时,这些处理函数会被调用。这种不急于栈帧的处理异常的方式,叫做向量化异常。
向量化异常处理函数
我们可以通过调用下面的函数来注册一个向量化异常处理函数,它可以抢在SEH之前捕获异常:
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
其中,如果
First
不为0,那么这个处理函数会被放到链表的首位,第一个被调用;否则,这个处理函数会被放到链表的末尾。
PVECTORED_EXCEPTION_HANDLER
的结构如下:
typedef LONG (NTAPI *PVECTORED_EXCEPTION_HANDLER)(
struct _EXCEPTION_POINTERS *ExceptionInfo
当异常产生时,注册的处理函数一个接一个地被调用。它的返回值无非是下列两种:
-
EXCEPTION_CONTINUE_EXECUTION
:返回后,异常的指令会被重新执行。如果你的VEH处理函数能够修复问题,那么应该返回这个返回值。 -
EXCEPTION_CONTINUE_SEARCH
:表示本处理函数无法处理异常,链表中的下一个处理函数会被调用。如果所有的处理函数都返回EXCEPTION_CONTINUE_SEARCH
,SEH的流程才会开始。
注意,VEH函数不能返回
EXCEPTION_EXECUTE_HANDLER
。
你可以通过下面的函数来移除一个已经注册了的向量化异常处理函数:
ULONG RemoveVectoredExceptionHandler(
PVOID Handle
其中
Handle
是
AddVectoredExceptionHandler()
的返回值。
继续处理函数
在一个未处理异常被SEH处理之前,VEH可以得到通知。通过下面2个函数可以注册/反注册处理函数:
PVOID AddVectoredContinueHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
ULONG RemoveVectoredContinueHandler(
PVOID Handle
继续处理函数和异常处理函数类似,不过它多用于追踪、诊断和调试。
C++异常的实现
如何选择在代码编写的时候是使用C++异常还是结构化异常?如果你在开发C++程序,那么最好使用C++异常;如果开发操作系统相关的程序(Windows程序),那么则可以使用SEH。
MSVC编译器底层其实是用SEH来实现了C++的异常。C++异常中,
catch
块代码会被转换为
__except
块代码,而
throw
则会被转换为
RaiseException()
,
throw
中的变量会成为
RaiseException()
中的附加参数。
例如有以下代码:
void Foo() {
try {
// try body
throw 5;
} catch (int x) {
// catch body
MSVC会生成对应的SEH(伪)代码:
void Foo() {
__try {
// try body
RaiseException(Code=0xE06D7363,
Flag=EXCEPTION_NONCONTINUABLE,
Args=5);
} __except ((ArgType == Integer) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
// catch body
有几点细节需要注意:
- C++异常的异常码为0xE06D7363,这个魔法数字中的 0D 73 63表示字符串 msc 。
-
任何C++异常,其
Flag
都为EXCEPTION_NONCONTINUABLE
,说明C++异常无法继续执行,对应的SEH中的__except
块中的exception filter的返回值,只可能是EXCEPTION_EXECUTE_HANDLER
或EXCEPTION_CONTINUE_SEARCH
。如果你一定要编写可以继续执行的代码,那么才考虑用SEH代替C++异常。 -
RaiseException
中所携带的参数个数和参数内容由编译器自己决定,细节没有公开。 -
如果
throw
的参数对应的类型正好是我们所catch
的类型,那么SEH中exception filter会返回EXCEPTION_EXECUTE_HANDLER
来让catch
块中的语句执行,否则返回EXCEPTION_CONTINUE_SEARCH
来选择上一个catch
栈帧来进行求值。
Visual Studio中异常的调试
VS中提供了出色的异常调试机制,通过Debug->Exceptions...,可以打开异常设置窗口:
可以看到,VS对于每一类异常都归了类,你可以选择某一些异常,在第一次被触发的时候停止程序,此时线程还没有机会来查找exception filter,但是我们可以抢先断下来,设置断点进行调试。
CRT中常见的异常处理器
MSVC中的C++运行时(C++ Runtime, CRT)中提供了一些捕获异常的处理函数。常见的有下面几个:
_set_invalid_parameter_handler
_invalid_parameter_handler _set_invalid_parameter_handler(
_invalid_parameter_handler pNew
// _invalid_parameter_handler签名如下
void _invalid_parameter(
const wchar_t * expression,
const wchar_t * function,
const wchar_t * file,
unsigned int line,
uintptr_t pReserved
调用
set_invalid_parameter_handler()
时,将设置一个新的处理函数,然后返回旧的处理函数。
在许多C函数被调用时,会检查参数是否合法,如果不合法,则会设置
errno
,同时
_set_invalid_parameter_handler()
中所设置的处理函数也会被调用。
_set_purecall_handler
typedef void (__cdecl* _purecall_handler)(void);
_purecall_handler __cdecl _get_purecall_handler(void);
_purecall_handler __cdecl _set_purecall_handler(
_purecall_handler function
如果一个纯虚函数被调用了,如下面这样的情况:
class Base {
public:
virtual void foo()=0;
Base() { call_foo();}
void call_foo() { foo(); }
class Derived: Base{
void foo() { }
int main() {
Derived d;
由于
Base
在构造过程中,
Derived
的虚表尚未创建,所以基类构造函数
foo()
指向的是基类的纯虚函数,此时
_set_purecall_handler()
所设置的处理程序就会被调用。
一旦一个纯虚函数被调用,则
_purecall()
会被调用,紧接着
abort()
将会被调用:
// VS2010
void __cdecl _purecall(
_purecall_handler purecall = (_purecall_handler) DecodePointer(__pPurecall);
if(purecall != NULL)
purecall();
/* shouldn't return, but if it does, we drop back to
default behaviour
_NMSG_WRITE(_RT_PUREVIRT);
/* do not write the abort message */
_set_abort_behavior(0, _WRITE_ABORT_MSG);
abort();
可以看到,在调用
abort()
之前,我们还是有一次机会来调用
_purecall_handler
的。一般情况下
_purecall_handler
应该直接终止程序,如果它没有这么做,或者根本没有设置
_purecall_handler
,那么程序则会调用
abort()
被终止。
_set_new_handler
_PNH _set_new_handler( _PNH pNewHandler );
其中
_PNH
定义在了
new.h
中,定义如下:
typedef int (__cdecl * _PNH)( size_t );
每当你的程序
new
失败时,这个回调函数将会被调用。如果这个回调函数返回非0,则系统会重新分配空间(重试
new
),如果返回0,则表示这一次
new
真的是失败了。所以,new handler可以用来做一些失败时的资源管理。
默认情况下,
malloc
失败时并不会调用new handler。如果你希望
malloc
失败时也调用new handler,则使用
_set_new_mode()
更改默认行为:
_set_new_mode(1); // malloc 在失败时调用new handler
_set_new_mode(0); // 默认行为,malloc失败时不调用new handler
举例来说,假设我们有下面这样一段代码(32位程序):
#include "stdafx.h"
#include <new.h>
#include <limits>
#include <stdlib.h>
int handle_program_memory_depletion( size_t )
return 0;
int main( void )
_set_new_handler( handle_program_memory_depletion );
_set_new_mode(1);
int *pi = (int*)malloc(0x7fffffff);
在程序运行过程中,
malloc
分配了一个过大的内存,在VS2010的Release模式下,
malloc
实现如下:
void * __cdecl _malloc_base (size_t size)
void *res = NULL;
// validate size
if (size <= _HEAP_MAXREQ) {
for (;;) {
// allocate memory block
res = _heap_alloc(size);
// if successful allocation, return pointer to memory
// if new handling turned off altogether, return NULL
if (res != NULL)
break;
if (_newmode == 0)
errno = ENOMEM;
break;
// call installed new handler
if (!_callnewh(size))
break;
// new handler was successful -- try to allocate again
} else {
_callnewh(size);
errno = ENOMEM;
return NULL;
RTCCALLBACK(_RTC_Allocate_hook, (res, size, 0));
if (res == NULL)
errno = ENOMEM;
return res;
注意,
_newmode
就是我们通过
_set_new_mode()
设置进去的1,所以我们会调用到
_callnewh()
:
extern "C" int __cdecl _callnewh(size_t size)
_PNH pnh = (_PNH) DecodePointer(_pnhHeap);