From b24ea8ef99f32f3bfc89c8f6ccae1316d4571978 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:48:15 +0900 Subject: [PATCH] [CPU/XThread] Add DWARF .eh_frame unwind info for JIT code on Linux Generate per-function CIE+FDE records and register them via __register_frame so the C++ exception unwinder can propagate through JIT frames. Replace setjmp/longjmp fiber reentry with throw/catch on Linux to ensure destructors and RAII guards run during fiber stack switches. --- .../cpu/backend/x64/x64_code_cache_posix.cc | 293 +++++++++++++++++- src/xenia/kernel/xthread.cc | 67 +++- src/xenia/kernel/xthread.h | 21 +- 3 files changed, 358 insertions(+), 23 deletions(-) diff --git a/src/xenia/cpu/backend/x64/x64_code_cache_posix.cc b/src/xenia/cpu/backend/x64/x64_code_cache_posix.cc index 7f12212d5..d2356f5d0 100644 --- a/src/xenia/cpu/backend/x64/x64_code_cache_posix.cc +++ b/src/xenia/cpu/backend/x64/x64_code_cache_posix.cc @@ -9,11 +9,74 @@ #include "xenia/cpu/backend/x64/x64_code_cache.h" +#include +#include + +#include "xenia/base/assert.h" +#include "xenia/base/logging.h" +#include "xenia/base/math.h" +#include "xenia/cpu/backend/x64/x64_stack_layout.h" + +// libgcc/libunwind APIs for registering DWARF .eh_frame unwind info. +extern "C" void __register_frame(void*); +extern "C" void __deregister_frame(void*); + namespace xe { namespace cpu { namespace backend { namespace x64 { +// Maximum size of DWARF .eh_frame data per function (CIE + FDE + terminator). +static constexpr uint32_t kMaxUnwindInfoSize = 96; + +// DWARF register numbers for x86-64. +static constexpr uint8_t kDwarfRegRBX = 3; +static constexpr uint8_t kDwarfRegRBP = 6; +static constexpr uint8_t kDwarfRegRSP = 7; +static constexpr uint8_t kDwarfRegR12 = 12; +static constexpr uint8_t kDwarfRegR13 = 13; +static constexpr uint8_t kDwarfRegR14 = 14; +static constexpr uint8_t kDwarfRegR15 = 15; +static constexpr uint8_t kDwarfRegRA = 16; + +// DWARF CFA opcodes. +static constexpr uint8_t kDW_CFA_advance_loc1 = 0x02; +static constexpr uint8_t kDW_CFA_advance_loc2 = 0x03; +static constexpr uint8_t kDW_CFA_def_cfa = 0x0c; +static constexpr uint8_t kDW_CFA_def_cfa_offset = 0x0e; +static constexpr uint8_t kDW_CFA_nop = 0x00; + +// DWARF pointer encoding constants. +static constexpr uint8_t kDW_EH_PE_pcrel = 0x10; +static constexpr uint8_t kDW_EH_PE_sdata4 = 0x0b; + +static size_t WriteULEB128(uint8_t* p, uint64_t value) { + size_t count = 0; + do { + uint8_t byte = value & 0x7F; + value >>= 7; + if (value) byte |= 0x80; + p[count++] = byte; + } while (value); + return count; +} + +static size_t WriteSLEB128(uint8_t* p, int64_t value) { + size_t count = 0; + bool more = true; + while (more) { + uint8_t byte = value & 0x7F; + value >>= 7; + if ((value == 0 && !(byte & 0x40)) || (value == -1 && (byte & 0x40))) { + more = false; + } else { + byte |= 0x80; + } + p[count++] = byte; + } + return count; +} + class PosixX64CodeCache : public X64CodeCache { public: PosixX64CodeCache(); @@ -24,16 +87,19 @@ class PosixX64CodeCache : public X64CodeCache { void* LookupUnwindInfo(uint64_t host_pc) override { return nullptr; } private: - /* UnwindReservation RequestUnwindReservation(uint8_t* entry_address) override; - void PlaceCode(uint32_t guest_address, void* machine_code, size_t code_size, - size_t stack_size, void* code_execute_address, + void PlaceCode(uint32_t guest_address, void* machine_code, + const EmitFunctionInfo& func_info, void* code_execute_address, UnwindReservation unwind_reservation) override; void InitializeUnwindEntry(uint8_t* unwind_entry_address, - size_t unwind_table_slot, void* code_address, - size_t code_size, size_t stack_size); - */ + void* code_execute_address, + const EmitFunctionInfo& func_info); + + // Pointers registered with __register_frame, for cleanup. + std::vector registered_frames_; + // Current number of unwind table entries. + uint32_t unwind_table_count_ = 0; }; std::unique_ptr X64CodeCache::Create() { @@ -41,9 +107,220 @@ std::unique_ptr X64CodeCache::Create() { } PosixX64CodeCache::PosixX64CodeCache() = default; -PosixX64CodeCache::~PosixX64CodeCache() = default; -bool PosixX64CodeCache::Initialize() { return X64CodeCache::Initialize(); } +PosixX64CodeCache::~PosixX64CodeCache() { + for (auto frame : registered_frames_) { + __deregister_frame(frame); + } +} + +bool PosixX64CodeCache::Initialize() { + if (!X64CodeCache::Initialize()) { + return false; + } + registered_frames_.reserve(kMaximumFunctionCount); + return true; +} + +X64CodeCache::UnwindReservation PosixX64CodeCache::RequestUnwindReservation( + uint8_t* entry_address) { +#if defined(NDEBUG) + if (unwind_table_count_ >= kMaximumFunctionCount) { + xe::FatalError( + "Unwind table count exceeded maximum! Please report this to " + "Xenia developers"); + } +#else + assert_false(unwind_table_count_ >= kMaximumFunctionCount); +#endif + UnwindReservation unwind_reservation; + unwind_reservation.data_size = xe::round_up(kMaxUnwindInfoSize, 16); + unwind_reservation.table_slot = unwind_table_count_++; + unwind_reservation.entry_address = entry_address; + return unwind_reservation; +} + +void PosixX64CodeCache::PlaceCode(uint32_t guest_address, void* machine_code, + const EmitFunctionInfo& func_info, + void* code_execute_address, + UnwindReservation unwind_reservation) { + // Write the DWARF .eh_frame data into the reserved unwind space. + InitializeUnwindEntry(unwind_reservation.entry_address, code_execute_address, + func_info); + + // Register with the runtime unwinder using the execute-side address. + // The execute mapping is readable (kExecuteReadOnly = PROT_EXEC|PROT_READ), + // so the unwinder can read the .eh_frame data at runtime. + void* unwind_execute_address = unwind_reservation.entry_address - + generated_code_write_base_ + + generated_code_execute_base_; + __register_frame(unwind_execute_address); + registered_frames_.push_back(unwind_execute_address); +} + +void PosixX64CodeCache::InitializeUnwindEntry( + uint8_t* unwind_entry_address, void* code_execute_address, + const EmitFunctionInfo& func_info) { + // Compute execute-side base address of the unwind buffer. + // We write via the write mapping but pc-relative offsets must be relative + // to the execute mapping (which is what __register_frame sees). + uint8_t* unwind_execute_base = unwind_entry_address - + generated_code_write_base_ + + generated_code_execute_base_; + + uint8_t* p = unwind_entry_address; + uint8_t* cie_start = p; + + // === CIE (Common Information Entry) === + uint8_t* cie_length_ptr = p; + p += 4; // placeholder for length + + uint8_t* cie_content_start = p; + + // CIE ID = 0 (distinguishes CIE from FDE in .eh_frame format). + *reinterpret_cast(p) = 0; + p += 4; + + // Version = 1. + *p++ = 1; + + // Augmentation string "zR" - indicates augmentation data with FDE encoding. + *p++ = 'z'; + *p++ = 'R'; + *p++ = '\0'; + + // Code alignment factor = 1. + p += WriteULEB128(p, 1); + + // Data alignment factor = -8. + p += WriteSLEB128(p, -8); + + // Return address register column = 16 (x86-64 RA). + p += WriteULEB128(p, kDwarfRegRA); + + // Augmentation data length = 1 (just the FDE encoding byte). + p += WriteULEB128(p, 1); + + // FDE pointer encoding: pc-relative, signed 32-bit. + *p++ = kDW_EH_PE_pcrel | kDW_EH_PE_sdata4; + + // Initial instructions: + // DW_CFA_def_cfa RSP, 8 — at function entry, CFA = RSP + 8. + *p++ = kDW_CFA_def_cfa; + p += WriteULEB128(p, kDwarfRegRSP); + p += WriteULEB128(p, 8); + + // DW_CFA_offset RA, 1 — return address at CFA - 8 (factored: 1 * 8). + *p++ = 0x80 | kDwarfRegRA; + p += WriteULEB128(p, 1); + + // Pad CIE to pointer-size (8-byte) alignment. + size_t cie_content_len = static_cast(p - cie_content_start); + size_t cie_padded_len = xe::round_up(cie_content_len, sizeof(void*)); + while (p < cie_content_start + cie_padded_len) { + *p++ = kDW_CFA_nop; + } + + // Write CIE length (excludes the length field itself). + *reinterpret_cast(cie_length_ptr) = + static_cast(p - cie_content_start); + + // === FDE (Frame Description Entry) === + uint8_t* fde_length_ptr = p; + p += 4; // placeholder for length + + uint8_t* fde_content_start = p; + + // CIE pointer: offset from this field back to the start of the CIE. + *reinterpret_cast(p) = static_cast(p - cie_start); + p += 4; + + // PC begin: pc-relative offset to the start of the function code. + // Computed relative to the execute-side address of this field. + uint8_t* pc_begin_execute_addr = + unwind_execute_base + (p - unwind_entry_address); + *reinterpret_cast(p) = + static_cast(reinterpret_cast(code_execute_address) - + reinterpret_cast(pc_begin_execute_addr)); + p += 4; + + // PC range: size of the function code. + *reinterpret_cast(p) = + static_cast(func_info.code_size.total); + p += 4; + + // Augmentation data length = 0 (no LSDA pointer). + p += WriteULEB128(p, 0); + + // FDE instructions: describe how the stack frame changes during the prolog. + if (func_info.stack_size > 0) { + // Advance location to the instruction after the stack allocation. + size_t alloc_offset = func_info.prolog_stack_alloc_offset; + assert_true(alloc_offset > 0); + if (alloc_offset < 64) { + *p++ = 0x40 | static_cast(alloc_offset); + } else if (alloc_offset < 256) { + *p++ = kDW_CFA_advance_loc1; + *p++ = static_cast(alloc_offset); + } else { + *p++ = kDW_CFA_advance_loc2; + *reinterpret_cast(p) = static_cast(alloc_offset); + p += 2; + } + + // DW_CFA_def_cfa_offset: CFA = RSP + 8 + stack_size after stack alloc. + *p++ = kDW_CFA_def_cfa_offset; + p += WriteULEB128(p, 8 + func_info.stack_size); + + // For thunk functions, encode callee-saved register save locations. + // The thunk saves non-volatile registers at known offsets from RSP. + if (func_info.stack_size == StackLayout::THUNK_STACK_SIZE) { + size_t cfa = 8 + func_info.stack_size; // 272 + + // RBX at rsp+0x18 → CFA-248, factored offset = 31 + *p++ = 0x80 | kDwarfRegRBX; + p += WriteULEB128(p, (cfa - 0x18) / 8); + + // RBP at rsp+0x20 → CFA-240, factored offset = 30 + *p++ = 0x80 | kDwarfRegRBP; + p += WriteULEB128(p, (cfa - 0x20) / 8); + + // R12 at rsp+0x40 → CFA-208, factored offset = 26 + *p++ = 0x80 | kDwarfRegR12; + p += WriteULEB128(p, (cfa - 0x40) / 8); + + // R13 at rsp+0x48 → CFA-200, factored offset = 25 + *p++ = 0x80 | kDwarfRegR13; + p += WriteULEB128(p, (cfa - 0x48) / 8); + + // R14 at rsp+0x50 → CFA-192, factored offset = 24 + *p++ = 0x80 | kDwarfRegR14; + p += WriteULEB128(p, (cfa - 0x50) / 8); + + // R15 at rsp+0x58 → CFA-184, factored offset = 23 + *p++ = 0x80 | kDwarfRegR15; + p += WriteULEB128(p, (cfa - 0x58) / 8); + } + } + + // Pad FDE to pointer-size (8-byte) alignment. + size_t fde_content_len = static_cast(p - fde_content_start); + size_t fde_padded_len = xe::round_up(fde_content_len, sizeof(void*)); + while (p < fde_content_start + fde_padded_len) { + *p++ = kDW_CFA_nop; + } + + // Write FDE length. + *reinterpret_cast(fde_length_ptr) = + static_cast(p - fde_content_start); + + // === Terminator (zero-length entry) === + *reinterpret_cast(p) = 0; + p += 4; + + assert_true(static_cast(p - unwind_entry_address) <= + kMaxUnwindInfoSize); +} } // namespace x64 } // namespace backend diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index 7588b1cb0..f89bd29ad 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -9,6 +9,10 @@ #include "xenia/kernel/xthread.h" +#if XE_PLATFORM_LINUX +#include +#endif + #include "xenia/base/byte_stream.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" @@ -541,29 +545,59 @@ void XThread::Execute() { want_exit_code = true; } - // Set up reentry jump buffer for fiber-based stack switching. - // When Reenter() is called (e.g., by KeSetCurrentStackPointers), it will - // longjmp back here instead of throwing an exception through JIT code. + // Set up reentry mechanism for fiber-based stack switching. + // When Reenter() is called (e.g., by KeSetCurrentStackPointers), it + // unwinds back here to re-enter at a new guest address. + // + // On Linux, C++ exceptions are used so that DWARF unwind info (registered + // for JIT code via __register_frame) allows proper destructor/RAII cleanup + // through both JIT and host C++ frames. + // + // On Windows, setjmp/longjmp is used because MSVC's longjmp performs SEH + // stack unwinding which already calls destructors. uint32_t next_address; +#if XE_PLATFORM_LINUX + try { + exit_code = static_cast(kernel_state()->processor()->Execute( + thread_state_, address, args.data(), args.size())); + next_address = 0; + } catch (const FiberReentryException& e) { + // Ensure SIGRTMIN (used for thread suspend) is not left blocked. + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGRTMIN); + pthread_sigmask(SIG_UNBLOCK, &set, nullptr); + next_address = e.address; + } + + while (next_address != 0) { + try { + kernel_state()->processor()->ExecuteRaw(thread_state_, next_address); + next_address = 0; + if (want_exit_code) { + exit_code = static_cast(thread_state_->context()->r[3]); + } + } catch (const FiberReentryException& e) { + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGRTMIN); + pthread_sigmask(SIG_UNBLOCK, &set, nullptr); + next_address = e.address; + } + } +#else if (setjmp(reentry_jmp_buf_) != 0) { - // Longjmp returned here - reentry requested next_address = reentry_address_; } else { - // Initial execution exit_code = static_cast(kernel_state()->processor()->Execute( thread_state_, address, args.data(), args.size())); next_address = 0; } - // Handle reentry loop for fiber switching. - // See XThread::Reenter comments. while (next_address != 0) { - // Set up jump buffer for potential reentries during this execution if (setjmp(reentry_jmp_buf_) != 0) { - // Nested reentry occurred next_address = reentry_address_; } else { - // Execute at the reentry address kernel_state()->processor()->ExecuteRaw(thread_state_, next_address); next_address = 0; if (want_exit_code) { @@ -571,6 +605,7 @@ void XThread::Execute() { } } } +#endif // If we got here it means the execute completed without an exit being called. // Treat the return code as an implicit exit code (if desired). @@ -578,12 +613,18 @@ void XThread::Execute() { } void XThread::Reenter(uint32_t address) { - // Use setjmp/longjmp instead of exceptions to avoid issues with unwinding - // through JIT-compiled code, which lacks exception handling metadata. - // This is called when the game switches fiber stacks (e.g., via + // Called when the game switches fiber stacks (e.g., via // KeSetCurrentStackPointers in games like Forza Horizon 2). + // Must unwind through all frames between here and Execute(). +#if XE_PLATFORM_LINUX + // Throw a C++ exception that unwinds through JIT frames (using DWARF + // .eh_frame info) and host frames (using compiler-generated DWARF), + // calling destructors properly along the way. + throw FiberReentryException{address}; +#else reentry_address_ = address; std::longjmp(reentry_jmp_buf_, 1); +#endif } void XThread::EnterCriticalRegion() { diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index 23d5e5346..727d9a554 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -11,14 +11,17 @@ #define XENIA_KERNEL_XTHREAD_H_ #include -#include #include #include "xenia/base/mutex.h" #if XE_PLATFORM_LINUX #include +#include #include #endif +#if XE_PLATFORM_WIN32 +#include +#endif #include "xenia/base/threading.h" #include "xenia/cpu/thread.h" #include "xenia/cpu/thread_state.h" @@ -340,6 +343,15 @@ struct X_KTHREAD { }; static_assert_size(X_KTHREAD, 0xAB0); +#if XE_PLATFORM_LINUX +// Exception thrown by XThread::Reenter() to unwind through JIT frames. +// C++ exception unwinding uses DWARF .eh_frame info registered for JIT code, +// ensuring destructors and RAII guards in host C++ frames are properly called. +struct FiberReentryException { + uint32_t address; +}; +#endif + class XThread : public XObject, public cpu::Thread { public: static const XObject::Type kObjectType = XObject::Type::Thread; @@ -484,9 +496,14 @@ class XThread : public XObject, public cpu::Thread { std::condition_variable suspend_cv_; #endif - // Reentry context for setjmp/longjmp based stack unwinding + // Reentry mechanism for fiber-based stack switching. + // On Linux, C++ exceptions are used instead of setjmp/longjmp so that + // destructors and RAII guards in host C++ frames are properly unwound. + // JIT code has DWARF .eh_frame unwind info registered via __register_frame. +#if XE_PLATFORM_WIN32 std::jmp_buf reentry_jmp_buf_; uint32_t reentry_address_ = 0; +#endif std::mutex thread_lock_; };