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