trouble understanding value in dwarf exception mechanism

I'm having trouble understanding the value in the way exceptions are
handled on Linux, the dwarf/system V ABI exception spec. The mechanism
allows for both cleanup routines and catch handlers, where by cleanup
handlers don't stop the search for a normal handler. The personality
function (I guess no longer part of the standard, but a C++ thing) can
also compare types of the landingpads.

I'm having trouble understanding a few points. I would like to
understand since I have exceptions in my language as well and want to
make effective use of the model.

1. The way basic blocks come together often means you can have embedded
try/catch handlers without an intervening function call. You can't have
multiple landingpads without an intervening invoke thus one block needs
to branch/include the other handler. This appears to result in the need
to check the types of exceptions in the landingpad. This seems wasteful
to check the types in both the personality and the landingpad.

2. Cleanup landingpads, in the general case, will need to call
functions. Not knowing what is in these functions one must assume they
could in turn have their own exception flow: they have a try/catch but
don't allow exceptions to escape. The ABI mechanism though seems like
only one exception at a time can be in progress. This would mean that
cleanup code would have to register as catch handlers and rethrow the
exception -- they cannot be "cleanup" routines according to the standard.

In all I don't see why the current approach is better than simply
treating all landingpads equally in the personality. Each landingpad
would do its own type checking and rethrow or continue as appropriate.
That is, no cleanup routines, and no type matching in the personality.

Hi,

I'm having trouble understanding the value in the way exceptions are
handled on Linux, the dwarf/system V ABI exception spec. The mechanism
allows for both cleanup routines and catch handlers, where by cleanup
handlers don't stop the search for a normal handler. The personality
function (I guess no longer part of the standard, but a C++ thing) can
also compare types of the landingpads.

I'm having trouble understanding a few points. I would like to
understand since I have exceptions in my language as well and want to
make effective use of the model.

Yes, it's not the most well-written spec I've ever had to read...

1. The way basic blocks come together often means you can have embedded
try/catch handlers without an intervening function call. You can't have
multiple landingpads without an intervening invoke thus one block needs
to branch/include the other handler. This appears to result in the need
to check the types of exceptions in the landingpad. This seems wasteful
to check the types in both the personality and the landingpad.

You can optimise this in the front end, by generating a single landing pad for invokes in the nested try blocks and having them emit a single landing pad that declares all of the catchable types in the order that they'll be tested and then jumps to the correct one. I'm not sure if clang does this for C++ or not.

LLVM could do this, but it requires some knowledge of the semantics of the personality function, so it's something that is only safe to turn on for known personality functions. Again, I'm not sure if LLVM does this (there are some optimisations that check the name of the personality function against ones that have known semantics).

2. Cleanup landingpads, in the general case, will need to call
functions. Not knowing what is in these functions one must assume they
could in turn have their own exception flow: they have a try/catch but
don't allow exceptions to escape. The ABI mechanism though seems like
only one exception at a time can be in progress. This would mean that
cleanup code would have to register as catch handlers and rethrow the
exception -- they cannot be "cleanup" routines according to the standard.

C++ supports nested exceptions, so when you throw an exception while catching another, the runtime library maintains a stack of them. The semantics of this are quite messy, and I don't remember them exactly, but they are (not very well) documented in Part III of the Itanium EH ABI doc. When something is thrown from a catch block in C++, the compiler emits an invoke for the throwing function that jumps to a block that calls the end-catch function and then resumes.

A simpler model is to have every call in a cleanup be an invoke with the unwind target a basic block that calls abort(). In practice, this is often enough...

In all I don't see why the current approach is better than simply
treating all landingpads equally in the personality. Each landingpad
would do its own type checking and rethrow or continue as appropriate.
That is, no cleanup routines, and no type matching in the personality.

There are three factors:

1) Having the personality function in a separate shared library means that you can modify its semantics (e.g. add support for dependent exceptions or add an optimised path if a new kind of type info is present) without having to recompile every shared library.

2) Exceptions are supposed to be infrequent and so shouldn't cause much code bloat. The unwind tables are in a separate section and so (hopefully) will only be paged in when an exception is thrown. Code at the landing pad is typically very close to the call[1] and so will not just be paged in, it will probably also even be in the instruction cache. You want to minimise this, especially in a language like C++ that is designed for running microbenchmarks and already has significant problems with i-cache churn.

3) The code for selecting the correct exception can be very complex, especially in the presence of foreign exceptions. Inlining that is not likely to be a significant performance win, and will be optimising for the code path that, by definition, happens in exceptional circumstances.

David

[1] One interesting optimisation would be to move all basic blocks that are landing pads, or solely reachable from landing pads, away from the rest of the code when codegening the module, so that they will not even be swapped in when they are not used. The unwind ABI allows the unwind target to be a very large offset from the rest of the function, so it would be possible to stick all landing pads right at the end of the text section...

I'm having trouble understanding a few points. I would like to
understand since I have exceptions in my language as well and want to
make effective use of the model.

Hi,

You probably got there, but just reiterating: LLVM EH is not modelled with
only C++ exceptions in mind, so some things will look redundant for C++,
but they could not be for other languages. For instance, C++ can only have
one exception, Ada can have as many as you like, and you have to express
both on the same IR language, possibly both styles on the same module!

But also, the IR has to be generic AND efficient, so having more
information on the IR than needed at a lower level might mean that you'll
have better ways to prune stuff at all levels on all EH styles.

1. The way basic blocks come together often means you can have embedded

try/catch handlers without an intervening function call. You can't have
multiple landingpads without an intervening invoke thus one block needs
to branch/include the other handler. This appears to result in the need
to check the types of exceptions in the landingpad. This seems wasteful
to check the types in both the personality and the landingpad.

There are two ways to get to landing pads: when an exception is thrown and
when you're unwinding. On the first case, you go straight to the right
landing pad and it looks a bit redundant, but on the second case, the type
of the exception can be anything, so the personality (or whatever will scan
the landing table) has to go over all types, and you have to have types
that bail, types that continue and types that are treated here and now.

Why not leave all to the personality, you ask down there: because it can
take a considerable amount of time to go all the way to runtime library
code and back, just to go to the right place. Compilers can make that a
jump with a very small immediate instead.

2. Cleanup landingpads, in the general case, will need to call

functions. Not knowing what is in these functions one must assume they
could in turn have their own exception flow: they have a try/catch but
don't allow exceptions to escape. The ABI mechanism though seems like
only one exception at a time can be in progress. This would mean that
cleanup code would have to register as catch handlers and rethrow the
exception -- they cannot be "cleanup" routines according to the standard.

The runtime exception library, at least in C++, has THE exception object
allocated and freed, so if you try to throw another object while one is
being handled, it'll detect and call terminate() in the run time code, not
clean ups, not landing pads, not user code. This is a not-so-hidden
contract[1] between user code, landing pads and the runtime library that
has to be followed, and why it's so hard to implement (and test) exception
handling.

cheers,
--renato

[1] http://mentorembedded.github.io/cxx-abi/abi-eh.html

Hi,

I'm having trouble understanding the value in the way exceptions are
handled on Linux, the dwarf/system V ABI exception spec. The mechanism
allows for both cleanup routines and catch handlers, where by cleanup
handlers don't stop the search for a normal handler. The personality
function (I guess no longer part of the standard, but a C++ thing) can
also compare types of the landingpads.

I'm having trouble understanding a few points. I would like to
understand since I have exceptions in my language as well and want to
make effective use of the model.

1. The way basic blocks come together often means you can have embedded
try/catch handlers without an intervening function call. You can't have
multiple landingpads without an intervening invoke thus one block needs
to branch/include the other handler. This appears to result in the need
to check the types of exceptions in the landingpad.

the kind of test involved is completely different. The personality function
has to do language specific tests with a type, eg class comparisons, which
might be complicated. The landing pad is essentially just executing a switch
instruction (in GCC they experimented with having codegen output a real switch,
but found that a series of "if" comparisons was usually faster). Also,
inlining naturally creates nested catch blocks, so I don't see how you can
escape something like this (need to test multiple catch possibilities in one
landing pad) in general.

  This seems wasteful

to check the types in both the personality and the landingpad.

2. Cleanup landingpads, in the general case, will need to call
functions. Not knowing what is in these functions one must assume they
could in turn have their own exception flow: they have a try/catch but
don't allow exceptions to escape. The ABI mechanism though seems like
only one exception at a time can be in progress. This would mean that
cleanup code would have to register as catch handlers and rethrow the
exception -- they cannot be "cleanup" routines according to the standard.

It is perfectly fine to have a cleanup throw an exception (and maybe catch
it or maybe let it propagate, or whatever), the unwind library doesn't care.
Ada does this and doesn't have to jump through any hoops to get it to work.
The reason that C++ has trouble with this is not coming from the unwind
library, it's coming from C++ semantics (the fact that exception objects
themselves may need complicated destructing IIRC). It's a C++ problem.

In all I don't see why the current approach is better than simply
treating all landingpads equally in the personality. Each landingpad
would do its own type checking and rethrow or continue as appropriate.
That is, no cleanup routines, and no type matching in the personality.

Wouldn't that mean that for every stack frame, you would have to restore
all registers, jump to your code for the stack frame (which would then do
the type testing, i.e. in essence be an inlined version of the personality
function), then maybe continue unwinding. That seems expensive, and like
a good way to get very fat landing pad code. Anyway you can always have
your personality function be very simple: it can not bother with any type
comparisons, just always say "yes, there is a match", causing control to
always branch to your landing pad, giving the effect that you want.

Ciao, Duncan.

I guess I'm looking at my language, and even considering C++, where once
you start using closures, destructors, and defer statements, the frames
which have handling code greatly exceeds those which have no handling
code. Frames which don't need cleanup are often small enough to be
inlined. So the likelihood that an exception can skip over a frame is
very unlikely. Whereas the exception model seems to be optimized with
the assumption that a lot of stack frames can be skipped entirely.