收录查询

Visual C++ exception handling

原文地址:http://www.thunderguy.com/semicolon/2002/08/15/visual-c-exception-handling/1/

This article describes a problem with the default handling of exceptions in Microsoft’s Visual C++ compiler. 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.

In December 1999, I posted several articles to Microsoft C++ newsgroups about this topic. This article is based largely on these postings. The original postings were based on the then-current Visual C++ 6, but the same issues exist in the latest version (this is version 7, included with Visual Studio .NET).

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 are unstable by default

Visual C++ is a good compiler. But, like all compilers, it has some bad features. Its default handling of Win32 exceptions 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.

Unfortunately, the default behaviour in a 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 is dangerous

According to the default behaviour, 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.

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 /GX option 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 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 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  };
9
10  int reciprocal(int i) {
11      return 1/i;
12  }
13
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  }

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 {
3
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 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.)

Solution overview

So the basic problem is that, by default, Win32 exceptions can sometimes be caught with catch(...) and will sometimes fail to unwind the stack properly. Here I describe three solutions to this, each requiring different actions on the part of the programmer. They are:

1. Don’t catch Win32 exceptions. If you never catch Win32 exceptions, they will always cause the program to crash. This is perhaps the most obvious result, and is how most computer programs work. When a Win32 exception occurs, you will be able to use your normal debugging tools to isolate and correct the error that led to the exception.

2. Ensure the stack is unwound correctly when catching Win32 exceptions. This approach ensures that Win32 exceptions are always caught by catch(...) and will unwind the stack properly when caught. The disadvantage here is that no information about the exception is available at the point where it is caught.

3. Turn Win32 exceptions into C++ exceptions. This approach fully integrates Win32 exceptions into the normal C++ exception handling mechanism. When a Win32 exception occurs, it is automatically turned into a rich C++ exception object and handled according to the normal C++ exception rules. This is perhaps the most elegant approach, but it requires the most work on the part of the programmer.

Solution 1: Don’t catch Win32 exceptions

If you never catch Win32 exceptions, they will always cause the program to crash. This is perhaps the most obvious result, and is how most computer programs work. To get this behaviour, you must avoid using catch(...) anywhere.

The problem with this is, of course, that catch(...) can be quite useful. Even when the developer knows which types of exception can be thrown, and therefore writes:

catch (exceptionBase&) {
// clean up
throw;
}

It may better express the programmer’s intention to write this instead:

catch (...) {
// clean up
throw;
}

In other words, “if any exception occurs, clean up and rethrow it”. It’s a shame to have to avoid this.

Solution 2: Ensure the stack is unwound correctly

It is possible to ensure that Win32 exceptions are always caught by catch(...) and will unwind the stack properly. However, no information about the exception will be available. This happens if you compile with the “asynchronous” exception-handling model.

In the asynchronous exception-handling model, the compiler assumes that any code can cause an exception. This is actually (more-or-less) true of any Visual C++ code that has a catch(...) handler, since catch(...) always catches Win32 exceptions.

To enable the asynchronous exception-handling model, compile with the /EHa compiler switch.

Listing 4: Unwinding the stack

 1  class thing
2  {
3  public:
4      thing() { std::cerr < < "hello "  ; }
5      ~thing() { std::cerr << "goodbye "; }
6  };
7
8  int main() {
9      try {
10          std::cerr << "try ";
11          thing t1;
12          char* cp = 0;
13          *cp = 'a';
14      }
15      catch (...) {
16          std::cerr << "catch";
17      }
18  }

Compiled with the default /GX switch, Listing 4 outputs “try hello catch“. It fails to unwind the stack correctly; the t1 object destructor does not get called. If you compile with /EHa, the stack will unwind properly and you get “try hello goodbye catch” as expected.

Therefore, if a Visual C++ program contains catch(...), then it should be compiled with asynchronous exception handling enabled (/EHa). Otherwise, if the catch(...) catches a Win32 exception, “some of the unwindable objects in the function where the exception occurs may not get unwound” (Visual C++ Programmer’s Guide), which could lead to resource leaks or worse.

Note that this approach requires the compiler to insert complete cleanup code in every try block, even if it could normally determine that no C++ exception can be thrown from within it. This may increase overall code size.

Solution 3: Turn Win32 exceptions into C++ exceptions

Perhaps the best solution, especially in a large program, is to transform Win32 exceptions into C++ objects that can be handled according to the normal C++ exception rules. To get this behaviour, you must create your own class and write an exception handler to “catch” Win32 exceptions and throw C++ exceptions. You must also compile with the /EHa compiler option to enable asynchronous exception handling. At runtime, you install your handler by calling the global function _set_se_translator().

Background information on _set_se_translator() is available in Microsoft’s MSDN Library. There’s also a bare-bones example of this approach to exception handling.

The nice thing about this scheme is that the C++ exception object created can contain quite thorough information. For example, it could contain the kind of exception (divide by zero, access violation, etc.), the address at which the exception occurred, and even a stack trace.

So here is the sequence of events when a Win32 exception occurs in a program using this scheme.

  1. The program installs its Win32 exception handler.
  2. The program runs for a while.
  3. At a crucial, deeply-nested point in the program, an access violation occurs.
  4. The Win32 exception handler is called, with details of the exception.
  5. The Win32 exception handler creates a new C++ object, initialising it with detailed information about the exception.
  6. The Win32 exception handler throws the C++ object as a C++ exception.
  7. The call stack unwinds to the nearest catch block for the C++ exception type. All destructors are called correctly, and in general everything is cleaned up as expected.
  8. The catch block has access to full information about the Win32 exception, though the C++ exception. It writes a log message for the programmers, whose debugging task is now much easier.

Example: Turning Win32 exceptions into C++ exceptions

Here a sample implementation of a win32_exception class hierarchy. First, the header file for the exception classes:

Listing 5: win32_exception.h

#include "windows.h"
#include <exception>
class win32_exception: public std::exception
{
public:
typedef const void* Address; // OK on Win32 platform
static void install_handler();
virtual const char* what() const { return mWhat; };
Address where() const { return mWhere; };
unsigned code() const { return mCode; };
protected:
win32_exception(const EXCEPTION_RECORD& info);
private:
const char* mWhat;
Address mWhere;
unsigned mCode;
static void translate(unsigned code, EXCEPTION_POINTERS* info);
};
class access_violation: public win32_exception
{
public:
bool isWrite() const { return mIsWrite; };
Address badAddress() const { return mBadAddress; };
private:
bool mIsWrite;
Address mBadAddress;
access_violation(const EXCEPTION_RECORD& info);
friend void win32_exception::translate(unsigned code, EXCEPTION_POINTERS* info);
};
</exception>

A few notes on this file:

  • The basic class is win32_exception. Since access violations represent a common program bug, and since such exceptions have more information available (the type and location of the bad memory access), there is a specialised derived class for this case only.
  • The win32_exception class inherits from the standard library exception classes for consistency. However, even though access violations and divisions by zero could legitimately inherit from std::logic_error, this is not the case in general for Win32 exceptions. Therefore, win32_exception inherits from std::exception only.
  • The constructors are private, since only win32_exception::translate() is allowed to instantiate these exceptions.
  • The two classes are tightly coupled (note that the base class’s win32_exception::translate() is a friend of the derived class access_violation). This is fine in this lean implementation, but if you wanted to create a large hierarchy of separate classes for each kind of Win32 exception, you’d want to change this design.

Now here’s the implementation of these classes:

Listing 6: win32_exception.cpp

#include "win32_exception.h"
#include "eh.h"
void win32_exception::install_handler()
{
_set_se_translator(win32_exception::translate);
}
void win32_exception::translate(unsigned code, EXCEPTION_POINTERS* info)
{
// Windows guarantees that *(info->ExceptionRecord) is valid
switch (code) {
case EXCEPTION_ACCESS_VIOLATION:
throw access_violation(*(info->ExceptionRecord));
break;
default:
throw win32_exception(*(info->ExceptionRecord));
}
}
win32_exception::win32_exception(const EXCEPTION_RECORD& info)
: mWhat("Win32 exception"), mWhere(info.ExceptionAddress), mCode(info.ExceptionCode)
{
switch (info.ExceptionCode) {
case EXCEPTION_ACCESS_VIOLATION:
mWhat = "Access violation";
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
case EXCEPTION_INT_DIVIDE_BY_ZERO:
mWhat = "Division by zero";
break;
}
}
access_violation::access_violation(const EXCEPTION_RECORD& info)
: win32_exception(info), mIsWrite(false), mBadAddress(0)
{
mIsWrite = info.ExceptionInformation[0] == 1;
mBadAddress = reinterpret_cast<win32_exception ::Address>(info.ExceptionInformation[1]);
}
</win32_exception>

For more details on EXCEPTION_RECORD and the suspicious-looking cast in the access_violation constructor, look up EXCEPTION_RECORD in the MSDN Library.

And finally, here’s part of a program that uses these classes:

Listing 7: main.cpp

#include "win32_exception.h"
#include <iostream>
int main() {
win32_exception::install_handler();
try {
DoComplexAndErrorProneThings();
}
catch (const access_violation& e) {
std::cerr < < e.what() << " at " << std::hex << e.where()
<< ": Bad " << (e.isWrite()?"write":"read")
<< " on " << e.badAddress() << std::endl;
}
catch (const win32_exception& e) {
std::cerr << e.what() << " (code " << std::hex << e.code()
<< ") at " << e.where() << std::endl;
}
}

If DoComplexAndErrorProneThings() attempts to write through a null pointer, a win32_exception will be thrown and something like the following will appear.

Access violation at 72a5ee00: Bad write on 00000000

The great thing about this is that the call stack unwinds correctly as the exception is being thrown: local objects are destroyed, resources are released, and log messages can be written, just as if a normal C++ exception had been thrown — because it has!

The EXCEPTION_POINTERS structure that gets passed to win32_exception::translate() contains a lot of other information. In particular, you can use it to get a stack trace, which would be useful for debugging. This would complicate the exception classes a bit, so I haven’t done it here.

Summary

Visual C++ treats certain runtime errors such as access violations in a similar way to regular C++ exceptions. Unfortunately, the default compiler and project settings cause this to happen in a half-baked way, leading to unstable programs. However, by writing a small amount of extra code, such errors can be handled exactly the same way as C++ exceptions. This greatly aids tracking down mysterious crashes, and allows programs to recover from bugs that normally would prove fatal.

posted @ 2006-06-11 08:06  ->  阅读(1189)  评论(0)    收藏  举报