[llvm] [DebugInfo][WebAssembly] Anchor stack locals to the frame base (PR #211826)

Jonas Devlieghere via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 24 08:16:51 PDT 2026


https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/211826

WebAssembly's stack pointer is not a register that can be described in DWARF. When a function has no virtual frame base, because its stack pointer is never explicitly referenced (e.g. a function whose only local is dead), the frame base falls back to the __stack_pointer global and the frame register is the physical SP.

addMachineReg failed for that register, so addMachineRegExpression dropped the base and the caller emitted the frame offset with nothing under it: a bare DW_OP_plus_uconst that underflows the DWARF stack when the location is evaluated.

  DW_AT_frame_base (DW_OP_WASM_location 0x3 0x0, DW_OP_stack_value)
    DW_AT_location  (DW_OP_plus_uconst 0xc)   ;; before
    DW_AT_location  (DW_OP_fbreg +12)         ;; after

A frame-relative location does not need the frame register to be describable on its own, since it is anchored through DW_AT_frame_base and DW_OP_fbreg. Take the same path already used for a virtual frame register so a physical one without a DWARF number is handled too.

Assisted-by: Claude

PS: I also updated the frame-base fallback comment in WebAssemblyFrameLowering: reads now work at the innermost frame, and the remaining limitation is that the __stack_pointer global does not describe outer frames.

>From 09b5819b4e2c0aeed4945f46627b0890aa8afd33 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Fri, 24 Jul 2026 08:12:39 -0700
Subject: [PATCH] [DebugInfo][WebAssembly] Anchor stack locals to the frame
 base

WebAssembly's stack pointer is not a register that can be described in
DWARF. When a function has no virtual frame base, because its stack
pointer is never explicitly referenced (e.g. a function whose only local
is dead), the frame base falls back to the __stack_pointer global and
the frame register is the physical SP.

addMachineReg failed for that register, so addMachineRegExpression
dropped the base and the caller emitted the frame offset with nothing
under it: a bare DW_OP_plus_uconst that underflows the DWARF stack when
the location is evaluated.

  DW_AT_frame_base (DW_OP_WASM_location 0x3 0x0, DW_OP_stack_value)
    DW_AT_location  (DW_OP_plus_uconst 0xc)   ;; before
    DW_AT_location  (DW_OP_fbreg +12)         ;; after

A frame-relative location does not need the frame register to be
describable on its own, since it is anchored through DW_AT_frame_base
and DW_OP_fbreg. Take the same path already used for a virtual frame
register so a physical one without a DWARF number is handled too.

Assisted-by: Claude

PS: I also updated the frame-base fallback comment in
WebAssemblyFrameLowering: reads now work at the innermost frame, and the
remaining limitation is that the __stack_pointer global does not
describe outer frames.
---
 .../CodeGen/AsmPrinter/DwarfExpression.cpp    |  7 ++++
 .../WebAssembly/WebAssemblyFrameLowering.cpp  |  5 ++-
 .../dbg-fbreg-global-frame-base.ll            | 39 +++++++++++++++++++
 3 files changed, 49 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/DebugInfo/WebAssembly/dbg-fbreg-global-frame-base.ll

diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 9b676f88a0cff..d8c38e521f578 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -122,6 +122,13 @@ bool DwarfExpression::addMachineReg(const TargetRegisterInfo &TRI,
     return true;
   }
 
+  // The frame register is referenced through DW_OP_fbreg relative to
+  // DW_AT_frame_base, so it needs no DWARF register number of its own.
+  if (isFrameRegister(TRI, MachineReg)) {
+    DwarfRegs.push_back(Register::createRegister(-1, nullptr));
+    return true;
+  }
+
   // Walk up the super-register chain until we find a valid number.
   // For example, EAX on x86_64 is a 32-bit fragment of RAX with offset 0.
   for (MCPhysReg SR : TRI.superregs(MachineReg)) {
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyFrameLowering.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyFrameLowering.cpp
index 50f820086c5a6..442d75f1884f0 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyFrameLowering.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyFrameLowering.cpp
@@ -413,8 +413,9 @@ WebAssemblyFrameLowering::getDwarfFrameBase(const MachineFunction &MF) const {
     // that this code is not reached in that case, but assert here to be sure.
     assert(!MF.getSubtarget<WebAssemblySubtarget>().hasLibcallThreadContext());
 
-    // TODO: This should work on a breakpoint at a function with no frame,
-    // but probably won't work for traversing up the stack.
+    // The __stack_pointer global holds only the innermost frame's stack
+    // pointer, so this frame base is correct only for the innermost frame.
+    // TODO: Describe outer frames, which need a per-frame stack pointer value.
     Loc.Location.WasmLoc = {WebAssembly::TI_GLOBAL_RELOC, 0};
   }
   return Loc;
diff --git a/llvm/test/DebugInfo/WebAssembly/dbg-fbreg-global-frame-base.ll b/llvm/test/DebugInfo/WebAssembly/dbg-fbreg-global-frame-base.ll
new file mode 100644
index 0000000000000..c736274466d53
--- /dev/null
+++ b/llvm/test/DebugInfo/WebAssembly/dbg-fbreg-global-frame-base.ll
@@ -0,0 +1,39 @@
+;; A function whose stack pointer is never referenced has no virtual frame base
+;; local, so its frame base is the __stack_pointer global. A stack local must
+;; still be described relative to that frame base with DW_OP_fbreg: the stack
+;; pointer is not a DWARF register, but a frame-relative location needs no
+;; register of its own. Otherwise the base is lost and the offset underflows
+;; the DWARF stack when the location is evaluated.
+
+; RUN: llc < %s -filetype=obj | llvm-dwarfdump - | FileCheck %s
+
+; CHECK:      DW_TAG_subprogram
+; CHECK:        DW_AT_frame_base ({{.*}}0x3 0x0, DW_OP_stack_value)
+; CHECK:        DW_TAG_variable
+; CHECK-NEXT:     DW_AT_location (DW_OP_fbreg +12)
+; CHECK-NEXT:     DW_AT_name ("t1")
+
+target triple = "wasm32-unknown-unknown"
+
+define hidden i32 @main() #0 !dbg !6 {
+  %1 = alloca i32, align 4
+    #dbg_declare(ptr %1, !11, !DIExpression(), !13)
+  ret i32 0, !dbg !13
+}
+
+attributes #0 = { noinline nounwind optnone }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "t.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 4}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!6 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !7, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !10)
+!7 = !DISubroutineType(types: !8)
+!8 = !{!9}
+!9 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!10 = !{!11}
+!11 = !DILocalVariable(name: "t1", scope: !6, file: !1, line: 1, type: !9)
+!13 = !DILocation(line: 1, column: 1, scope: !6)



More information about the llvm-commits mailing list