[RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions

From: Jens Remus

Date: Fri Aug 21 2026 - 15:55:22 EST


Add x86-specific support for handling the CFA expression found in GNU
linker generated .eh_frame for PLT.

This enables unwinding through GNU linker generated PLT entries on i386
and x86-64, unless linker option --no-ld-generated-unwind-info is used.

Signed-off-by: Jens Remus <jremus@xxxxxxxxxxxxx>
---
arch/x86/include/asm/unwind_user_eh_frame.h | 68 ++++++++++++++++++++-
kernel/unwind/eh_frame.c | 3 +-
kernel/unwind/eh_frame.h | 14 +++++
3 files changed, 83 insertions(+), 2 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user_eh_frame.h b/arch/x86/include/asm/unwind_user_eh_frame.h
index fdccbda2fe4b..8268eb1a0ff5 100644
--- a/arch/x86/include/asm/unwind_user_eh_frame.h
+++ b/arch/x86/include/asm/unwind_user_eh_frame.h
@@ -2,6 +2,8 @@
#ifndef _ASM_X86_UNWIND_USER_EH_FRAME_H
#define _ASM_X86_UNWIND_USER_EH_FRAME_H

+#include <linux/unwind_user_eh_frame_types.h>
+
#ifdef CONFIG_X86_64

#define EH_FRAME_REG_SP 7 /* designated stack pointer register */
@@ -15,7 +17,71 @@
#define EH_FRAME_MIN_DATA_ALIGN -8
#define EH_FRAME_MAX_DATA_ALIGN -1

-#endif
+#endif /* CONFIG_X86_64 */
+
+static inline int memcmp_masked(const void *s1, const void *s2,
+ const void *mask, size_t n)
+{
+ const unsigned char *p1 = s1, *p2 = s2, *m = mask;
+ int res = 0;
+
+ while (n--)
+ if ((res = (*p1++ ^ *p2++) & *m++))
+ break;
+
+ return res;
+}
+
+static inline int eh_frame_do_def_cfa_expression(const char *expr,
+ int size,
+ unsigned long ip,
+ struct eh_frame_reg_state *reg_state)
+{
+ /*
+ * PLT CFA expression:
+ *
+ * DW_OP_breg<SP> + <SP_offset> // 4 (ESP) + 4 or 7 (RSP) + 8
+ * DW_OP_breg<IP> + 0 // 8 (EIP) or 16 (RIP)
+ * DW_OP_lit15
+ * DW_OP_and
+ * DW_OP_lit<N>
+ * DW_OP_ge
+ * DW_OP_lit<shift> // 2 or 3
+ * DW_OP_shl
+ * DW_OP_plus
+ *
+ * CFA = (SP + offset) + (((IP & 0xf) >= N) << shift)
+ */
+ static const char plt_expr[] = {0x00,0x00,0x00,0x00,0x3f,0x1a,0x30,0x2a,0x30,0x24,0x22};
+ static const char plt_mask[] = {0x00,0xf0,0x00,0xff,0xff,0xff,0xf0,0xff,0xf0,0xff,0xff};
+
+ if (size == sizeof(plt_expr) &&
+ !memcmp_masked(expr, plt_expr, plt_mask, sizeof(plt_expr))) {
+ unsigned char sp_op = expr[0];
+ unsigned char sp_offset = expr[1] & 0x0f;
+ unsigned char ip_op = expr[2];
+ unsigned char n = DW_OP_lit_value(expr[6]);
+ unsigned char shift = DW_OP_lit_value(expr[8]);
+ unsigned char sp_reg, ip_reg;
+
+ if (!DW_OP_is_breg(sp_op) || !DW_OP_is_breg(ip_op))
+ return -EOPNOTSUPP;
+
+ sp_reg = DW_OP_breg_register(sp_op);
+ ip_reg = DW_OP_breg_register(ip_op);
+ if (sp_reg != EH_FRAME_REG_SP || ip_reg != EH_FRAME_REG_RA)
+ return -EOPNOTSUPP;
+
+ /* CFA = (SP + SP_offset) + (((IP & 0xf) >= N) << shift) */
+ reg_state->cfa_rule = CFA_REG_OFFSET;
+ reg_state->cfa_regnum = EH_FRAME_REG_SP;
+ reg_state->cfa_offset = sp_offset + (((ip & 15) >= n) << shift);
+ return 0;
+ }
+
+ return -EOPNOTSUPP;
+}
+#define eh_frame_do_def_cfa_expression eh_frame_do_def_cfa_expression

#include <asm-generic/unwind_user_eh_frame.h>

diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index a98804dbe588..0a81ba1a0377 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -15,11 +15,12 @@
#include <linux/types.h>
#include <linux/unwind_user_types.h>
#include <linux/unwind_user_eh_frame_types.h>
-#include <asm/unwind_user_eh_frame.h>

#include "eh_frame.h"
#include "eh_frame_debug.h"

+#include <asm/unwind_user_eh_frame.h>
+
struct eh_frame_cfi_context {
struct eh_frame_reg_state state;
struct eh_frame_reg_state stack[EH_FRAME_MAX_STATE_STACK];
diff --git a/kernel/unwind/eh_frame.h b/kernel/unwind/eh_frame.h
index a43560486283..2f87d1b32bbc 100644
--- a/kernel/unwind/eh_frame.h
+++ b/kernel/unwind/eh_frame.h
@@ -60,6 +60,20 @@
#define DW_EH_PE_format(encoding) ((encoding) & 0x0f)
#define DW_EH_PE_application(encoding) ((encoding) & 0x70)

+/* DWARF expression operations */
+#define DW_OP_lit0 0x30
+/* ... */
+#define DW_OP_lit31 0x4f
+#define DW_OP_breg0 0x70
+/* ... */
+#define DW_OP_breg31 0x8f
+
+/* Helpers for DWARF expression operations */
+#define DW_OP_is_lit(op) ((op) >= DW_OP_lit0 && (op) <= DW_OP_lit31)
+#define DW_OP_is_breg(op) ((op) >= DW_OP_breg0 && (op) <= DW_OP_breg31)
+#define DW_OP_lit_value(op) ((op) - DW_OP_lit0)
+#define DW_OP_breg_register(op) ((op) - DW_OP_breg0)
+
/* CIE/FDE constants */
#define EH_FRAME_CIE_ID 0
#define EH_FRAME_DWARF64_LENGTH 0xffffffff
--
2.53.0