From 680aeaf52b41398a9595da2154dfd0d5df3ca2f3 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:36:29 +0900 Subject: [PATCH] [A64] Fix ARM64 DWARF/SEH unwind info and add registration test --- src/xenia/cpu/backend/a64/a64_backend.cc | 3 + .../cpu/backend/a64/a64_code_cache_posix.cc | 58 ++++++++-- .../cpu/backend/a64/a64_code_cache_win.cc | 68 +++++++----- src/xenia/cpu/backend/a64/a64_emitter.cc | 1 + src/xenia/cpu/backend/code_cache_base.h | 10 ++ .../cpu/testing/backend_integration_test.cc | 104 ++++++++++++++++++ 6 files changed, 208 insertions(+), 36 deletions(-) diff --git a/src/xenia/cpu/backend/a64/a64_backend.cc b/src/xenia/cpu/backend/a64/a64_backend.cc index 701389fe4..b19f1d401 100644 --- a/src/xenia/cpu/backend/a64/a64_backend.cc +++ b/src/xenia/cpu/backend/a64/a64_backend.cc @@ -175,6 +175,7 @@ HostToGuestThunk A64HelperEmitter::EmitHostToGuestThunk() { func_info.prolog_stack_alloc_offset = code_offsets.prolog_stack_alloc - code_offsets.prolog; func_info.stack_size = thunk_stack; + func_info.lr_save_offset = 0x058; // stp x29, x30, [sp, #0x50] void* fn = Emplace(func_info); return reinterpret_cast(fn); @@ -295,6 +296,7 @@ GuestToHostThunk A64HelperEmitter::EmitGuestToHostThunk() { func_info.prolog_stack_alloc_offset = code_offsets.prolog_stack_alloc - code_offsets.prolog; func_info.stack_size = g2h_stack; + func_info.lr_save_offset = 0x1C8; // stp x29, x30, [sp, #0x1C0] void* fn = Emplace(func_info); return reinterpret_cast(fn); @@ -364,6 +366,7 @@ ResolveFunctionThunk A64HelperEmitter::EmitResolveFunctionThunk() { func_info.prolog_stack_alloc_offset = code_offsets.prolog_stack_alloc - code_offsets.prolog; func_info.stack_size = thunk_stack; + func_info.lr_save_offset = 0x058; // stp x29, x30, [sp, #0x50] void* fn = Emplace(func_info); return reinterpret_cast(fn); diff --git a/src/xenia/cpu/backend/a64/a64_code_cache_posix.cc b/src/xenia/cpu/backend/a64/a64_code_cache_posix.cc index 534fc89a9..57bc9773a 100644 --- a/src/xenia/cpu/backend/a64/a64_code_cache_posix.cc +++ b/src/xenia/cpu/backend/a64/a64_code_cache_posix.cc @@ -275,29 +275,65 @@ void PosixA64CodeCache::InitializeUnwindEntry( *p++ = kDW_CFA_def_cfa_offset; p += WriteULEB128(p, func_info.stack_size); - // For thunk functions, encode callee-saved register save locations. if (func_info.stack_size == StackLayout::THUNK_STACK_SIZE) { - size_t cfa = func_info.stack_size; + // Thunk: encode all callee-saved register save locations. + // See a64_stack_layout.h for the layout. + size_t cfa = func_info.stack_size; // 224 - // x19 at sp+0x000 + // GPRs: x19-x28 saved as stp pairs at sp+0x00..0x48 *p++ = 0x80 | kDwarfRegX19; p += WriteULEB128(p, (cfa - 0x000) / 8); - - // x20 at sp+0x008 *p++ = 0x80 | kDwarfRegX20; p += WriteULEB128(p, (cfa - 0x008) / 8); - - // x21 at sp+0x010 *p++ = 0x80 | kDwarfRegX21; p += WriteULEB128(p, (cfa - 0x010) / 8); - - // x29 at sp+0x050 + *p++ = 0x80 | kDwarfRegX22; + p += WriteULEB128(p, (cfa - 0x018) / 8); + *p++ = 0x80 | kDwarfRegX23; + p += WriteULEB128(p, (cfa - 0x020) / 8); + *p++ = 0x80 | kDwarfRegX24; + p += WriteULEB128(p, (cfa - 0x028) / 8); + *p++ = 0x80 | kDwarfRegX25; + p += WriteULEB128(p, (cfa - 0x030) / 8); + *p++ = 0x80 | kDwarfRegX26; + p += WriteULEB128(p, (cfa - 0x038) / 8); + *p++ = 0x80 | kDwarfRegX27; + p += WriteULEB128(p, (cfa - 0x040) / 8); + *p++ = 0x80 | kDwarfRegX28; + p += WriteULEB128(p, (cfa - 0x048) / 8); + // x29 (FP) and x30 (LR) at sp+0x050, sp+0x058 *p++ = 0x80 | kDwarfRegFP; p += WriteULEB128(p, (cfa - 0x050) / 8); - - // x30 at sp+0x058 *p++ = 0x80 | kDwarfRegLR; p += WriteULEB128(p, (cfa - 0x058) / 8); + // NEON: d8-d15 saved as full q8-q15 via stp pairs at sp+0x060..0xDF. + // Each Q is 16 bytes; d8-d15 are the low 64 bits of q8-q15. + // stp q8,q9 at sp+0x060: d8=sp+0x060, d9=sp+0x070 + // stp q10,q11 at sp+0x080: d10=sp+0x080, d11=sp+0x090 + // stp q12,q13 at sp+0x0A0: d12=sp+0x0A0, d13=sp+0x0B0 + // stp q14,q15 at sp+0x0C0: d14=sp+0x0C0, d15=sp+0x0D0 + *p++ = 0x80 | kDwarfRegD8; + p += WriteULEB128(p, (cfa - 0x060) / 8); + *p++ = 0x80 | kDwarfRegD9; + p += WriteULEB128(p, (cfa - 0x070) / 8); + *p++ = 0x80 | kDwarfRegD10; + p += WriteULEB128(p, (cfa - 0x080) / 8); + *p++ = 0x80 | kDwarfRegD11; + p += WriteULEB128(p, (cfa - 0x090) / 8); + *p++ = 0x80 | kDwarfRegD12; + p += WriteULEB128(p, (cfa - 0x0A0) / 8); + *p++ = 0x80 | kDwarfRegD13; + p += WriteULEB128(p, (cfa - 0x0B0) / 8); + *p++ = 0x80 | kDwarfRegD14; + p += WriteULEB128(p, (cfa - 0x0C0) / 8); + *p++ = 0x80 | kDwarfRegD15; + p += WriteULEB128(p, (cfa - 0x0D0) / 8); + } else if (func_info.lr_save_offset > 0) { + // Record where x30 (LR / return address) is saved. + // Without this, the unwinder cannot find the return address. + *p++ = 0x80 | kDwarfRegLR; + p += WriteULEB128(p, + (func_info.stack_size - func_info.lr_save_offset) / 8); } } diff --git a/src/xenia/cpu/backend/a64/a64_code_cache_win.cc b/src/xenia/cpu/backend/a64/a64_code_cache_win.cc index 5f74e5bef..4cbd4a400 100644 --- a/src/xenia/cpu/backend/a64/a64_code_cache_win.cc +++ b/src/xenia/cpu/backend/a64/a64_code_cache_win.cc @@ -36,18 +36,18 @@ namespace a64 { // Codes are stored as a byte array (big-endian for multi-byte codes), listed // in reverse prolog order (last prolog instruction's code first). // -// For the thunk prolog: -// sub sp, sp, #0xA0 (alloc_s or alloc_m) -// stp x19,x20, [sp, #0x00] (save_regp) -// stp x21,x22, [sp, #0x10] (save_regp) -// stp x23,x24, [sp, #0x20] (save_regp) -// stp x25,x26, [sp, #0x30] (save_regp) -// stp x27,x28, [sp, #0x40] (save_regp) -// stp x29,x30, [sp, #0x50] (save_fplr) -// stp d8, d9, [sp, #0x60] (save_fregp) -// stp d10,d11, [sp, #0x70] (save_fregp) -// stp d12,d13, [sp, #0x80] (save_fregp) -// stp d14,d15, [sp, #0x90] (save_fregp) +// For the thunk prolog (224 bytes): +// sub sp, sp, #0xE0 (alloc_s) +// stp x19,x20, [sp, #0x00] (save_regp) +// stp x21,x22, [sp, #0x10] (save_regp) +// stp x23,x24, [sp, #0x20] (save_regp) +// stp x25,x26, [sp, #0x30] (save_regp) +// stp x27,x28, [sp, #0x40] (save_regp) +// stp x29,x30, [sp, #0x50] (save_fplr) +// stp q8, q9, [sp, #0x60] (save_freg x2, 16 bytes apart) +// stp q10,q11, [sp, #0x80] (save_freg x2) +// stp q12,q13, [sp, #0xA0] (save_freg x2) +// stp q14,q15, [sp, #0xC0] (save_freg x2) // ARM64 unwind code builders. // alloc_s: 000XXXXX, allocate X*16 bytes (0..496). @@ -97,6 +97,17 @@ static void EmitSaveFregp(uint8_t* buf, size_t& off, uint32_t reg_offset, buf[off++] = static_cast(((reg_offset & 0x03) << 6) | (z & 0x3F)); } +// save_freg: 1101110X XXzzzzzz +// Save individual d(8+X) at [sp + Z*8]. +static void EmitSaveFreg(uint8_t* buf, size_t& off, uint32_t reg_offset, + uint32_t sp_offset) { + assert_true(reg_offset <= 7); + assert_true((sp_offset % 8) == 0 && sp_offset / 8 <= 63); + uint32_t z = sp_offset / 8; + buf[off++] = static_cast(0xDC | ((reg_offset >> 2) & 0x01)); + buf[off++] = static_cast(((reg_offset & 0x03) << 6) | (z & 0x3F)); +} + // end: 0xE4 static void EmitEnd(uint8_t* buf, size_t& off) { buf[off++] = 0xE4; } @@ -106,14 +117,21 @@ static void EmitEnd(uint8_t* buf, size_t& off) { buf[off++] = 0xE4; } static size_t BuildThunkUnwindCodes(uint8_t* buf) { size_t off = 0; // Codes listed in reverse prolog order (last prolog instruction first). - // stp d14, d15, [sp, #0x90] — d14 = d(8+6) - EmitSaveFregp(buf, off, 6, 0x90); - // stp d12, d13, [sp, #0x80] — d12 = d(8+4) - EmitSaveFregp(buf, off, 4, 0x80); - // stp d10, d11, [sp, #0x70] — d10 = d(8+2) - EmitSaveFregp(buf, off, 2, 0x70); - // stp d8, d9, [sp, #0x60] — d8 = d(8+0) - EmitSaveFregp(buf, off, 0, 0x60); + // Q8-Q15 saved via stp Qn, Qn+1 — each Q is 16 bytes, so d registers + // are 16 bytes apart (not 8 as save_fregp assumes). Use individual + // save_freg codes pointing to the low 64 bits of each Q register. + // stp q14, q15, [sp, #0xC0]: d15 at sp+0xD0, d14 at sp+0xC0 + EmitSaveFreg(buf, off, 7, 0xD0); // d15 + EmitSaveFreg(buf, off, 6, 0xC0); // d14 + // stp q12, q13, [sp, #0xA0]: d13 at sp+0xB0, d12 at sp+0xA0 + EmitSaveFreg(buf, off, 5, 0xB0); // d13 + EmitSaveFreg(buf, off, 4, 0xA0); // d12 + // stp q10, q11, [sp, #0x80]: d11 at sp+0x90, d10 at sp+0x80 + EmitSaveFreg(buf, off, 3, 0x90); // d11 + EmitSaveFreg(buf, off, 2, 0x80); // d10 + // stp q8, q9, [sp, #0x60]: d9 at sp+0x70, d8 at sp+0x60 + EmitSaveFreg(buf, off, 1, 0x70); // d9 + EmitSaveFreg(buf, off, 0, 0x60); // d8 // stp x29, x30, [sp, #0x50] EmitSaveFplr(buf, off, 0x50); // stp x27, x28, [sp, #0x40] — x27 = x(19+8) @@ -126,7 +144,7 @@ static size_t BuildThunkUnwindCodes(uint8_t* buf) { EmitSaveRegp(buf, off, 2, 0x10); // stp x19, x20, [sp, #0x00] — x19 = x(19+0) EmitSaveRegp(buf, off, 0, 0x00); - // sub sp, sp, #0xA0 (160 bytes) + // sub sp, sp, #0xE0 (224 bytes) EmitAllocS(buf, off, StackLayout::THUNK_STACK_SIZE); EmitEnd(buf, off); return off; @@ -154,10 +172,10 @@ static size_t BuildGuestUnwindCodes(uint8_t* buf, uint32_t stack_size) { } // Size of .xdata record for a thunk (header + codes + padding). -// Thunk codes: 5x save_regp(2B) + 1x save_fplr(1B) + 4x save_fregp(2B) + -// 1x alloc_s(1B) + end(1B) = 21 bytes -> 24 bytes padded -> 6 -// code words. Header is 1 word. Total: 7 words = 28 bytes. -static constexpr uint32_t kThunkXdataSize = 28; +// Thunk codes: 5x save_regp(2B) + 1x save_fplr(1B) + 8x save_freg(2B) + +// 1x alloc_s(1B) + end(1B) = 29 bytes -> 32 bytes padded -> 8 +// code words. Header is 1 word. Total: 9 words = 36 bytes. +static constexpr uint32_t kThunkXdataSize = 36; // Size of .xdata record for a guest function (header + codes + padding). // Guest codes: alloc_s(1B) or alloc_m(2B) + end(1B) = 2-3 bytes -> 4 bytes diff --git a/src/xenia/cpu/backend/a64/a64_emitter.cc b/src/xenia/cpu/backend/a64/a64_emitter.cc index aab8b03f5..a84a32351 100644 --- a/src/xenia/cpu/backend/a64/a64_emitter.cc +++ b/src/xenia/cpu/backend/a64/a64_emitter.cc @@ -136,6 +136,7 @@ bool A64Emitter::Emit(hir::HIRBuilder* builder, EmitFunctionInfo& func_info) { // ARM64 ABI: SP must always be 16-byte aligned. assert_true(stack_size % 16 == 0); func_info.stack_size = stack_size; + func_info.lr_save_offset = StackLayout::HOST_RET_ADDR; stack_size_ = stack_size; struct { diff --git a/src/xenia/cpu/backend/code_cache_base.h b/src/xenia/cpu/backend/code_cache_base.h index e3e526998..0e4e9d0f2 100644 --- a/src/xenia/cpu/backend/code_cache_base.h +++ b/src/xenia/cpu/backend/code_cache_base.h @@ -45,6 +45,16 @@ struct EmitFunctionInfo { } code_size; size_t prolog_stack_alloc_offset; size_t stack_size; +#if XE_ARCH_ARM64 + // Offset from SP where x30 (LR) is saved. ARM64 callees save LR + // explicitly at varying offsets; the unwind info generator needs this + // to tell the unwinder where to find the return address. + // Currently only used by the POSIX DWARF .eh_frame generator; the + // Windows .xdata format encodes LR saves differently. Adds 8 bytes + // to the struct on ARM64 Windows builds where it is unused, to avoid + // #if clutter in the backend/emitter code that sets it. + size_t lr_save_offset; +#endif }; // CRTP base class for JIT code caches. Contains all platform-independent diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index b17a7229c..fdcf9414a 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -847,3 +847,107 @@ TEST_CASE("FPCR_PRESERVED_ACROSS_HOST_CALLBACK", "[backend]") { processor->backend()->SetGuestRoundingMode(ctx, 0); memory->SystemHeapFree(stack_address); } + +// ============================================================================= +// Unwind info registration for JIT code +// ============================================================================= +// Verify that the backend registers unwind data for JIT'd functions so that +// debuggers, profilers, and exception handlers can walk the stack through +// JIT code. +// +// Windows: RtlLookupFunctionEntry directly queries the registered SEH tables. +// POSIX: we call backtrace() from inside a JIT callback and verify we get +// enough frames to have unwound through the JIT thunks. This exercises the +// DWARF .eh_frame data registered via __register_frame. + +#if !XE_PLATFORM_WIN32 +#include +static int jit_backtrace_depth = 0; +static void CaptureJITBacktrace(ppc::PPCContext* ctx, void* arg0, void* arg1) { + void* frames[64]; + jit_backtrace_depth = backtrace(frames, 64); +} +#endif + +TEST_CASE("JIT_UNWIND_INFO_REGISTERED", "[backend]") { + auto memory = std::make_unique(); + memory->Initialize(); + + std::unique_ptr backend; +#if XE_ARCH_AMD64 + backend.reset(new xe::cpu::backend::x64::X64Backend()); +#elif XE_ARCH_ARM64 + backend.reset(new xe::cpu::backend::a64::A64Backend()); +#endif + REQUIRE(backend); + + auto processor = std::make_unique(memory.get(), nullptr); + processor->Setup(std::move(backend)); + +#if XE_PLATFORM_WIN32 + // Compile a minimal guest function and check that Windows can find its + // RUNTIME_FUNCTION entry via RtlLookupFunctionEntry. + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [](HIRBuilder& b) { + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + auto* guest_fn = static_cast(fn); + void* code = guest_fn->machine_code(); + REQUIRE(code != nullptr); + + DWORD64 image_base = 0; + auto* entry = RtlLookupFunctionEntry(reinterpret_cast(code), + &image_base, nullptr); + REQUIRE(entry != nullptr); + REQUIRE(image_base != 0); +#else + // On POSIX, call backtrace() from inside a JIT callback. If the .eh_frame + // unwind info is correctly registered, backtrace will unwind through: + // callback -> GuestToHostThunk -> guest func -> HostToGuestThunk -> Call + // giving at least 4 frames. Without unwind info it stops at 1-2. + jit_backtrace_depth = 0; + + auto* builtin_fn = processor->DefineBuiltin( + "CaptureJITBacktrace", CaptureJITBacktrace, nullptr, nullptr); + + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn](HIRBuilder& b) { + b.CallExtern(builtin_fn); + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + uint32_t stack_size = 64 * 1024; + uint32_t stack_address = memory->SystemHeapAlloc(stack_size); + auto thread_state = std::make_unique(processor.get(), 0x100, + stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + + REQUIRE(jit_backtrace_depth >= 4); + + memory->SystemHeapFree(stack_address); +#endif + + memory.reset(); +}