C++ exception (3) – catching an exception
This is the third post of a series that I am making on C++ exceptions.
Personality Routine
To understand how an exception is caught in C++, we need to understand a little more about personality routine. Personality routine at a high level is basically a language specific callback that allows an unwind library (language agnostic) to perform the unwind process. The Itanium ABI specifies the personality routine interface, which looks like the following. (The personality routine implemented in libc++
and libstdc++
(libc++abi
and libsupc++
to be precise) is called __gxx_personality_v0
.)
_Unwind_Reason_Code (*__personality_routine)
(int version,
_Unwind_Action actions,
uint64 exceptionClass,
struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context);
As you might have guessed, _Unwind_Reason_Code
is an enum
that lets the personality routine communicate the result back to libunwind
(or other implementation of the base unwind APIs). E.g. _URC_END_OF_STACK
means no handler was found by the personality routine, _URC_HANDLER_FOUND
means a handler was found, etc. version
here is used to make sure libunwind
and __gxx_personality_v0
are speaking the same protocol. _Unwind_Action
is another enum
that defines the set of jobs a personality routine needs to support (e.g. phase-one search, phase-two cleanup, etc.). The exceptionClass
is not very interesting in this context; we will ignore it for now. _Unwind_Exception
is the type for exception objects.
struct _Unwind_Exception {
uint64 exception_class;
_Unwind_Exception_Cleanup_Fn exception_cleanup;
uint64 private_1;
uint64 private_2;
};
All four fields are mostly not useful for us actually. The exception_class
is mostly not very interesting. The exception_cleanup
function pointer is only useful if you expect a different runtime to clean up the exception object (e.g. catch an exception thrown from Java in C++). We are not supposed to use private_1
or private_2
at all. Where does the actual information about the C++ exception go? E.g. the C++ exception type, etc.?
... a typical runtime such as the C++ runtime will add language-specific information used to process the exception. This is expected to be a contiguous area of memory after the _Unwind_Exception
object ... (https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html)
That makes sense. Since C++ exception allocation needs to go through the C++ ABI __cxa_allocate_exception
anyway, C++ runtime can allocate a few more bytes then and __gxx_personality_v0
can safely assume there are more bytes at the end of _Unwind_Exception
for C++ specific information.
In terms of _Unwind_Context
, it's an opaque struct. struct _Unwind_Context;
is all what it is. We don't need a struct
definition if it's opaque. It's entirely language specific. The C++ ABI can use it however it wants.
Here's what it actually looks like in libc++abi
,
// https://github.com/llvm-mirror/libcxxabi/blob/master/src/cxa_personality.cpp#L945-L966
...
__gxx_personality_v0
#endif
#endif
(int version, _Unwind_Action actions, uint64_t exceptionClass,
_Unwind_Exception* unwind_exception, _Unwind_Context* context)
{
if (version != 1 || unwind_exception == 0 || context == 0)
return _URC_FATAL_PHASE1_ERROR;
bool native_exception = (exceptionClass & get_vendor_and_language) ==
(kOurExceptionClass & get_vendor_and_language);
scan_results results;
if (actions & _UA_SEARCH_PHASE)
{
...
Install the personality routine
Now we have the interface and the implementation in libc++abi
(or libsupc++
). We need to hook the personality routine and the unwind library up. This is done through the .eh_frame
section in DWARF. Basically frame information and personality routines are stored in .eh_frame
and that's where libunwind
finds them. The frame information helps libunwind
find the previous frame, especially in the absence of frame pointers. DWARF itself is very complicated, and I dare not look at it.
Catch an exception
We've roughly covered __cxa_allocate_exception
and __cxa_throw
which are both built on top of the base unwind API. Now the last two __cxa_*
functions in our code are __cxa_begin_catch
and __cxa_end_catch
. Both take an exception object pointer. The main job for this pair of functions is to maintain reference count on the exception object. Refcount is needed because the same exception can be re-thrown, and handled by another exception handler — a catch clause.
As part of the unwind process, libunwind
would work with the personality routine in libc++abi
, read DWARF in the object file, it will find an exception handler (if it exists) and transition control (set program counter) to the exception handler. The actual transition requries setting registers and managing context. libunwind
provides a set of APIs for these tasks.
At a very high level, this is how C++ exception works.