From 89d1d6657b0ae382bd2251d8c7871730b22a00d4 Mon Sep 17 00:00:00 2001 From: David Chisnall Date: Mon, 27 Apr 2020 18:42:19 +0100 Subject: [PATCH] Rework C++ exception interop again. We now, the first time we encounter a foreign exception, throw a C++ exception through a frame that has a custom personality function and probe the layout of the __cxa_exception structure. We then use the offsets learned from this along with the public ABI functions for allocating the structure. At the same time, add a test that we are correctly setting the count of uncaught exceptions. Fixes #146 --- CMakeLists.txt | 9 +- Test/ObjCXXEHInterop.m | 2 + Test/ObjCXXEHInterop.mm | 11 + cxx_eh_setup.cc | 53 ---- eh_personality.c | 2 + eh_trampoline.cc | 1 + libstdcxx_current_primary_exception.cc | 19 -- objcxx_eh.cc | 321 ++++++++++++++++++++----- unwind-itanium.h | 4 +- 9 files changed, 280 insertions(+), 142 deletions(-) delete mode 100644 cxx_eh_setup.cc delete mode 100644 libstdcxx_current_primary_exception.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index de5c5e93..b5a9ac08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,7 +84,6 @@ if (WIN32) list(APPEND libobjc_CXX_SRCS eh_win32_msvc.cc) else () list(APPEND libobjc_C_SRCS eh_personality.c) - set(libobjcxx_CXX_SRCS objcxx_eh.cc libstdcxx_current_primary_exception.cc) endif (WIN32) @@ -301,11 +300,11 @@ if (ENABLE_OBJCXX) set(ENABLE_OBJCXX false) endif() endif () - add_custom_command(OUTPUT eh_trampoline.S - COMMAND ${CMAKE_CXX_COMPILER} -fPIC -S "${CMAKE_SOURCE_DIR}/eh_trampoline.cc" -o - -fexceptions -fno-inline | sed "s/__gxx_personality_v0/test_eh_personality/g" > "${CMAKE_BINARY_DIR}/eh_trampoline.S" + add_custom_command(OUTPUT eh_trampoline.s + COMMAND ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_FLAGS} -fPIC -S "${CMAKE_SOURCE_DIR}/eh_trampoline.cc" -o - -fexceptions -fno-inline | sed "s/__gxx_personality_v0/test_eh_personality/g" > "${CMAKE_BINARY_DIR}/eh_trampoline.s" MAIN_DEPENDENCY eh_trampoline.cc) - list(APPEND libobjc_ASM_SRCS eh_trampoline.S) - list(APPEND libobjc_CXX_SRCS cxx_eh_setup.cc) + list(APPEND libobjc_ASM_SRCS eh_trampoline.s) + list(APPEND libobjc_CXX_SRCS objcxx_eh.cc) endif () endif (ENABLE_OBJCXX) diff --git a/Test/ObjCXXEHInterop.m b/Test/ObjCXXEHInterop.m index 8d4d275c..36be8b9d 100644 --- a/Test/ObjCXXEHInterop.m +++ b/Test/ObjCXXEHInterop.m @@ -3,6 +3,7 @@ #import "stdio.h" void poke_objcxx(void); +void check_uncaught_count(void); void rethrow(id x) { @@ -18,5 +19,6 @@ int main(void) } @catch (Test *localException) { printf("In NS_HANDLER block, %p\n", localException); } + check_uncaught_count(); } diff --git a/Test/ObjCXXEHInterop.mm b/Test/ObjCXXEHInterop.mm index 768e5071..32fa0558 100644 --- a/Test/ObjCXXEHInterop.mm +++ b/Test/ObjCXXEHInterop.mm @@ -1,6 +1,17 @@ #import "Test.h" #import "stdio.h" +#if __has_include() +#include +extern "C" void check_uncaught_count(void) +{ + assert(__cxxabiv1::__cxa_get_globals()->uncaughtExceptions == 0); +} +#else +extern "C" void check_uncaught_count(void) {} +#error +#endif + extern "C" void rethrow(id); diff --git a/cxx_eh_setup.cc b/cxx_eh_setup.cc deleted file mode 100644 index 3164e924..00000000 --- a/cxx_eh_setup.cc +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include -#include "objc/runtime.h" -#include "dwarf_eh.h" -#include "objcxx_eh.h" - -void cxx_throw() -{ - throw 1; -} - -int eh_trampoline(); -uint64_t cxx_exception_class; - -namespace -{ -inline _Unwind_Reason_Code continueUnwinding(struct _Unwind_Exception *ex, - struct _Unwind_Context *context) -{ -#if defined(__arm__) && !defined(__ARM_DWARF_EH__) - if (__gnu_unwind_frame(ex, context) != _URC_OK) { return _URC_FAILURE; } -#endif - return _URC_CONTINUE_UNWIND; -} - -bool done_setup; -} - -extern "C" -BEGIN_PERSONALITY_FUNCTION(test_eh_personality) - fprintf(stderr, "Fake EH personality called\n"); - if (!done_setup) - { - done_setup = true; - cxx_exception_class = exceptionClass; - } - return CALL_PERSONALITY_FUNCTION(__gxx_personality_v0); -} - -extern "C" void test_cxx_eh_implementation() -{ - bool caught = false; - try - { - eh_trampoline(); - } - catch(int) - { - caught = true; - } - assert(caught); -} - diff --git a/eh_personality.c b/eh_personality.c index a5db35d1..b74ad76b 100644 --- a/eh_personality.c +++ b/eh_personality.c @@ -503,6 +503,8 @@ BEGIN_PERSONALITY_FUNCTION(__gnustep_objcxx_personality_v0) if (0 == ex->cxx_exception) { ex->cxx_exception = objc_init_cxx_exception(ex->object); + ex->cxx_exception->private_1 = exceptionObject->private_1; + ex->cxx_exception->private_2 = exceptionObject->private_2; } exceptionObject = ex->cxx_exception; exceptionClass = cxx_exception_class; diff --git a/eh_trampoline.cc b/eh_trampoline.cc index 24be1772..bd3f9f18 100644 --- a/eh_trampoline.cc +++ b/eh_trampoline.cc @@ -1,5 +1,6 @@ void cxx_throw(); +__attribute((visibility("hidden")) int eh_trampoline() { struct X { ~X() {} } x; diff --git a/libstdcxx_current_primary_exception.cc b/libstdcxx_current_primary_exception.cc deleted file mode 100644 index 5bdd94e4..00000000 --- a/libstdcxx_current_primary_exception.cc +++ /dev/null @@ -1,19 +0,0 @@ -#include "visibility.h" -#include - -#ifdef __GLIBCXX__ -/** - * libsupc++ doesn't expose __cxa_current_primary_exception, so implement this - * using the libstdc++ wrapper. The exception pointer in the - * `std::exception_ptr` object is reference counted, so stealing it by poking - * at the pointer directly means that we acquire it with a reference count of - * 1. - */ -PRIVATE extern "C" void *__cxa_current_primary_exception() -{ - std::exception_ptr p = std::current_exception(); - void *obj = *(void**)&p; - *(void**)&p = nullptr; - return obj; -} -#endif diff --git a/objcxx_eh.cc b/objcxx_eh.cc index ade63ccf..25466ab3 100644 --- a/objcxx_eh.cc +++ b/objcxx_eh.cc @@ -1,16 +1,60 @@ typedef struct objc_object* id; +#include #include #include #include "dwarf_eh.h" #include "objcxx_eh.h" -#include - +#include "visibility.h" #include "objc/runtime.h" +/** + * Helper function that has a custom personality function. + * This calls `cxx_throw` and has a destructor that must be run. We intercept + * the personality function calls and inspect the in-flight C++ exception. + */ +int eh_trampoline(); + +uint64_t cxx_exception_class; +extern "C" void *__cxa_allocate_exception(size_t) noexcept; + +/** + * Our own definitions of C++ ABI functions and types. These are provided + * because this file must not include cxxabi.h. We need to handle subtly + * different variations of the ABI and including one specific implementation + * would make that very difficult. + */ namespace __cxxabiv1 { + /** + * Type info for classes. Forward declared because the GNU ABI provides a + * method on all type_info objects that the dynamic the dynamic cast header + * needs. + */ struct __class_type_info; + /** + * The C++ in-flight exception object. We will derive the offset of fields + * in this, so we do not ever actually see a concrete definition of it. + */ + struct __cxa_exception; + /** + * The public ABI structure for current exception state. + */ + struct __cxa_eh_globals + { + /** + * The current exception that has been caught. + */ + __cxa_exception *caughtExceptions; + /** + * The number of uncaught exceptions still in flight. + */ + unsigned int uncaughtExceptions; + }; + /** + * Retrieve the above structure. + */ + extern "C" __cxa_eh_globals *__cxa_get_globals(); } namespace std @@ -18,13 +62,6 @@ namespace std struct type_info; } -extern "C" -void __cxa_throw(void *thrown_exception, std::type_info *tinfo, - void (*dest)(void *)); - -extern "C" -void *__cxa_current_primary_exception(); - using namespace __cxxabiv1; // Define some C++ ABI types here, rather than including them. This prevents @@ -69,10 +106,103 @@ namespace std }; } -using namespace std; +namespace +{ +/** + * Helper needed by the unwind helper headers. + */ +inline _Unwind_Reason_Code continueUnwinding(struct _Unwind_Exception *ex, + struct _Unwind_Context *context) +{ +#if defined(__arm__) && !defined(__ARM_DWARF_EH__) + if (__gnu_unwind_frame(ex, context) != _URC_OK) { return _URC_FAILURE; } +#endif + return _URC_CONTINUE_UNWIND; +} + + +/** + * Flag indicating that we've already inspected a C++ exception and found all + * of the offsets. + */ +std::atomic done_setup; +/** + * The offset of the C++ type_info object in a thrown exception from the unwind + * header in a `__cxa_exception`. + */ +std::atomic type_info_offset; +/** + * The offset of the reference count in a + */ +std::atomic refcount_offset; +/** + * The size of the `_Unwind_Exception` (including padding) in a + * `__cxa_exception`. + */ +std::atomic exception_struct_size; + +/** + * Helper function to find a particular value scanning backwards in a + * structure. + */ +template +ptrdiff_t find_backwards(void *addr, T val) +{ + T *ptr = reinterpret_cast(addr); + for (ptrdiff_t disp = -1 ; (disp * sizeof(T) > -128) ; disp--) + { + if (ptr[disp] == val) + { + return disp * sizeof(T); + } + } + fprintf(stderr, "Unable to find field in C++ exception structure\n"); + abort(); +} + +/** + * Helper function to find a particular value scanning forwards in a + * structure. + */ +template +ptrdiff_t find_forwards(void *addr, T val) +{ + T *ptr = reinterpret_cast(addr); + for (ptrdiff_t disp = 0 ; (disp * sizeof(T) < 256) ; disp++) + { + if (ptr[disp] == val) + { + return disp * sizeof(T); + } + } + fprintf(stderr, "Unable to find field in C++ exception structure\n"); + abort(); +} + +template +T *pointer_add(void *ptr, ptrdiff_t offset) +{ + return reinterpret_cast(reinterpret_cast(ptr) + offset); +} + +/** + * Exception cleanup function for C++ exceptions that wrap Objective-C + * exceptions. + */ +void exception_cleanup(_Unwind_Reason_Code reason, + struct _Unwind_Exception *ex) +{ + // __cxa_exception takes a pointer to the end of the __cxa_exception + // structure, and so we find that by adding the size of the generic + // exception structure + padding to the pointer to the generic exception + // structure field of the enclosing structure. + auto *cxxEx = pointer_add<__cxa_exception>(ex, exception_struct_size); + __cxa_free_exception(cxxEx); +} -static std::atomic exception_object_offset; -static std::atomic exception_type_offset; +} + +using namespace std; static BOOL isKindOfClass(Class thrown, Class type) @@ -189,58 +319,28 @@ gnustep::libobjc::__objc_id_type_info __objc_id_type_info; struct _Unwind_Exception *objc_init_cxx_exception(id obj) { - void *cxxexception = nullptr; - try - { - id *exception_object = static_cast(__cxa_allocate_exception(sizeof(id))); - *exception_object = obj; - __cxa_throw(exception_object, &__objc_id_type_info, nullptr); - } - catch (...) - { - cxxexception = __cxa_current_primary_exception(); - } - assert(cxxexception); - uint64_t *ehcls = reinterpret_cast(cxxexception); - ehcls--; - int count = 1; - while (*ehcls != cxx_exception_class) - { - ehcls--; - count++; - assert((count < 8) && "Exception structure appears to be corrupt"); - } - ptrdiff_t displacement = reinterpret_cast(cxxexception) - reinterpret_cast(ehcls); - assert((exception_object_offset == 0) || (exception_object_offset == displacement)); - - exception_object_offset = displacement; - - std::type_info **ehtype = reinterpret_cast(ehcls); - ehtype--; - count = 1; - while (*ehtype != &__objc_id_type_info) - { - ehtype--; - count++; - assert((count < 32) && "Exception structure appears to be corrupt"); - } - displacement = reinterpret_cast(ehtype) - reinterpret_cast(ehcls); - assert((exception_type_offset == 0) || (exception_type_offset == displacement)); - - exception_type_offset = displacement; - return reinterpret_cast<_Unwind_Exception*>(ehcls); + id *newEx = static_cast(__cxa_allocate_exception(sizeof(id))); + *newEx = obj; + _Unwind_Exception *ex = pointer_add<_Unwind_Exception>(newEx, -exception_struct_size); + *pointer_add(ex, type_info_offset) = &__objc_id_type_info; + ex->exception_class = cxx_exception_class; + ex->exception_cleanup = exception_cleanup; + __cxa_get_globals()->uncaughtExceptions++; + return ex; } void* objc_object_for_cxx_exception(void *thrown_exception, int *isValid) { - ptrdiff_t type_offset = exception_type_offset; + ptrdiff_t type_offset = type_info_offset; if (type_offset == 0) { *isValid = 0; return nullptr; } + const std::type_info *thrownType = - *reinterpret_cast(reinterpret_cast(thrown_exception) + type_offset); + *pointer_add(thrown_exception, type_offset); + if (!dynamic_cast(thrownType) && !dynamic_cast(thrownType)) { @@ -248,18 +348,113 @@ void* objc_object_for_cxx_exception(void *thrown_exception, int *isValid) return 0; } *isValid = 1; - return *reinterpret_cast(reinterpret_cast(thrown_exception) + exception_object_offset); + return *pointer_add(thrown_exception, exception_struct_size); } -/* -void print_type_info(void *thrown_exception) +} // extern "C" + + +/** + * C++ structure that is thrown through a frame with the `test_eh_personality` + * personality function. This contains a well-known value that we can search + * for after the unwind header. + */ +struct +PRIVATE +MagicValueHolder { - __cxa_exception *ex = (__cxa_exception*) ((char*)thrown_exception - - offsetof(struct __cxa_exception, unwindHeader)); - fprintf(stderr, "Type info: %s\n", ex->exceptionType->name()); - fprintf(stderr, "offset is: %d\n", offsetof(struct __cxa_exception, unwindHeader)); + /** + * The constant that we will search for to identify this object. + */ + static constexpr uint32_t magic = 0x01020304; + /** + * The single field in this structure. + */ + uint32_t magic_value; + /** + * Constructor. Initialises the field with the magic constant. + */ + MagicValueHolder() { magic_value = magic; } +}; + + +/** + * Function that simply throws an instance of `MagicValueHolder`. + */ +PRIVATE void cxx_throw() +{ + MagicValueHolder x; + throw x; } -*/ -} // extern "C" +/** + * Personality function that wraps the C++ personality and inspects the C++ + * exception structure on the way past. This should be used only for the + * `eh_trampoline` function. + */ +extern "C" +PRIVATE +BEGIN_PERSONALITY_FUNCTION(test_eh_personality) + // Don't bother with a mutex here. It doesn't matter if two threads set + // these values at the same time. + if (!done_setup) + { + uint64_t cls = __builtin_bswap64(exceptionClass); + type_info_offset = find_backwards(exceptionObject, &typeid(MagicValueHolder)); +#ifdef __LP64__ + // On 64-bit platforms, the refcount is added to the front of the + // structure. + ptrdiff_t refcount_backwards_offset = type_info_offset - sizeof(uintptr_t); +#else + // On 32-bit platforms, this should be immediately before the + // _Unwind_Exception in some spare padding, but libsupc++ puts it in + // the same place as for 64-bit. Try the one that's definitely in the + // object first and then fall back to the other... + ptrdiff_t refcount_backwards_offset = -sizeof(uint32_t); + auto read_offset = [](void *obj, ptrdiff_t offset) + { + char *addr = reinterpret_cast(obj) + offset; + uintptr_t v = *reinterpret_cast(addr); + return v; + }; + if (read_offset(exceptionObject, refcount_backwards_offset) != 1) + { + refcount_backwards_offset = type_info_offset - sizeof(uintptr_t); + } + if (read_offset(exceptionObject, refcount_backwards_offset) != 1) + { + fprintf(stderr, "Unable to find refcount field\n"); + abort(); + } +#endif + exception_struct_size = find_forwards(exceptionObject, MagicValueHolder::magic); + cxx_exception_class = exceptionClass; + done_setup = true; + } + return CALL_PERSONALITY_FUNCTION(__gxx_personality_v0); +} + +/** + * Probe the C++ exception handling implementation. This throws a C++ + * exception through a function that uses `test_eh_personality` as its + * personality function, allowing us to inspect a C++ exception that is in a + * known state. + */ +extern "C" void test_cxx_eh_implementation() +{ + if (done_setup) + { + return; + } + bool caught = false; + try + { + eh_trampoline(); + } + catch(MagicValueHolder) + { + caught = true; + } + assert(caught); +} diff --git a/unwind-itanium.h b/unwind-itanium.h index 4afca6f7..f7c044de 100644 --- a/unwind-itanium.h +++ b/unwind-itanium.h @@ -79,8 +79,8 @@ struct _Unwind_Exception { uint64_t exception_class; _Unwind_Exception_Cleanup_Fn exception_cleanup; - unsigned long private_1; - unsigned long private_2; + uintptr_t private_1; + uintptr_t private_2; } __attribute__((__aligned__)); extern _Unwind_Reason_Code _Unwind_RaiseException (struct _Unwind_Exception *);