【翻译】异常和异常处理(windows平台)
翻译的不好,莫怪。
原文地址: http://crashrpt.sourceforge.net/docs/html/exception_handling.html#getting_exception_context
About Exceptions and Exception Handling
About Exception
当程序遇到一个异常或一个严重的错误时,通常意味着它不能继续正常运行并且需要停止执行。
例如,当遇到下列情况时,程序会出现异常:
程序访问一个不可用的内存地址(例如,NULL指针);
l 无限递归导致的栈溢出;
l 向一个较小的缓冲区写入较大块的数据;
l 类的纯虚函数被调用;
l 申请内存失败(内存空间不足);
l 一个非法的参数被传递给C++函数;
l C运行时库检测到一个错误并且需要程序终止执行。
有两种不同性质的异常:结构化异常(Structured Exception Handling, SEH)和类型化的C++异常。
SEH是为C语言设计的,但是他们也能够被用于C++。SEH异常由__try{}__except(){}结构来处理。SEH是VC++编译器特有的,因此如果你想要编写可移植的代码,就不应当使用SEH。
C++中类型化的异常是由try{}catch(){}结构处理的。例如(例子来自这里http://www.cplusplus.com/doc/tutorial/exceptions/):
#include <iostream>
using namespace std;
int main(){
try{
throw 20;
}
catch (int e){
cout << "An exception occrred. Exception Nr. " << e << endl;
}
return 0;
}
结构化异常处理
当发生一个SEH异常时,你通常会看到一个意图向微软发送错误报告的弹出窗口。
你可以使用RaiseException()函数自己产生一个SEH异常。
你可以在你的代码中使用__try{}__except(Expression){}结构来捕获SEH异常。程序中的main()函数被这样的结构保护,因此默认地,所有未被处理的SEH异常都会被捕获。
例如:
#include <Windows.h>
int main(){
int *p = NULL; // pointer to NULL
__try{
// Guarded code
*p = 13; // causes an access violation exception;
}
__except(EXCEPTION_EXECUTE_HANDLER){ // Here is exception filter expression
// Here is exception handler
// Terminate program
ExitProcess(1);
}
return 0;
}
每一个SEH异常都有一个与其相关联的异常码(exception code)。你可以使用GetExceptionCode()函数来获取异常码。你可以通过GetExceptionInformation()来获取异常信息。为了使用这些函数,你通常会像下面示例中一样定制自己的exception filter。
下面的例子说明了如何使用SEH exception filter。
int seh_filter(unsigned int code, struct _EXCEPTION_POINTERS *ep){
// Generate error report
// Execute exception handler
return EXCEPTION_EXECUTE_HANDLER;
}
int main(){
__try{
// .. some buggy code here
}
__except(seh_filter(GetExceptionCode(), GetExceptionInformation())){
// Terminate program
ExitProcess(1);
}
return 0;
}
__try{}__exception(){}结构是面向C语言的,但是,你可以将一个SEH异常重定向到C++异常,并且你可以像处理C++异常一样处理它。我们可以使用C++运行时库中的_set_se_translator()函数来实现。
看一个MSDN中的例子(译者注:运行此例子需打开/EHa编译选项):
#include <cstdio>
#include <windows.h>
#include <eh.h>
void SEFunc();
void trans_func(unsigned int, EXCEPTION_POINTERS *);
class SE_Exception{
private:
unsigned int nSE;
public:
SE_Exception(){}
SE_Exception(unsigned int n) : nSE(n){}
~SE_Exception() {}
unsigned int getSeNumber(){ return nSE; }
};
int main(void){
try{
_set_se_translator(trans_func);
SEFunc();
}
catch(SE_Exception e){
printf("Caught a __try exception with SE_Exception.\n");
}
}
void SEFunc(){
__try{
int x, y=0;
x = 5 / y;
}
__finally{
printf("In finally\n");
}
}
void trans_func(unsigned int u, EXCEPTION_POINTERS* pExp){
printf("In trans_func.\n");
throw SE_Exception();
}
你可能忘记对一些潜在的错误代码使用__try{}__catch(Expression){}结构进行保护,而这些代码可能会产生异常,但是这个异常却没有被你的程序所处理。不用担心,这个未被处理的SEH异常能够被unhandled Exception filter所捕获,我们可以使用SetUnhandledExceptionFilter()函数设置top-levelunhandled exception filter。
异常信息(异常发生时的CPU状态)通过EXCEPTION_POINTERS被传递给exception handler。
例如:
// crt_settrans.cpp
// compile with: /EHa
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
}
top-level SEH exception handler对进程中的每个线程都起作用,因此在你的main()函数开头调用一次就够了。
top-level SEH exception handler在发生异常的线程的上下文中被调用。这会影响异常处理函数从异常中恢复的能力。
如果你的异常处理函数位于一个DLL中,那么在使用SetUnhandledExceptionFilter()函数时就要小心了。如果你的函数在程序崩溃时还未被加载,这种行为是不可预测的。
向量化异常处理(Vectored Exception Handling)
向量化异常处理(VEH)是结构化异常处理的一个扩展,它在Windows XP中被引入。
你可以使用AddVectoredExceptionHandler()函数添加一个向量化异常处理器,VEH的缺点是它只能用在WinXP及其以后的版本,因此需要在运行时检查AddVectoredExceptionHandler()函数是否存在。
要移除先前安装的异常处理器,可以使用RemoveVectoredExceptionHandler()函数。
VEH允许查看或处理应用程序中所有的异常。为了保持后向兼容,当程序中的某些部分发生SEH异常时,系统依次调用已安装的VEH处理器,直到它找到有用的SEH处理器。
VEH的一个优点是能够链接异常处理器(chain exception handlers),因此如果有人在你之前安装了向量化异常处理器,你仍然能截获这些异常。
当你需要像调试器一样监事所有的异常时,使用VEH是很合适的。问题是你需要决定哪个异常需要处理,哪个异常需要跳过。 In program's code, some exceptions may be intentionally guarded by __try{}__except(){} construction, and handling such exceptions in VEH and not passing it to frame-based SEH handler, you may introduce bugs into application logics.
VEH目前没有被CrashRpt所使用。SetUnhandledExceptionFilter()更加适用,因为它是top-level SEH处理器。如果没有人处理异常,top-level SEH处理器就会被调用,并且你不用决定是否要处理这个异常。
CRT 错误处理
除了SEH异常和C++类型化异常,C运行库(C runtime libraries, CRT)也提供它自己的错误处理机制,在你的程序中也应该考虑使用它。
当CRT遇到一个未被处理的C++类型化异常时,它会调用terminate()函数。如果你想拦截这个调用并提供合适的行为,你应该使用set_terminate()函数设置错误处理器(error hanlder)。例如:
#include <iostream>
void my_terminate_handler()
{
// Abnormal program termination (terminate() function was called)
// Do something here
// Finally, terminate program
std::cout << "terminate.\n";
exit(1);
}
int main()
{
set_terminate(my_terminate_handler);
terminate();
return 0;
}
Note:在多线程环境中,每个线程维护各自的unexpected和terminate函数。每个新线程需要安装自己的unexpected和terminate函数。因此,每个线程负责自己的unexpected和terminate处理器。
使用_set_purecall_handler()函数来处理纯虚函数调用。这个函数可以用于VC++2003及其后续版本。这个函数可以用于一个进程中的所有线程。例如(来源于MSDN):
// compile with: /EHa
// _set_purecall_handler.cpp
// compile with: /W1
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>
class CDerived;
class CBase{
public:
CBase(CDerived *derived): m_pDerived(derived) {};
~CBase();
virtual void function(void) = 0;
CDerived * m_pDerived;
};
class CDerived : public CBase{
public:
CDerived() : CBase(this) {}; // C4355
virtual void function(void) {};
};
CBase::~CBase(){
m_pDerived -> function();
}
void myPurecallHandler(void){
printf("In _purecall_handler.");
exit(0);
}
int _tmain(int argc, _TCHAR* argv[]){
//_set_purecall_handler(myPurecallHandler);
CDerived myDerived;
}
使用_set_new_handler()函数处理内存分配失败。这个函数能够用于VC++2003及其后续版本。这个函数可以用于一个进程中的所有线程。也可以考虑使用_set_new_mode()函数来为malloc()函数定义错误时的行为。例如(来自MSDN):
// crt_settrans.cpp
#include <new.h>
int handle_program_memory_depletion( size_t ){
// Your code
}
int main( void ){
_set_new_handler( handle_program_memory_depletion );
int *pi = new int[BIG_NUMBER];
}
在VC++2003中,你能够使用_set_security_error_handler()函数来处理缓冲区溢出错误。这个函数已经被废弃,并且从之后VC++版本的CRT中移除。
当系统函数调用检测到非法的参数时,会使用_set_invalid_parameter_handler()函数来处理这种情况。这个函数能够用于VC++2005及其以后的版本。这个函数可用于进程中的所有线程。
例子(来源于MSDN):
// compile with: /Zi /MTd
#include <stdio.h>
#include <stdlib.h>
#include <crtdbg.h> // For _CrtSetReportMode
void myInvalidParameterHandler(const wchar_t* expression,
const wchar_t* function,
const wchar_t* file,
unsigned int line,
uintptr_t pReserved){
wprintf(L"Invalid parameter detected in function %s."
L" File: %s Line: %d\n", function, file, line);
wprintf(L"Expression: %s\n", expression);
}
int main( ){
char* formatString;
_invalid_parameter_handler oldHandler, newHandler;
newHandler = myInvalidParameterHandler;
oldHandler = _set_invalid_parameter_handler(newHandler);
// Disable the message box for assertions.
_CrtSetReportMode(_CRT_ASSERT, 0);
// Call printf_s with invalid parameters.
formatString = NULL;
printf(formatString);
return 0;
}
C++信号处理C++ Singal Handling
C++提供了被称为信号的中断机制。你可以使用signal()函数处理信号。
Visual C++提供了6中类型的信号:
l SIGABRT Abnormal termination
l SIGFPE Floating-point error
l SIGILL Illegal instruction
l SIGINT CTRL+C signal
l SIGSEGV Illegal storage access
l SIGTERM
MSDN中说SIGILL, SIGSEGV,和SIGTERM are not generated under Windows NT并且与ANSI相兼容。但是,如果你在主线程中设置SIGSEGV signal handler,CRT将会调用它,而不是调用SetUnhandledExceptionFilter()函数设置的SHE exception handler,全局变量_pxcptinfoptrs中包含了指向异常信息的指针。
_pxcptinfoptrs也会被用于SIGFPE handler中,而在所有其他的signal handlers中,它将会被设为NULL。
当一个floating point 错误发生时,例如除零错,CRT将调用SIGFPE signal handler。然而,默认情况下,不会产生float point 异常,取而代之的是,将会产生一个NaN或无穷大的数作为这种浮点数运算的结果。可以使用_controlfp_s()函数使得编译器能够产生floating point异常。
使用raise()函数,你可以人工地产生所有的6中信号。例如:
#include <cstdlib>
#include <csignal>
#include <iostream>
void sigabrt_handler(int){
// Caught SIGABRT C++ signal
// Terminate program
std::cout << "handled.\n";
exit(1);
}
int main(){
signal(SIGABRT, sigabrt_handler);
// Cause abort
abort();
}
Note:
虽然MSDN中没有详细地说明,但是你应该为你程序中的每个线程都安装SIGFPE, SIGILL和SIGSEGV signal hanlders。SIGABRT, SIGINT和SIGTERM signal hanlders对程序中的每个线程都起作用,因此你只需要在你的main函数中安装他们一次就够了。
获取异常信息 Retrieving Exception Information
译者注:这一小节不太懂,以后有时间再翻译
When an exception occurs you typically want to get the CPU state to determine the place in your code that caused the problem. You use the information to debug the problem. The way you retrieve the exception information differs depending on the exception handler you use.
In the SEH exception handler set with the SetUnhandledExceptionFilter() function, the exception information is retrieved from EXCEPTION_POINTERS structure passed as function parameter.
In __try{}__catch(Expression){} construction you retrieve exception information using GetExceptionInformation() intrinsic function and pass it to the SEH exception filter function as parameter.
In the SIGFPE and SIGSEGV signal handlers you can retrieve the exception information from the _pxcptinfoptrs global CRT variable that is declared in <signal.h>. This variable is not documented well in MSDN.
In other signal handlers and in CRT error handlers you have no ability to easily extract the exception information. I found a workaround used in CRT code (see CRT 8.0 source files, invarg.c, line 104).
The following code shows how to get current CPU state used as exception information.
#if _MSC_VER>=1300
#include <rtcapi.h>
#endif
#ifndef _AddressOfReturnAddress
// Taken from: http://msdn.microsoft.com/en-us/library/s975zw7k(VS.71).aspx
#ifdef __cplusplus
#define EXTERNC extern "C"
#else
#define EXTERNC
#endif
// _ReturnAddress and _AddressOfReturnAddress should be prototyped before use
EXTERNC void * _AddressOfReturnAddress(void);
EXTERNC void * _ReturnAddress(void);
#endif
// The following function retrieves exception info
void GetExceptionPointers(DWORD dwExceptionCode,
EXCEPTION_POINTERS** ppExceptionPointers)
{
// The following code was taken from VC++ 8.0 CRT (invarg.c: line 104)
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextRecord;
memset(&ContextRecord, 0, sizeof(CONTEXT));
#ifdef _X86_
__asm {
mov dword ptr [ContextRecord.Eax], eax
mov dword ptr [ContextRecord.Ecx], ecx
mov dword ptr [ContextRecord.Edx], edx
mov dword ptr [ContextRecord.Ebx], ebx
mov dword ptr [ContextRecord.Esi], esi
mov dword ptr [ContextRecord.Edi], edi
mov word ptr [ContextRecord.SegSs], ss
mov word ptr [ContextRecord.SegCs], cs
mov word ptr [ContextRecord.SegDs], ds
mov word ptr [ContextRecord.SegEs], es
mov word ptr [ContextRecord.SegFs], fs
mov word ptr [ContextRecord.SegGs], gs
pushfd
pop [ContextRecord.EFlags]
}
ContextRecord.ContextFlags = CONTEXT_CONTROL;
#pragma warning(push)
#pragma warning(disable:4311)
ContextRecord.Eip = (ULONG)_ReturnAddress();
ContextRecord.Esp = (ULONG)_AddressOfReturnAddress();
#pragma warning(pop)
ContextRecord.Ebp = *((ULONG *)_AddressOfReturnAddress()-1);
#elif defined (_IA64_) || defined (_AMD64_)
/* Need to fill up the Context in IA64 and AMD64. */
RtlCaptureContext(&ContextRecord);
#else /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ContextRecord, sizeof(ContextRecord));
#endif /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ExceptionRecord, sizeof(EXCEPTION_RECORD));
ExceptionRecord.ExceptionCode = dwExceptionCode;
ExceptionRecord.ExceptionAddress = _ReturnAddress();
EXCEPTION_RECORD* pExceptionRecord = new EXCEPTION_RECORD;
memcpy(pExceptionRecord, &ExceptionRecord, sizeof(EXCEPTION_RECORD));
CONTEXT* pContextRecord = new CONTEXT;
memcpy(pContextRecord, &ContextRecord, sizeof(CONTEXT));
*ppExceptionPointers = new EXCEPTION_POINTERS;
(*ppExceptionPointers)->ExceptionRecord = pExceptionRecord;
(*ppExceptionPointers)->ContextRecord = pContextRecord;
}
Visual C++ Complier Flags
Visual C++编译器中有一些编译选项和异常处理有关。
在Project Properties->Configuration Properties->C/C++ ->Code Generation中可以找到这些选项。
异常处理模型Exception Handling Model
你可以为VC++编译器选择异常处理模型。选项/EHs(或者EHsc)用来指定同步异常处理模型,/EHa用来指定异步异常处理模型。可以查看下面参考小节的"/EH(Exception Handling Model)"以获取更多的信息。
Floating Point Exceptions
你可以使用/fp:except编译选项打开float point exceptions。
缓冲区安全检查Buffer Security Checks
你可以使用/GS(Buffer Security Check)选项来强制编译器插入代码以检查缓冲区溢出。缓冲区溢出指的是一大块数据被写入一块较小的缓冲区中。当检测到缓冲区溢出,CRT calls internal security handler that invokes Watson directly。
Note:
在VC++(CRT7.1)中,缓冲区溢出被检测到时,CRT会调用由_set_security_error_handler函数设置的处理器。然而,在之后的VC版本中这个函数被废弃。
从CRT8.0开始,你在你的代码中不能截获安全错误。当缓冲区溢出被检测到时,CRT会直接请求Watson,而不是调用unhandled exception filter。这样做是由于安全原因并且微软不打算改变这种行为。
更多的信息请参考如下链接
https://connect.microsoft.com/VisualStudio/feedback/details/101337/a-proposal-to-make-dr-watson-invocation-configurable
http://blog.kalmbachnet.de/?postid=75
异常处理和CRT链接Exception Handling and CRT Linkage
你的应用程序中的每个module(EXE, DLL)都需要链接CRT。你可以将CRT链接为多线程静态库(multi-threaded static library)或者多线程动态链接库(multi-threaded dynamic link library)。如果你设置了CRT error handlers,例如你设置了terminate handler, unexcepted handler, pure call handler, invalid parameter handler, new operator error handler or a signal handler,那么他们将只在你链接的CRT上运行,并且不会捕获其他CRT模块中的异常(如果存在的话),因为每个CRT模块都有它自己的内部状态。
多个工程中的module可以共享CRT DLL。这将使得被链接的CRT代码达到最小化,并且CRT DLL中的所有异常都会被立刻处理。这也是推荐使用multi-threaded CRT DLL作为CRT链接方式的原因。