kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 898aaa7475961d5f8545fffbcc3aba525a4f181b
parent e4406133899dbe7188e20d4f269d259b2df0d6a2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 13:27:42 -0700

x64: map hardware GPR encoding to DWARF reg number in CFI

x64_func_end fed the x86-64 *hardware* register encoding (X64_RBP = 5) into
mc->cfi_def_cfa / cfi_offset, but CFI operands are DWARF register numbers,
where 5 is RDI and RBP is 6. So .eh_frame encoded the frame-pointer CFA rule
as 'def_cfa RDI' / 'offset RDI', corrupting frame-pointer unwinding on
x64-Linux (gdb/libunwind/C++ EH). aa64/rv64 were unaffected because their
frame registers' hardware numbers equal their DWARF numbers.

Factor the existing hw->dwarf table out of x64_register_index to file scope
and expose x64_dwarf_from_hw_gpr; apply it to the RBP def_cfa/offset rules
and the callee-saved cfi_offset loop (the latter is identity today but stays
correct if the callee-saved set grows).

Tests: add an x64 case to cfi_unit.c (pins the SysV CIE template) and wire
cfi_unit.c into test-debug (it built nothing before); add hw->dwarf map
assertions to roundtrip_unit's register checks.

Verified: llvm-dwarfdump now shows DW_CFA_def_cfa: RBP +16 / DW_CFA_offset:
RBP -16 (was RDI).

Diffstat:
Msrc/arch/x64/native.c | 10+++++++---
Msrc/arch/x64/regs.c | 15+++++++++++----
Msrc/arch/x64/regs.h | 5+++++
Mtest/debug/cfi_unit.c | 27+++++++++++++++++++++++----
Mtest/debug/roundtrip_unit.c | 20++++++++++++++++++++
Mtest/test.mk | 10+++++++++-
6 files changed, 75 insertions(+), 12 deletions(-)

diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -1693,12 +1693,16 @@ static void x64_func_end(NativeTarget* t) { u32 post = a->prologue_pos + a->prologue_nbytes; u32 k; mc->cfi_set_next_pc_offset(mc, post - a->func_start); - mc->cfi_def_cfa(mc, X64_RBP, 16); - mc->cfi_offset(mc, X64_RBP, -16); + /* CFI register operands are DWARF numbers, which differ from the x86-64 + * hardware encoding for rbp/rsp/rsi/rdi/rcx/rdx (e.g. rbp is HW 5 but + * DWARF 6). Map every hardware GPR through x64_dwarf_from_hw_gpr; rip's + * DWARF number (16) is already correct. */ + mc->cfi_def_cfa(mc, x64_dwarf_from_hw_gpr(X64_RBP), 16); + mc->cfi_offset(mc, x64_dwarf_from_hw_gpr(X64_RBP), -16); mc->cfi_offset(mc, 16u /* rip */, -8); for (k = 0; k < n_int; ++k) { i32 off = -(i32)xmm_base - (i32)n_fp * 16 - (i32)(k + 1u) * 8; - mc->cfi_offset(mc, cs_int[k], off); + mc->cfi_offset(mc, x64_dwarf_from_hw_gpr(cs_int[k]), off); } } diff --git a/src/arch/x64/regs.c b/src/arch/x64/regs.c @@ -22,6 +22,16 @@ static const X64Reg X64_REGS[] = { static const uint32_t X64_REGS_N = (uint32_t)(sizeof X64_REGS / sizeof X64_REGS[0]); +/* x86-64 hardware GPR index (ModR/M/REX encoding) -> System V DWARF number. + * Single source of truth for both name lookup and CFI register emission. */ +static const uint32_t X64_HW_TO_DWARF[16] = { + 0, 2, 1, 3, 7, 6, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, +}; + +uint32_t x64_dwarf_from_hw_gpr(uint32_t hw) { + return (hw < 16u) ? X64_HW_TO_DWARF[hw] : hw; +} + static int gpr_alias_index(const char* name, const uint32_t* map, uint32_t* idx_out) { static const char* aliases[16][5] = { @@ -68,9 +78,6 @@ const char* x64_register_name(uint32_t dwarf_idx) { int x64_register_index(const char* name, uint32_t* idx_out) { uint32_t i; - static const uint32_t dwarf[16] = { - 0, 2, 1, 3, 7, 6, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, - }; if (!name) return 1; if (name[0] == '%') ++name; { @@ -82,7 +89,7 @@ int x64_register_index(const char* name, uint32_t* idx_out) { } } } - return gpr_alias_index(name, dwarf, idx_out); + return gpr_alias_index(name, X64_HW_TO_DWARF, idx_out); } int x64_register_hw_index(const char* name, uint32_t* idx_out) { diff --git a/src/arch/x64/regs.h b/src/arch/x64/regs.h @@ -6,6 +6,11 @@ const char* x64_register_name(uint32_t dwarf_idx); int x64_register_index(const char* name, uint32_t* idx_out); int x64_register_hw_index(const char* name, uint32_t* idx_out); +/* Map an x86-64 hardware GPR index (0..15, the ModR/M/REX encoding) to its + * System V DWARF register number. The two namespaces differ for rcx/rdx/rsi/ + * rdi/rsp/rbp (e.g. rbp is HW 5 but DWARF 6); r8..r15 and rax/rbx are identity. + * Indices >= 16 (e.g. the literal rip column) pass through unchanged. */ +uint32_t x64_dwarf_from_hw_gpr(uint32_t hw); uint32_t x64_register_iter_size(void); int x64_register_iter_get(uint32_t i, uint32_t* dwarf_out, const char** name_out); diff --git a/test/debug/cfi_unit.c b/test/debug/cfi_unit.c @@ -2,11 +2,12 @@ * mc_emit_eh_frame producer, then spot-check the resulting .eh_frame * section bytes. * - * Covers both aa64 and rv64; the rv64 case validates the locked psABI + * Covers aa64, rv64, and x64; the rv64 case validates the locked psABI * defaults (CFA=sp, RA=ra (DWARF 1), saved s0/ra, callee-saved s2..s11 - * + fs2..fs11) end-to-end. The producer is driven directly via - * MCEmitter and arch_for_compiler so the test stays independent of the - * backend lowering pipeline. */ + * + fs2..fs11) end-to-end, and the x64 case pins the SysV x86-64 DWARF + * register numbering (which diverges from the hardware encoding). The + * producer is driven directly via MCEmitter and arch_for_compiler so the + * test stays independent of the backend lowering pipeline. */ #include <cfree/arch.h> #include <cfree/core.h> @@ -357,6 +358,24 @@ int main(void) { }; check_arch(&ex); } + /* x64: RA=rip (DWARF 16), code_align=1, data_align=-8, CFA init = rsp + * (DWARF 7). After setup, CFA = rbp (DWARF 6) + 16. x64 is the only arch + * whose DWARF register numbers diverge from the hardware encoding (rbp is + * HW 5 = DWARF RDI), so this case pins the SysV x86-64 DWARF numbering. */ + { + CfiExpect ex = { + .arch = CFREE_ARCH_X86_64, + .tag = "x64", + .expected_return_reg = 16, + .expected_code_align = 1, + .expected_data_align = -8, + .expected_cfa_init_reg = 7, + .expected_cfa_init_offset = 8, + .cfa_reg_after_setup = 6, + .cfa_off_after_setup = 16, + }; + check_arch(&ex); + } if (g_fail) { fprintf(stderr, "%d FAILED\n", g_fail); diff --git a/test/debug/roundtrip_unit.c b/test/debug/roundtrip_unit.c @@ -22,6 +22,7 @@ #include <stdlib.h> #include <string.h> +#include "arch/x64/regs.h" #include "core/core.h" #include "core/pool.h" #include "debug/debug.h" @@ -325,6 +326,25 @@ static void run_arch_register_checks(void) { "[rv64] register_index(fp) expected 8, got %u (status %d)", idx, (int)st); } + + /* x64 hardware-GPR-index -> SysV DWARF number. This is the map the x64 + * backend applies before emitting CFI register operands; rcx/rdx/rsi/rdi/ + * rsp/rbp diverge between the two namespaces, while rax/rbx/r8..r15 are + * identity. (Guards the table behind x64_dwarf_from_hw_gpr; the rbp=HW5 case + * is exactly the one that misencoded .eh_frame as RDI before the map was + * applied.) */ + { + static const uint32_t expect[16] = {0, 2, 1, 3, 7, 6, 4, 5, + 8, 9, 10, 11, 12, 13, 14, 15}; + uint32_t hw; + for (hw = 0; hw < 16u; ++hw) { + uint32_t got = x64_dwarf_from_hw_gpr(hw); + EXPECT(got == expect[hw], "[x64] dwarf_from_hw_gpr(%u) expected %u got %u", + hw, expect[hw], got); + } + /* rip (16) and any non-GPR index passes through unchanged. */ + EXPECT(x64_dwarf_from_hw_gpr(16u) == 16u, "[x64] rip passthrough"); + } } static int run_x64_debug_line_check(void) { diff --git a/test/test.mk b/test/test.mk @@ -219,14 +219,22 @@ $(DWARF_TEST_BIN): test/dwarf/dwarf_test.c $(LIB_OBJS) # function symbol). Deliberately bypasses the consumer (cfree_dwarf_open) # so encoder bugs aren't masked by matching decoder bugs. DEBUG_TEST_BIN = build/test/debug_roundtrip_unit +CFI_TEST_BIN = build/test/debug_cfi_unit -test-debug: $(DEBUG_TEST_BIN) +test-debug: $(DEBUG_TEST_BIN) $(CFI_TEST_BIN) $(DEBUG_TEST_BIN) + $(CFI_TEST_BIN) $(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_OBJS) @mkdir -p $(dir $@) $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/roundtrip_unit.c $(LIB_OBJS) -o $@ +# CFI/.eh_frame producer roundtrip for aa64/rv64/x64 (validates the per-arch +# CIE template: code/data-align, return-address reg, CFA-init reg). +$(CFI_TEST_BIN): test/debug/cfi_unit.c $(LIB_OBJS) + @mkdir -p $(dir $@) + $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/cfi_unit.c $(LIB_OBJS) -o $@ + test-dbg: bin @CFREE=$(abspath $(BIN)) sh test/dbg/run.sh