[A64] Fix ARM64 DWARF/SEH unwind info and add registration test
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user