INativeExceptionHelper
INativeExcepionHandler
is a helper that may be provided by an ISystem
implementation to facilitate propagating
(but currently not catching!) native exceptions through managed code. It does this by creating helpers which sit
on the native side of P/Invoke transitions which catch and rethrow native exceptions as needed. The native exception
helper implementation will store the current native exception pointer in a thread-local accessible through
the NativeException
property, and provide means to create the transition helpers for arbitrary function pointers
(currently as long as they pass all arguments in-register, and none on stack) to allow users to safely deal with
native exceptions.
Why is this needed?
On Linux, MacOS, and similar OS's, exception handling information is stored in the executable or shared library
ELF/MachO binaries. This information is then read by the platform's libunwind
(governed by the Itanium ABI
specification) when an exception is thrown. This
information is used to unwind the stack, giving each frame an opportunity to perform cleanup work and decide whether
it is able to catch the exception that is being propagated. The appropriate handler is then decided, and execution
resumes at the 'landingpad' for the exception handler. The specification describes the API which can be used by
both application code and by the so-called 'personality' functions for each stack frame, but not the format which
unwind information is stored in, nor where it should be stored.
In practice, on Linux, this is stored in the ELF files that make up the loaded binaries, and have a format governed
by this document.
The format specified is based on the DWARF debug information format, which is horribly overengineered. Moreover,
because these platforms look for unwind info in a section of the loaded binary, there is no means to dynamically
register such information, as would be required by the .NET runtime. (There are, for some platforms, undocumented
functions which serve this purpose: __register_frame
for both GNU libgcc
and NonGNU libunwind
, though those
libraries actually take different parameters, and a much older __register_frame_info
which seems to have stuck
around in MacOS's unwind.h
header, but is documented to not actually work.) As such, for these platforms, we must
dynamically load a shared library containing exception handing information which we must call through in order to
handle exceptions.
Usage
Generally, whenever a call is made to a native function which may throw a native exception, it should be called
through a generated managed-to-native helper, if the managed code is itself called by native code through a
native-to-managed helper. Immediately after such a call, NativeException
should be checked, and if it is
non-null, it should be saved, and execution should be made to exit as immediately as possible. During the final
stages of cleanup, right before returning out of managed code, NativeException
should be set to the exception
being manually propagated. This will cause the native-to-managed helper to rethrow the exception.
It is important to note that exceptions caught this way must be manually propagated, and not caught and
swallowed in managed code. This is because on Linux, such handling of an exception must call
_Unwind_DeleteException
, which is not exposed via the native exception helper.
Implementation
Linux
The shared library for Linux x86-64 is exhelper_linux_x86_64.so
, and is implemented in NASM assembly in
src/MonoMod.Core/Platforms/Architectures/x86_64/exhelper_linux_x86_64.asm
.
Like the rest of the assembly files in its sibling folders, the first line contains a command line which compiles
it.
Broadly speaking, the exception info is automatically generated via a mess of macros, mostly defined in asminc/dwarf_eh.inc
. These macros are largely designed to resemble the CFI directives exposed by the GNU assembler for this
same purpose. Further macros exist in x86_64/macros.inc
and x86_64/dwarf_eh.inc
, which further assist creation
of function with exception handling.
eh_get_exception
and eh_set_exception
These exports expose the TLS cell which holds the current exception. This cell is automatically set by
eh_manged_to_native
when necessary, and cleared by eh_native_to_managed
.
eh_native_to_managed
This export is the native-to-manged entrypoint. First, it clears the TLS cell. Then, it calls the target, passed in
through the architecture-specific special argument register rax
. (This register is not used for argument passing
in any calling convention.) After the target returns, it checks the TLS cell for an exception, and if one is present,
calls _Unwind_RaiseException
with it. Because _Unwind_RaiseException
must unwind the stack, this entrypoint
erects a stack frame for it to unwind through, and ensures that unwind info is present. Due to this, however, stack
passed arguments are not supported currently. They may be in the future, however.
eh_managed_to_native
This export is the managed-to-native entrypoint. This is the meat of the behaviour, containing EH unwind info, as well
as a landingpad which recieves the exception in r15
. When called, it saves rax
(which contains the target) and r15
to the stack, to be restored later. The svreg
macro does this while ensuring that they are present in the unwind info. It then calls the target, with the same limitations as eh_native_to_managed
. Finally, it returns.
If, however, an exception was caught, it saves the exception to the TLS cell, clears the return value, and returns
normally.
_personality
This is the personality function. This is called by the unwinder to decied how to handle exceptions. Our personality function is fairly simple. It uses the language-specific data area pointer to point to a 32-bit relative pointer to the landingpad for a catch if present, and if not, it is zero. Any given procedure may only have one landingpad, and it is hit for all exceptions.