Throwing an exception in C++ requires more than unwinding the stack. As the program unwinds, local variable destructors must be executed. Catch clauses must be examined to see if they should catch the exception. Exception specifications must be checked to see if the exception should be redirected to the unexpected handler. Similar issues arise in Go, Java, and even C when using gcc’s cleanup function attribute.
As I described earlier, each CIE in the unwind data may contain a pointer to a personality function, and each FDE may contain a pointer to the LSDA, the Language Specific Data Area. Each language has its own personality function. The LSDA is only used by the personality function, so it could in principle differ for each language. However, at least for gcc, every language uses the same format, since the LSDA is generated by the language-independent middle-end.
The personality function takes five arguments:
The exception class permits code written in one language to work correctly when an exception is thrown by code written in a different language. The value for g++ is “GNUCC++\0” (or “GNUCC++\1” for a dependent exception, which is used when rethrowing an exception). The value for Go is “GNUCGO\0\0”. The exception specific information can only be examined if the exception class is recognized.
Unwinding the stack for an exception is done in two phases. In the first phase,
the unwinder walks up the stack passing the action
has the value 1) to each personality function that it finds. The personality
function should examine the LSDA to see if there is a handler for the exception
being thrown. It should return
6) if there is or
8) if there isn’t. The search phase will continue
until a handler is found or until the top of the stack is reached. The unwinder
will not actually change anything while walking. If the top of the stack is
reached the unwinder will simply return, and the calling code will take the
appropriate action, which for C++ is to call
std::terminate. Because of the
two phase unwinding approach, if
std::terminate dumps core, a backtrace will
show the code which threw the exception.
If a handler is found, the second phase begins. The unwinder walks up the stack
passing the action
2) to each personality function. The
unwinder will also set
8) in the actions bitmask if the
personality function may not catch the exception, because the unwinding is
happening due to some event like thread cancellation. The unwinder will walk up
the stack until it finds the handler—the stack frame for which the personality
_URC_HANDLER_FOUND. When it calls that function, the
unwinder will pass
4) in the actions bitmask. This time,
the unwinder will changes things as it goes, removing stack frames.
In order to run destructors, the personality function will call
on the context parameter to set the program counter to point to the cleanup
routine, and then return
7) to tell the unwinder to
branch to the current context. The address which starts the cleanup is known as
a landing pad. The cleanup should do whatever it needs to do, and then call
_Unwind_Resume. The exception information needs to be passed to
_Unwind_Resume. The personality routine arranges to pass the exception
information to the cleanup by calling
__builtin_eh_return_data_regno(0) and the exception information passed to the
personality routine. Each target which supports this approach has to dedicate
two registers to holding exception information. This is the first one.
The personality function which finds the handler works pretty much the same
way. It may also use
_Unwind_SetGR to set a value in
__builtin_eh_return_data_regno(1) to indicate which exception was found. The
exception handler may rethrow the exception via
_Unwind_RaiseException or it
may simply continue a normal execution path.
At this point we’ve seen everything except how the personality function decides whether it needs to run a cleanup or catch an exception. The personality function makes this decision based on the LSDA. As mentioned above, while the LSDA could be language dependent, in practice it is not. There is a different personality function for each language, but they all do more or less the same thing, omitting aspects which are not relevant for the language (e.g., there is a personality function for C, but it only runs cleanups and does not bother to look for exception handlers).
The LSDA is found in the section
.gcc_except_table (the personality function
is just a function and lives in the
.text section as usual). The personality
function gets a pointer to it by calling
LSDA starts with the following fields:
DW_EH_PE_omit, the landing pad base. This is the base from which landing pad offsets are computed. If this is omitted, the base comes from calling
_Unwind_GetRegionStart, which returns the beginning of the code described by the current FDE. In practice this field is normally omitted.
DW_EH_PE_omit, the types table pointer. This is an unsigned LEB128 value, and is the byte offset from this field to the start of the types table used for exception matching.
This header is immediately followed by the call-site table. Each entry in the call-site table has four fields. The number of bytes in the header gives the total length. Each entry in the call-site table describes a particular sequence of instructions within the function that the FDE desribes.
The call-site table is sorted by the start address field. If the personality
function finds that there is no entry for the current PC in the call-site
table, then there is no exception information. This should not happen in normal
operation, and in C++ will lead to a call to
std::terminate. If there is an
entry in the call-site table, but the landing pad is zero, then there is
nothing to do: there are no destructors to run or exceptions to catch. This is
a normal case, and the unwinder will simply continue. If the action record is
zero, then there are destructors to run but no exceptions to catch. The
personality function will arrange to run the destructors as described above,
and unwinding will continue.
Otherwise, we have an offset into the action table. Each entry in the action table is a pair of signed LEB128 values. The first number is a type filter. The second number is a byte offset to the next entry in the action table. A byte offset of 0 ends the current set of actions.
A type filter of zero indicates a cleanup, which is the same as an action record of zero in the call-site table. This means that there is a cleanup to be called even if none of the types match.
A positive type filter is an index into the types table. This is a negative
index: the value 1 means the entry preceding the types table base, 2 means the
entry before that, etc. The size of entries in the types table comes from the
encoding in the header, as does the base of the types table. Each entry in the
types table is a pointer to a type information structure. If this type
information structure matches the type of the exception, then we have found a
handler for this exception. The type filter value is a switch value will be
passed to the handler in exception register 1. The actual comparison of the
type information, and determining the type information from the exception
pointer, really is language dependent. In C++ this is a pointer to a
std::type_info structure. A
NULL pointer in the types table is a catch-all
A negative type filter is a byte offset into the types table of a
terminated list of pointers to type information structures. If the type of the
current exception does not match any of the entries in the list, then there is
an exception specification error. This is treated as an exception handler with
a negative switch value.
I think that covers everything about how gcc unwinds the stack and throws exceptions.