Visual C++ exception handling

Thursday, 15 August 2002

This article describes a problem with the default handling of exceptions in Microsoft’s Visual C++ compilers. The problem is caused by the compiler’s extension to the C++ exception handling mechanism. I then give a technique that properly exploits this extension, bringing it into line with normal C++ exception handling. This allows programs to deal with exceptions that are normally very difficult to handle, such as memory access violations.

This article was originally based on Visual C++ 6, and the same issues exist in the Visual Studio .NET C++ compiler. Finally, after at least six years, Microsoft fixed the fundamental problem in Visual C++ 2005. But to fully take advantage of this, it’s still necessary to implement an exception translation scheme like the one described in this article.

Win32 hardware exceptions

Under Visual C++, certain exceptional runtime conditions can be treated something like C++ exceptions. These exceptions are raised by the OS for events like memory access violations, division by zero, and so on. A (presumably) full list is in Microsoft’s MSDN Library under “EXCEPTION_RECORD”.

They are referred to in the documentation as “hardware exceptions”, “C exceptions”, “structured exceptions” and “C structured exceptions”. Actually, they are neither C-specific nor structured (at least compared to C++ exceptions). I refer to them as “Win32 hardware exceptions” or just “Win32 exceptions” because they are specific to the Win32 operating systems and hardware on which they run.

Visual C++ programs may be unstable by default

Visual C++ is a good compiler. But, like all compilers, it has some bad features. The default handling of Win32 exceptions in pre-2005 versions is one of them.

In general, when a computer program program causes (say) a memory access violation, the program could either:

  1. continue in a consistent state;
  2. crash immediately; or
  3. continue in an inconsistent state, possibly misbehaving or even crashing later.

The first option is clearly the best. If this is is not possible, the second option is acceptable (and usually easy to achieve), since it maximises chances of finding the bug. The second option is taken by Visual C++ 2005.

Unfortunately, the default behaviour in a pre-2005 Visual C++ program is the third option. This behaviour makes it very difficult to find the bug that lead to the problem in the first place; the program may make it through testing and into production before the bug comes to light. A good, old-fashioned crash is much more obvious, and therefore easier to find and debug.

Now, sometimes the third option may be acceptable. For example, if a program is required to use a third-party library, and the library has bugs of this kind, and the developers have no access to the library source code, then the first option may be ruled out, and it may be considered better for the program to limp along rather than die. In most cases though, we want to handle such errors gracefully and find the bug that causes them.

Default handling of Win32 exceptions may be dangerous

According to the default behaviour in pre-2005 Visual C++, when a Win32 exception occurs (possibly nested) in a try{} block that has a corresponding catch(...) block, the call stack is unwound and the catch block will be entered. (This in itself is strange behaviour: catch is meant to catch C++ exceptions only, so why anyone would expect it to catch an access violation or division by zero is beyond me.)

However, the stack may not be unwound properly as it would if it were a normal C++ exception being caught; in particular, destructors of some stack-based objects may not be called. Furthermore, no information will be available about the Win32 exception. (Of course, this is always a problem with catch(...) blocks.)

As a result, the program will now be in an unstable state, and there is no way to determine where the problem occurred that triggered the Win32 exception.

This problem does not occur in Visual C++ 2005, where catch(...) never catches Win32 exceptions. This makes a big difference to program stability and consistency, but there is still a lot of room for improvement, as discussed below.

This is a breaking change in Visual C++ 2005. In pre-2005 Visual C++, catch(...) can catch Win32 exceptions. In Visual C++ 2005, it never does. This change, which could break existing code, was introduced very quietly. Perhaps it’s an unintended (but welcome) side effect of the integration with .NET and Managed C++. We know it’s a breaking change because it breaks Microsoft’s own example code on their Exception Handling Differences page. We know it was introduced quietly because it is not listed on their Breaking Changes in the Visual C++ 2005 Compiler page.

Synchronous exception handling and catch(...)

The source of this instability lies in the interaction of two aspects of the behaviour of default programs: the synchronous exception-handling model, and the use of catch(...).

Synchronous exception handling is the default. The default /GX option (/EHsc in Visual C++ 2005) causes Visual C++ programs to be compiled with the “synchronous” exception-handling model, where the compiler assumes that exceptions can only be thrown with the throw statement (which is true in standard C++). This allows certain optimisations: for example, if the compiler can determine that nothing will be thrown inside a given try block, then it can avoid generating all the code to unwind the stack (e.g. call appropriate destructors) for the catch.

catch(...) always catches Win32 exceptions. In pre-2005 Visual C++, a catch(...) block will by default be entered whenever a Win32 exception (e.g. divide by zero or memory access violation) occurs in its try block. This is not much use, because by the time the catch block is entered, no information about the error is available.

Therefore, for any pre-2005 Visual C++ program that contains catch(...) and that was compiled with default settings, any line that (for example) dereferences a pointer can throw a catchable exception; but the compiler will optimise based on the assumption that it can’t.

The second catch block in Listing 1 illustrates this.

Listing 1: Demonstrating the problem

 1  class whatever
 2  {
 3  public:
 4      whatever(int i) : id(i) { std::cerr << id << "-constructor "; }
 5      ~whatever(void) { std::cerr << id << "-destructor " ; }
 6  private:
 7      int id;
 8  };
10  int reciprocal(int i) {
11      return 1/i;
12  }
14  int main() {
15      int k = 0;
16      std::cerr << "first-try ";
17      try {
18          whatever w1(1);
19          int r = reciprocal(0);
20      }
21      catch (...) {
22          std::cerr << "caught ";
23      }
24      std::cerr << "second-try ";
25      try {
26          whatever w2(2);
27          int r = 1/k;
28      }
29      catch (...) {
30          std::cerr << "caught ";
31      }
32      std::cerr << "exiting ";
33  }

Under Visual C++ 2005, this program behaves as you’d expect a program to behave on any platform: it prints “first-try” and then crashes.

Under pre-2005 Visual C++, line 19 calls a function that throws a Win32 exception. As expected, w1‘s destructor is called, and then the exception is caught at line 22. So far, so good.

But at line 27 we have a direct divide by zero. This throws a Win32 exception as before, and the exception is caught at line 30, but w2‘s destructor never gets called. This happens because the compiler has incorrectly assumed that the try block starting at line 25 will not throw any exceptions.

The previous try block behaves differently. Since it contains a function call, the optimiser has assumed that it may throw, so has not optimised away the destructor call. But note that if this program is compiled with the /O1 option to minimise code size, then neither destructor gets called.

Note that this is arguably not a bug; that’s how the compiler is meant to behave when synchronous exception handling is enabled. As the Visual C++ documentation states:

Catching hardware exceptions is still possible with the synchronous model. However, some of the unwindable objects in the function where the exception occurs may not get unwound, if the compiler judges their lifetime tracking mechanics to be unnecessary for the synchronous model.

In other words, catching Win32 exceptions (and asynchronous exception handling in general) under the synchronous exception handling model is at the compiler’s discretion. Unsurprisingly, the compiler’s discretion depends on the optimisation level.

How optimisation affects exception handling

The following near-identical programs illustrate how the optimisation level in Visual C++ can have a profound effect on the behaviour of a program when a Win32 exception occurs.

Listing 2

 1  int main() {
 2     try {
 4        char* bad = 0;
 5        *bad = 0; // Generates EXCEPTION_ACCESS_VIOLATION
 6     }
 7     catch(...) {
 8        std::cerr << "catch";
 9     }
10  }

Listing 3

 1  int main() {
 2     try {
 3        std::cerr << "try ";
 4        char* bad = 0;
 5        *bad = 0; // Generates EXCEPTION_ACCESS_VIOLATION
 6     }
 7     catch(...) {
 8        std::cerr << "catch";
 9     }
10  }

Listing 2 and Listing 3 are identical except that Listing 2 prints some text at line 3. According to the C++ standard, both programs produce undefined behaviour. However, in general you could reasonably expect Listing 2 just to crash, and Listing 3 to print “try” and then crash. Using Visual C++ with default options, you may instead expect Listing 2 to print “catch“, and Listing 3 to print “try catch“, as the Win32 exception is caught.

Actually, under pre-2005 Visual C++ with default options, the programs behave inconsistently.

Listing 2 crashes, because the catch(...) has been ignored by the optimising compiler. (This depends on the compiler switches.)

Listing 3, however, prints “try catch” as the catch(...) catches the access violation Win32 exception. (In fact, this occurs regardless of which compiler switches are used.)

Visual C++ 2005 behaves consistently, since the catch(...) has no effect. Both programs simply crash.

Pages: 1 2 3



You can leave a comment, or trackback from your own site.

Leave a comment