The Secret Life of C++: Day 3: Exceptions

Exceptions are wonderful things. They let you report error conditions in vaguely correct ways. But C++ exceptions have to live in a highly hostile environment. No virtual machine to coddle them, they have to leap over large blocks of legacy C code unharmed. TODO: More flavortext.

For more detail about exception handling, check out the Itanium Exception handling ABI.

How Exceptions Work

Exceptions are basically a non-local-goto. They allow a program to transfer control up the stack, in cases when the code at the bottom of the stack doesn't know what else to do. They are even more flexible then this becuase there is a registry system, so that an exception is thrown up the stack until it hits the first stack frame where some expressed interest in dealing with it (ie, a catch () handler).

But this is C++, and nothing is easy. So on our way up the stack, it is important that we properly destroy our stack frames. In C, we could just ignore them, becuase there is no such thing as a destructor. Of course, this simplicity makes it very hard to make C programs that properly handle non-local gotos. But we are talking about C++ and complexity.

So the basic way in which we handle exceptions is to walk up the stack, calling the cleanup code as we go, until we find someone who wants to handle our exception.

Exceptions are also objects, and the fact that they can use inheritance is important. They have an object lifecycle, they get created and destroyed. We will look at this in more detail.

Hello World-ish example.

A Hello Worldish example of exceptions: hello-exceptions.cc, hello-exceptions.s, hello-exceptions.listing.

Time for our good old list of symbols:

_ZNSaIcEC1E
std::allocator<char>::allocator
_ZNSsC1EPKcRKSaIcE
string(const char *)
_ZNSsD1Ev
~string()
__cxa_allocate_exception
Allocate memory for an exception. Generally on the heap. Has access to a last-resort piece of memory for this purpose, so we can throw out of memory exceptions.
__cxa_throw
External interface to throw in the C++ support library. Takes three arguments: an exception object, a typeinfo for that object, and a pointer to the destructor to call when we are done with that object.
_Unwind_RaiseException
Function called by __cxa_throw.
_Unwind_Resume
Resume the unwind process, called at the end of cleanup code that didn't return to the normal thread of execution (ie, not a catch).
__cxa_begin_catch
Keeps track of which exceptions are being caught in which order, pushes this exceptoin on the stack of exceptions that are being handled.
__cxa_end_catch
Take the exception we are processing off the stack and free it. When it returns, we should be in our normal execution thread.

Unwinding the Stack

How does unwinding the stack really work? It happens in two passes.

On the first pass (Phase One), we walk up the stack until we find an exception handler that wants to handle our exception. it is even possible to find a handler up the stack that tells us to ignore the exception. I'm told this functionality is used in Common Lisp implementations.

The second pass (Phase Two) walks up the stack, executing the cleanup code, until we get to the frame which is going to do the catch.

There are two specific methods:

SjLj stack unwinding

In SjLj (Setjmp, Longjmp) stack unwinding, we do a setjmp-ish call each time we enter a function. As we go up the stack, we just longjmp to each setjmp point in succession.

An example of SjLj exceptions: hello-exceptions.cc, hello-exceptions.listing.sjlj,

Dwarf2 stack unwinding

In Dwarf2 stack unwinding we don't have to do any work as long as there are no exceptions, but complexity is increased. We create a symbol table similar to debugging symbols that lets us find out the right places to walk up the stack to.

Forced Unwind versus Regular

There is the concept of a Forced unwind of the stack. A Forced unwind is not caused by an exception being thrown. A forced unwind is when the exception handlers on the call stack aren't allowed to catch an exception, and some other code takes care of knowing when to stop. Two examples of forced unwind are longjmp() and pthread_cancel().

Rethrowing

An example of rethrowing a caught exception: rethrow.cc, rethrow.s, rethrow.listing, rethrow.listing.sjlj.

Catching and Throwing a Different Exception

See the above example. Pretty straightforward. unless we want to throw and catch an exception while handling an exception.

throw() specification on a function

The throw() specification will cause the unwind Phase One to fail with unexepected exception.

Passing through code that doesn't know about exceptions

THis works out, partly because code that doesn't know about exceptions can't have destructors to be called. More succinctly, longjmp just works in native C code, stack frames can just be discarded.
Richard Tibbetts
Last modified: Wed Jan 21 17:36:31 EST 2004