| The Visual C++ Exception Model | |
|
The Visual C++ Exception Model
When programming in C++ with Microsoft Visual C++, your program has two exceptions models: the normal C++ exception model and the Structure Exception Handling (SEH) exception model.1) MSVC's C++ exception system is, in fact, implemented with SEH's functionality. This implementation leads to some interesting behaviors. This article discusses some of the consequences of the MSVC exception system from the standpoint of a C++ programmer. It assumes familiarity with C++ exceptions, including exception aware programming techniques like RAII, and basic Windows programming.2) It does not assume any background with SEH. The behavior described covers MSVC .NET 2002, 2003, 2005 and 2008. An Introduction to Structured Exception HandlingStructured Exception Handling is a mechanism built into the operating system that provides facilities to detect and deal with exceptional circumstances, much as C++ exceptions do for C++ programs. SEH exceptions come in two varieties: hardware exceptions and software exceptions. Hardware exceptions are exceptions raised by the CPU such as access violations and divide by zero. Software exceptions are exceptions raised explicitly by programs or, on occasion, the operating system itself. For the purposes of this article, there isn't much difference between the two. When an exception is raised, two pieces of information are captured: one is the state of the processor at the time of the exception, which is stored in a Windows exception codes follow the following format: Bits 31 and 30 represent the severity code. It should be 00 for success, 01 for informational, 10 for warning and 11 for an error. Bit 29 is set for user exception codes. Bit 28 is reserved and should not be set. Bits 27 to 16 represent a facility code. For example, Windows Update is assigned the facility code of 36, while 0 represents no particular facility. The bottom 16 bits then represent the actual error code. This is why exceptions like access violations start with 0xC: these codes represent non-user error exceptions. In theory, most user exception codes should start with 0xE to represent user defined error exceptions4). Actually dealing with SEH exceptions in MSVC takes two primary forms: frame based exception handling ( Frame Based Exception HandlingFrame based exception handling is roughly analogous to C++'s __try {
// code to attempt to execute
} __except (filter_expression) {
// handler code
}
The block after the __try is executed, and if an SEH exception is thrown the filter expression is evaluated. __try blocks have many of the restrictions, both de jure and de facto, that C++ try blocks have. For example, goto cannot be used to enter __try blocks and __try blocks inhibit inlining.
Unlike C++ catch block, where the catch contains types, the filter expression is more like the contents of the parenthesis after an if5): it can be almost anything that eventually evaluates to a value. In particular, the filter expression must evaluate to one of three values: Unlike C++ catch blocks, you can't chain __try {
// stuff
} __except (filter expression 1) {
// handler 1
} __except (filter expression 2) {
// handler 2
}
You can, instead, nest the blocks.
__try {
__try {
// stuff
} __except (filter expression 1) {
// handler 1
}
} __except (filter expression 2) {
// handler 2
}
Another, much bigger limitation of SEH blocks is that functions containing SEH blocks cannot contain C++ objects with unwind semantics. This translates into basically anything with a destructor is illegal to create as a auto or stack variable in a function with SEH blocks8). You also can't use C++ try/catch blocks. 9)
So what does a typical filter expression look like? If there was such a thing as a typical filter expression, it would probably look something like this: EXCEPTION_POINTERS * eps = 0;
__try {
// stuff
} __except (eps = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) {
// handler
}
GetExceptionInformation() is a macro that obtains pointers to the CONTEXT and EXCEPTION_RECORD structures mentioned earlier. It can only be called inside a filter expression; it can't be called in the __except block. So this filter expression assigns the exception pointers and then uses the comma operator to have the filter expression take on the value of EXCEPTION_EXECUTE_HANDLER. This rapidly gets unwieldy if the expression needs to have any additional logic rather unconditionally returning a single value. Fortunately, it is legal to call functions in the filter expression.10)
DWORD filter(EXCEPTION_POINTERS * eps) {
if (eps->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
return EXCEPTION_EXECUTE_HANDLER;
return EXCEPTION_CONTINUE_SEARCH;
}
EXCEPTION_POINTERS * eps = 0;
__try {
} __except (eps = GetExceptionInformation(), filter(eps)) {
}
Another macro used in filter expressions is GetExceptionCode(). Like GetExceptionInformation() it can only be called inside a filter expression. As you might guess from the name, it returns the exception code of the exception being filtered. With that you could write the above filter expression like so:
EXCEPTION_POINTERS * eps = 0;
__try {
} __except (eps = GetExceptionInformation(),
((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION)
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)) {
}
Termination HandlingTermination handling is much simpler than frame based exception handling. Instead of __try {
// try to execute this code
} __finally {
// execute this code afterwards (unless the thread or process ends)
}
Like __try/__except blocks, __try/__finally blocks can't be used in functions with objects with stack unwind semantics. You also cannot tag a __finally block on a __try/__except, though you can nest a __try/__finally block within a __try/__except (or vice versa).
__try {
__try {
// try to execute this code
} __finally {
// execute this code afterwards (unless the thread or process ends)
}
} __except (filter_expression) {
// exception handler
}
Termination handling brings one new concept to the table: abnormal termination. A __try block is said to end normally or abnormally. Normal termination means that program flow continued after the last statement in the __try block was executed and continued into the __finally block. Abnormal termination means that the control flow exited the block in any other way, including exceptions, but also include return, goto, continue or break statements; even if one of those control flow statements was the last statement in the __try block. Abnormal termination results in a performance penalty as the SEH code searches for all relevant __finally handlers. Abnormal termination can also be avoided with the __leave keyword, which immediately ends the __try block and starts the __finally block. Inside the __finally block, the AbnormalTermination() macro can be used to determine if the __try block was left normally or abnormally.
Implementing C++ ExceptionsThis section presents a very high level overview of how C++ exceptions are implemented. 11) Let's start with what happens when a C++ exception is thrown: first a copy of the exception is placed on the stack, then the program calls to function named Given that The throw information structure used is one of the interesting types that seem to be built into the compiler itself. Even if you include no headers, the compiler knows what the definition for the typedef void (__cdecl * _PMFN)(void); // despite the static typing, function pointers of // type _PMFN are usually used as pointers to other // function types struct _PMD { int mdisp; // member offset int pdisp; // offset of the vtable int vdisp; // offset to displacment inside the vtable }; struct _s__CatchableType { unsigned int properties; // bit 1: is a simple type // bit 2: can be caught by reference only // bit 3: has virtual base _TypeDescriptor * pType // pointer to std::type_info object. _PMD thisDisplacement; // displacement of the object for this type int sizeOrOffset; // size of the type _PMFN copyFunction; // pointer to copy constructor or 0 }; typedef const _s__CatchableType _CatchableType; struct _s__CatchableTypeArray { int nCatchableTypes; // number of entries in arrayOfCatchableTypes _CatchableType * arrayOfCatchableTypes[]; // array of pointers to all base types of the object // including types that it can be implicitly cast to }; typedef const _s__CatchableTypeArray _CatchableTypeArray; struct _s__ThrowInfo { unsigned int attributes; // bit 1 for const, bit 2 for volatile _PMFN pmfnUnwind; // function to destroy the exception object int (__cdecl * pForwardCompat)(...); _CatchableTypeArray * pCatchableTypeArray; // pointer to array of pointers to type information }; typedef const _s__ThrowInfo _ThrowInfo;So when the compiler runs into a catch (std::exception & e) it actually generates a SEH handler that checks the exception code for 0xE06D7363 then interprets the third exception information argument as a _s__ThrowInfo pointer, and checks the catchable types array for an entry for std::exception. If it finds it, it applies the address translation found in the _PMD record to the second exception information argument and transfers control to the handler block. If not, the exception handler continues the search for additional handlers until it hits one.13)
That brings us to the wonderful mess that is Well, sort of. Take this sample program: #include <iostream> #include <windows.h> int main(int, char **) { try { RaiseException(0, 0, 0, 0); } catch (...) { std::cout << "Moo." << std::endl; } return 0; }In debug mode for MSVC 2003, this will print "Moo." In release mode, with /EHsc (the default enable exception handling flag), the compiler sees that RaiseException(), as an extern "C" function, can't raise a C++ exception. Since catch (...) is only supposed to catch C++ exceptions, the compiler then removes the catch handler as dead code. This can lead to wonderfully bizarre and inconsistent behavior between debug and release builds14). Using /EHs or /EHa without /EHc will prevent the compiler from removing the catch block.
So what is the difference between /EHs and /EHa? MSVC supports two exception models: the synchronous exception model and the asynchronous exception model. The asynchronous model generates code with the assumption that any expression can throw an exception. The synchronous model assumes that exceptions can only be thrown by A case can be made either way for try { A the_object; f(); } catch (...) { // clean up stuff throw; }Of course, this kind of inconsistency is less important if no SEH exceptions are caught by your program and the program ends when an exception is raised. Unifying Exceptions in MSVC Assuming you want to make sure that You can also take an additional step: installing an exception translator, a function that translates SEH exceptions into C++ exceptions. One of the simplest examples of this would be: struct SEHException { SEHException(const EXCEPTION_RECORD & record) : record(record) {} EXCEPTION_RECORD record; }; void translator_function(unsigned int, EXCEPTION_POINTERS * eps) { throw SEHException(*eps->ExceptionRecord); } int main(int, char **) { try { _set_se_translator(&translator_function); // stuff } catch (SEHException & e) { // handle the exception } return 0; }This takes any SEH exception, no matter what it is and just makes a copy of the EXCEPTION_RECORD for the exception. This provides almost all the same information available to a __try/__except block to a C++ catch block. A translation function shouldn't be too complicated: if the translation function raises a SEH exception, terminate() will be called, just like if a C++ exception is thrown in a destructor during stack unwind. _set_se_translator() works on a per-thread basis, so if your program is multi-threaded, it needs to be called for each thread. This still requires that the /EHa flag is used when compiling your application.
If you don't want to install an exception translator or don't want to use /EHa, but want the equivalent of a Sometimes you want the opposite effect: being able to handle C++ exceptions inside an Within an SEH handler, to get at a C++ object for an exception requires duplicating some of the work that the compiler does automatically in C++ catch blocks. You need to check the exception code, and then use the Bringing it all TogetherBecause SEH handler blocks can't be used in functions with C++ objects that have unwind semantics, normal RAII techniques don't work without a little finesse. For example, let's say I want two C++ objects in my main() function. One is an RAII object to initialize and cleanup the symbol handler for the stack walk and the for the other I want my exception handler's filter expression to write to a buffer that the will be displayed and saved in the int main(int argc, char ** argv) { SymInit sym; std::string buffer; return seh_helper(argc, argv, buffer); } int seh_helper(int argc, char ** argv, std::string & buffer) { __try { return actual_main(argc, argv); } __except (filter(GetExceptionInformation(), buffer)) { if (!buffer.empty()) { save_buffer(buffer); MessageBoxA(0, buffer.c_str(), "Abnormal Termination", MB_OK); } return -1; } }Here, since the symbol handler doesn't need to do anything but be constructed and later destroyed, nothing special needs to happen to it. The string buffer, on the other hand, gets passed as a reference to the helper function, which in turn passes it to the filter() function which will fill the buffer. Also, the exception handler can get a pointer to the buffer's data with the c_str() member function. However, if instead of a std::string buffer had been defined as a std::stringstream, the similar call buffer.str().c_str() would not be valid for use in the seh_helper() function, since std::stringstream::str() returns a std::string by value.
Similarly, if we try to perform a stack walk inside a filter expression, things could go wrong because of the reason of the exception. An exception can result because of heap corruption, stack corruption or any number of other states that would make performing the stack walk impossible. So to address this, you can break the filter function in two parts: DWORD filter(EXCEPTION_POINTERS * eps, std::string & buffer) {
__try {
return do_filter(eps, buffer);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return EXCEPTION_CONTINUE_SEARCH;
}
}
This catches any exceptions raised by the actual filter function and returns EXCEPTION_CONTINUE_SEARCH to cause the next exception handler to attempt to handle the original exception. In the case of this sample application, this should be the operating system's handler, which will give applications like Dr. Watson a chance to get a crash dump.18)
For converting the SEH exception information to C++ exception information, I use this implementation: struct UntypedException { UntypedException(const EXCEPTION_RECORD & er) : exception_object(reinterpret_cast<void *>(er.ExceptionInformation[1])), type_array(reinterpret_cast<_ThrowInfo *>(er.ExceptionInformation[2])->pCatchableTypeArray) {} void * exception_object; _CatchableTypeArray * type_array; }; void * exception_cast_worker(const UntypedException & e, const type_info & ti) { for (int i = 0; i < e.type_array->nCatchableTypes; ++i) { _CatchableType & type_i = *e.type_array->arrayOfCatchableTypes[i]; const std::type_info & ti_i = *reinterpret_cast<std::type_info *>(type_i.pType); if (ti_i == ti) { char * base_address = reinterpret_cast<char *>(e.exception_object); base_address += type_i.thisDisplacement.mdisp; return base_address; } } return 0; } template <typename T> T * exception_cast(const UntypedException & e) { const std::type_info & ti = typeid(T); return reinterpret_cast<T *>(exception_cast_worker(e, ti)); } UntypedException simply performs some casts and stores the results, while exception_cast_worker() performs the check for type_info equality and the exception_cast() template supplies the correct type_info. The code that actually uses these building blocks looks like:
DWORD do_filter(EXCEPTION_POINTERS * eps, std::string & buffer) {
std::stringstream sstr;
const EXCEPTION_RECORD & er = *eps->ExceptionRecord;
int skip = 0;
switch (er.ExceptionCode) {
case 0xE06D7363: { // C++ exception
UntypedException ue(er);
if (std::exception * e = exception_cast<std::exception>(ue)) {
const std::type_info & ti = typeid(*e);
sstr << ti.name() << ":" << e->what();
} else {
sstr << "Unknown C++ exception thrown.";
}
skip = 2; // skip RaiseException and _CxxThrowException
} break;
case EXCEPTION_ACCESS_VIOLATION: {
sstr << "Access violation. Illegal "
<< (er.ExceptionInformation[0] ? "write" : "read")
<< " by "
<< er.ExceptionAddress
<< " at "
<< reinterpret_cast<void *>(er.ExceptionInformation[1]);
} break;
default: {
sstr << "SEH exception thrown. Exception code: "
<< std::hex << std::uppercase << er.ExceptionCode
<< " at "
<< er.ExceptionAddress;
}
}
sstr << "\n\nStack Trace:\n";
generate_stack_trace(sstr, *eps->ContextRecord, skip);
buffer = sstr.str();
return EXCEPTION_EXECUTE_HANDLER;
}
Here the intent is that the C++ exception handling block can be extended in the same way that adding catch blocks can be done. For example, you could extend it like so:
if (std::exception * e = exception_cast<std::exception>(ue)) { sstr << e->what(); } else if (exception_cast<boost::python::error_already_set>(ue)) { PyErr_Print(); // magically extract the contents of sys.stderr and put it in the stream } else { sstr << "Unknown C++ exception thrown."; }This results in the same caveat in the if chain as for catch blocks in C++ exception handlers: put more derived classes earlier in the chain than base classes or the base classes will consume the exception.19) Of course, since we have access to the _ThrowInfo pointer in the exception handler, we can also extract some type information from an unknown exception type.
void get_exception_types(std::ostream & os, const UntypedException & e) { for (int i = 0; i < e.type_array->nCatchableTypes; ++i) { _CatchableType & type_i = *e.type_array->arrayOfCatchableTypes[i]; const std::type_info & ti_i = *reinterpret_cast<std::type_info *>(type_i.pType); os << ti_i.name() << "\n"; } }This simply inserts the name of all the types in the catchable type array into a stream. It seems that the first entry is the actual type of the exception and the rest of the entries are base classes or types with implicit conversions.20) For example, throwing a string literal might produce "char *" and "void *" as output.
Exception Handling OverheadThe word "overhead" in the context of exception handling is something of a loaded term, but no discussion of an specific implementation of exception handling is really complete without at least a minimal discussion of the performance consequences. First off, when exception handling is enabled, every function that needs unwind semantics needs to execute additional prologue and epilogue code, as well as additional space on the stack to hold exception handling information. This depends on the function in question, the optimization settings, processor targeted and MSVC version, but works out to be about five instructions for the prologue and about three instructions for the epilogue in the common case and the additional space on the stack works out to about five pointer widths. The prologue and epilogue code may also be invoked via a function call rather than inlined; this will increase the cost of executing the prologue and epilogue code to gain some reductions in overall executable size. Additionally, quite a bit of code and data is added to the executable in order to actually process the exception and perform stack unwind. Fortunately, these sources of overhead are significantly reduced in the presence of aggressive inlining, especially link time cross-module inlining. On the other hand, while MSVC is very good about inlining code, it's not so good at separating out redundant code from multiple functions. As mentioned earlier, when throwing a C++ exception, the compiler generates code to place a copy of the exception on the stack, then calls _CxxThrowException() with a pointer to the exception and a pointer to the _ThrowInfo for the exception type. On 32-bit x86, ; 6 : throw std::exception();
lea ecx, DWORD PTR $T5541[esp+12]
call DWORD PTR __imp_??0exception@@QAE@XZ
push OFFSET FLAT:__TI1?AVexception@@
lea eax, DWORD PTR $T5541[esp+16]
push eax
call __CxxThrowException@8
This code should rarely be called, since exceptions should rarely occur, so on the face of it the number of instructions is not a large issue; however, because of the aggressive inlining required to reduce the stack frame overhead, the throw code can suddenly explode in a code base, especially if it occurs in template code. Therefore, if you have multiple identical throw statements (or a single throw in a template with multiple instantiations), you should strongly consider factoring them into a single function 21).
The exception model is implemented in such a way that translation units compiled with /EHs, /EHa or without exception handling enabled at all can coexist in an application. This means that another way to fight exception handling overhead is if a profiler shows a function to be a bottleneck, it can be separated into its own source file and compiled with a different exception handling switch to eliminate prologue and epilogue generation. Finally there's the cost incurred of throwing and catching an exception versus returning an error code and checking and propagating the value. The relative cost of these situations are highly dependent on a number of situation specific modifiers such as how far nested the point of failure is compared to the location of the error handler, the number and type of objects that need to be unwound, optimization settings and so on. However, even the most optimistic estimates place throwing an exception as hundreds of times more expensive than returning an error code. The cost is probably closer to several thousands of times, if not more. Of course, this shouldn't be news to anyone experienced with C++ exceptions; exceptions should only be used for exceptional circumstances. This does, however, supply a metric for exception use: if the error condition is expected to occur more often than once out of every 10,000 times the function is run, then use an error code; otherwise use an exception. Conclusion and ReferencesThis article only touches the surface of what can be done with customizing exception handling in MSVC, though it hopefully does cover the important consequences of the exception model if you don't intend on modifying the behavior. The following references include more detail on how SEH is implemented, how C++ exceptions are implemented with SEH and additional things you can do with exception handling with MSVC including instrumenting the
The Complete Sample Application The following is the complete code for running the application developed in the article. It suffers from many of the flaws of other article sample code examples: it exists to demonstrate techniques and points mentioned in the article and so does some things unnecessarily. For example, the //Copyright (c) 2007 Howard Jeng // //Permission is hereby granted, free of charge, to any person obtaining a copy //of this software and associated documentation files (the "Software"), to deal //in the Software without restriction, including without limitation the rights //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell //copies of the Software, and to permit persons to whom the Software is //furnished to do so, subject to the following condition: // //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN //THE SOFTWARE. #include <iostream> #include <sstream> #include <fstream> #include <windows.h> #include <dbghelp.h> class SymInit { public: SymInit(void) : process_(GetCurrentProcess()) { SymInitialize(process_, 0, TRUE); DWORD options = SymGetOptions(); options |= SYMOPT_LOAD_LINES; SymSetOptions(options); } ~SymInit() { SymCleanup(process_); } private: HANDLE process_; SymInit(const SymInit &); SymInit & operator=(const SymInit &); }; std::string get_module_path(HMODULE module = 0) { char path_name[MAX_PATH] = {}; DWORD size = GetModuleFileNameA(module, path_name, MAX_PATH); return std::string(path_name, size); } void write_module_name(std::ostream & os, HANDLE process, DWORD64 program_counter) { DWORD64 module_base = SymGetModuleBase64(process, program_counter); if (module_base) { std::string module_name = get_module_path(reinterpret_cast<HMODULE>(module_base)); if (!module_name.empty()) os << module_name << "|"; else os << "Unknown module|"; } else { os << "Unknown module|"; } } void write_function_name(std::ostream & os, HANDLE process, DWORD64 program_counter) { SYMBOL_INFO_PACKAGE sym = { sizeof(sym) }; sym.si.MaxNameLen = MAX_SYM_NAME; if (SymFromAddr(process, program_counter, 0, &sym.si)) { os << sym.si.Name << "()"; } else { os << "Unknown function"; } } void write_file_and_line(std::ostream & os, HANDLE process, DWORD64 program_counter) { IMAGEHLP_LINE64 ih_line = { sizeof(IMAGEHLP_LINE64) }; DWORD dummy = 0; if (SymGetLineFromAddr64(process, program_counter, &dummy, &ih_line)) { os << "|" << ih_line.FileName << ":" << ih_line.LineNumber; } } void generate_stack_trace(std::ostream & os, CONTEXT ctx, int skip) { STACKFRAME64 sf = {}; sf.AddrPC.Offset = ctx.Eip; sf.AddrPC.Mode = AddrModeFlat; sf.AddrStack.Offset = ctx.Esp; sf.AddrStack.Mode = AddrModeFlat; sf.AddrFrame.Offset = ctx.Ebp; sf.AddrFrame.Mode = AddrModeFlat; HANDLE process = GetCurrentProcess(); HANDLE thread = GetCurrentThread(); os << std::uppercase; for (;;) { SetLastError(0); BOOL stack_walk_ok = StackWalk64(IMAGE_FILE_MACHINE_I386, process, thread, &sf, &ctx, 0, &SymFunctionTableAccess64, &SymGetModuleBase64, 0); if (!stack_walk_ok || !sf.AddrFrame.Offset) return; if (skip) { --skip; } else { // write the address os << std::hex << reinterpret_cast<void *>(sf.AddrPC.Offset) << "|" << std::dec; write_module_name(os, process, sf.AddrPC.Offset); write_function_name(os, process, sf.AddrPC.Offset); write_file_and_line(os, process, sf.AddrPC.Offset); os << "\n"; } } } struct UntypedException { UntypedException(const EXCEPTION_RECORD & er) : exception_object(reinterpret_cast<void *>(er.ExceptionInformation[1])), type_array(reinterpret_cast<_ThrowInfo *>(er.ExceptionInformation[2])->pCatchableTypeArray) {} void * exception_object; _CatchableTypeArray * type_array; }; void * exception_cast_worker(const UntypedException & e, const type_info & ti) { for (int i = 0; i < e.type_array->nCatchableTypes; ++i) { _CatchableType & type_i = *e.type_array->arrayOfCatchableTypes[i]; const std::type_info & ti_i = *reinterpret_cast<std::type_info *>(type_i.pType); if (ti_i == ti) { char * base_address = reinterpret_cast<char *>(e.exception_object); base_address += type_i.thisDisplacement.mdisp; return base_address; } } return 0; } void get_exception_types(std::ostream & os, const UntypedException & e) { for (int i = 0; i < e.type_array->nCatchableTypes; ++i) { _CatchableType & type_i = *e.type_array->arrayOfCatchableTypes[i]; const std::type_info & ti_i = *reinterpret_cast<std::type_info *>(type_i.pType); os << ti_i.name() << "\n"; } } template <typename T> T * exception_cast(const UntypedException & e) { const std::type_info & ti = typeid(T); return reinterpret_cast<T *>(exception_cast_worker(e, ti)); } DWORD do_filter(EXCEPTION_POINTERS * eps, std::string & buffer) { std::stringstream sstr; const EXCEPTION_RECORD & er = *eps->ExceptionRecord; int skip = 0; switch (er.ExceptionCode) { case 0xE06D7363: { // C++ exception UntypedException ue(er); if (std::exception * e = exception_cast<std::exception>(ue)) { const std::type_info & ti = typeid(*e); sstr << ti.name() << ":" << e->what(); } else { sstr << "Unknown C++ exception thrown.\n"; get_exception_types(sstr, ue); } skip = 2; // skip RaiseException and _CxxThrowException } break; case EXCEPTION_ACCESS_VIOLATION: { sstr << "Access violation. Illegal " << (er.ExceptionInformation[0] ? "write" : "read") << " by " << er.ExceptionAddress << " at " << reinterpret_cast<void *>(er.ExceptionInformation[1]); } break; default: { sstr << "SEH exception thrown. Exception code: " << std::hex << std::uppercase << er.ExceptionCode << " at " << er.ExceptionAddress; } } sstr << "\n\nStack Trace:\n"; generate_stack_trace(sstr, *eps->ContextRecord, skip); buffer = sstr.str(); return EXCEPTION_EXECUTE_HANDLER; } DWORD filter(EXCEPTION_POINTERS * eps, std::string & buffer) { __try { return do_filter(eps, buffer); } __except (EXCEPTION_EXECUTE_HANDLER) { return EXCEPTION_CONTINUE_SEARCH; } } int actual_main(int, char **) { // do stuff // cause an access violation //char * ptr = 0; *ptr = 0; // divide by zero //int x = 5; x = x / (x - x); // C++ exception //throw std::runtime_error("I'm an exception!"); throw 5; return 0; } void save_buffer(const std::string & buffer) { std::ofstream ofs("err_log.txt"); if (ofs) ofs << buffer; } int seh_helper(int argc, char ** argv, std::string & buffer) { __try { return actual_main(argc, argv); } __except (filter(GetExceptionInformation(), buffer)) { if (!buffer.empty()) { save_buffer(buffer); MessageBoxA(0, buffer.c_str(), "Abnormal Termination", MB_OK); } return -1; } } int main(int argc, char ** argv) { SymInit sym; std::string buffer; return seh_helper(argc, argv, buffer); } 1) You can disable the C++ exception model by changing your project properties and avoiding exception code, but you're stuck with SEH no matter what you do. Even if you never put a single __try block in your code, the operating system wraps your program in a SEH handler that produces the standard dialogs for access violations and the like.2) Such as recognizing what an access violation is, the basic typedefs and the difference between the ANSI and Unicode versions of systems calls. 3) This is highly processor dependent information such as the contents of registers and the state of the floating point unit. 4) In practice, user defined exception codes range across the board, since the RaiseException() documentation only mentions the effect of bit 28 and the rest of the Platform SDK documentation on SEH doesn't mention the bit information for exception codes. These guidelines are actually mentioned in winerror.h and the Visual C++ language reference for SEH and not many other places.5) With some additional restrictions such as not being able to define a new variable in the filter expression. 6) If there is no enclosing __except block in the executable code, the exception handler installed by the operating system triggers, which generally displays an unhandled exception dialog and then ends the program.7) Unless the exception was raised with the EXCEPTION_NONCONTINUABLE flag, in which case if the filter expression returns EXCEPTION_CONTINUE_EXECUTION this causes an EXCEPTION_NONCONTINUABLE_EXCEPTION exception.8) However, pointers and references to such objects can be used. 9) It may help to think of functions with SEH blocks to be C only, though this isn't strictly true. 10) This is not necessarily a good thing in all instances. Since filter expressions are evaluated in the context of the exception, calling a function during a stack overflow could cause additional problems. 11) Much more detailed explanations are available, see the references section. 12) Provided comments come from examining disassembly, so may not be entirely accurate, though the names and types should be accurate, modulo typedefs. 13) This description ignores a lot of the other work it needs to do, such as checking the magic number and initiating stack unwind. 14) Not that I'm bitter. 15) Since this isn't an option given in project properties for MSVC 2003 and earlier, you'll need to specify it as an additional command line option. 16) It's also important to remember to remove the throw; re-throw statement from the finally block when performing this kind of translation. Also, all this work has a secondary benefit: the debugger will no longer bug you about re-thrown exceptions.17) This is far less information than a crash dump like that produced by Dr. Watson would include, so it isn't recommended for actual use in a complex program without heavy modification. 18) In some cases the process of executing the filter expression may make the crash dump less useful, so for this kind of exception handler, it's a good idea to include an option to disable the filter from executing at all. This can be via a command line argument or configuration file option. In general, it should be simple for the end user to toggle, which reduces the viability of mechanisms like environment variables or registry entries. 19) This is not the most efficient implementation for this task, especially if multiple exception types need to be differentiated. Of course, if program execution has reached this point then efficiency is not so much a prime concern. 20) Though, obviously, this is firmly in the realm of undocumented behavior land, and I wouldn't rely on that ordering. 21) Preferably one __declspec'd noinline and noreturn. Of course, for template code, factoring common code into non-template helpers is a good idea for things other than throwing exceptions.Discuss this article in the forums
See Also: © 1999-2008 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|