|
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.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. |