[A64] Fix ARM64 DWARF/SEH unwind info and add registration test

This commit is contained in:
Herman S.
2026-03-25 10:36:29 +09:00
parent 6e557abb0d
commit 680aeaf52b
6 changed files with 208 additions and 36 deletions

View File

@@ -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<HostToGuestThunk>(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<GuestToHostThunk>(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<ResolveFunctionThunk>(fn);

View File

@@ -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);
}
}

View File

@@ -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<uint8_t>(((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<uint8_t>(0xDC | ((reg_offset >> 2) & 0x01));
buf[off++] = static_cast<uint8_t>(((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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 <execinfo.h>
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>();
memory->Initialize();
std::unique_ptr<xe::cpu::backend::Backend> 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<Processor>(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<TestModule>(
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<GuestFunction*>(fn);
void* code = guest_fn->machine_code();
REQUIRE(code != nullptr);
DWORD64 image_base = 0;
auto* entry = RtlLookupFunctionEntry(reinterpret_cast<DWORD64>(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<TestModule>(
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<ThreadState>(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();
}