A GCC plugin allowing consistent detection of exceptions in the terminate handler without unwinding noexcept functions
GCC implements noexcept
functions containing catch blocks by adding a cleanup handler that calls std::terminate
. This causes multiple problems:
- The stack is not consistently unwound (GCC bug 55918). Because termination is implemented with a cleanup block rather than dedicated metadata, the runtime does not know that the function is
noexcept
and will continue past it when searching for acatch
block. If it reaches the end of the stack while searching, the stack will not be unwound, but if the exception could be caught by a handler farther up the stack, it will be unwound. - If the stack is unwound, the exception is not available via
std::current_exception
(GCC bug 97720). Exceptions are made available tostd::current_exception
via the__cxa_begin_catch
function. GCC inserts calls to__cxa_begin_catch
before catch blocks but not before cleanup blocks, and since the call tostd::terminate
is in a cleanup block this means the exception is never made available. This is in contrast to the Exception Handling Itanium C++ ABI section 2.5.3, which says that aterminate()
call due to a throw is an exception handler, and all exception handlers must call__cxa_begin_catch
. This is also in contrast to the C++ Standard, which says in section support.exception.propagation thatstd::current_exception
returns "anexception_ptr
object that refers to the currently handled exception", and in section except.handle says "the exception with the most recently activated handler that is still active is called the currently handled exception" and "an implicit handler is considered active when the functionstd::terminate
is entered due to a throw".
- MSVC does not use Itanium C++ ABI, and its ABI has metadata indicating whether a function is
noexcept
. - Clang effectively wraps the function with
try { function body } catch (...) { std::terminate(); }
1. This ensures thatstd::terminate
is always called in a handler, but also requires the stack to always be unwound, which makes postmortem debugging harder.
This approach was inspired by a comment on GCC bug 55918. It is divided into 2 parts: a user source file (compiled and linked with the final binary) that defines a new type and a personality function that catches that type, and a GCC plugin that modifies noexcept
functions to use this new type and personality.
The user source defines a type named __noexcept_marker
. It then defines the type info for that type, and overrides the libstdc++-internal function __do_catch
. This function is normally used to determine whether the thrown object is derived from the base type in the catch block, and to convert it to the base type. In this case, it unconditionally indicates that it can handle whatever exception is thrown, and "converts" it to __noexcept_marker
by ignoring the exception object's actual address and using the address of a global variable instead.
Additionally, the file defines a personality function, which normally checks for catch blocks and handles calling destructors during unwinding. This personality function calls the default personality function, and then checks if the exception was "converted" to __noexcept_marker
. If it was, it immediately calls __cxa_begin_catch
and std::terminate
.
The GCC plugin inserts a new compiler pass just before the exception handling code is lowered. This pass performs 2 transformations on every function being compiled:
- It replaces the default personality function with the one defined in the user source file.
- If the function is
noexcept
, it looks for the cleanup block which callsstd::terminate
and replaces it with a catch block that catches__noexcept_marker
.
- The aforementioned comment suggested calling
std::terminate
directly from the__do_catch
override. However,__do_catch
does not have access to the exception header, so it can't call__cxa_begin_catch
, which means the terminate handler would not have access to the exception. - Initially, the personality returned
_URC_END_OF_STACK
when it detected anoexcept
function, which caused its callee (_Unwind_RaiseException
) to return immediately, causing__cxa_throw
to callstd::terminate
correctly. However, this is not allowed by the ABI, which mandates that the personality function only return_URC_HANDLER_FOUND
or_URC_CONTINUE_UNWIND
when searching for a handler.
The CMake project includes samples demonstrating the plugin's effect and a convenience target for using the plugin. To run the samples, build the target "run-samples". To use the convenience target, add the project using add_subdirectory
and link your binary with use_noexcept_personality
.
Footnotes
-
Unlike if a user specified it, clang does not do this inline, but instead emits a call to
__clang_call_terminate
, which calls__cxa_begin_catch
followed bystd::terminate
↩