Author: Jason Molenda
Date: 2025-05-09T20:07:12-07:00
New Revision: e897cb139ee6ef5c145fed5394c4d96baa658e6b

URL: 
https://github.com/llvm/llvm-project/commit/e897cb139ee6ef5c145fed5394c4d96baa658e6b
DIFF: 
https://github.com/llvm/llvm-project/commit/e897cb139ee6ef5c145fed5394c4d96baa658e6b.diff

LOG: [lldb] Provide lr value in faulting frame on arm64 (#138805)

When a frameless function faults or is interrupted asynchronously, the
UnwindPlan MAY have no register location rule for the return address
register (lr on arm64); the value is simply live in the lr register when
it was interrupted, and the frame below this on the stack -- e.g.
sigtramp on a Unix system -- has the full register context, including
that register.

RegisterContextUnwind::SavedLocationForRegister, when asked to find the
caller's pc value, will first see if there is a pc register location. If
there isn't, on a Return Address Register architecture like
arm/mips/riscv, we rewrite the register request from "pc" to "RA
register", and search for a location.

On frame 0 (the live frame) and an interrupted frame, the UnwindPlan may
have no register location rule for the RA Reg, that is valid. A
frameless function that never calls another may simply keep the return
address in the live register the whole way. Our instruction emulation
unwind plans explicitly add a rule (see Pavel's May 2024 change
https://github.com/llvm/llvm-project/pull/91321 ), but an UnwindPlan
sourced from debug_frame may not.

I've got a case where this exactly happens - clang debug_frame for arm64
where there is no register location for the lr in a frameless function.
There is a fault in the middle of this frameless function and we only
get the lr value from the fault handler below this frame if lr has a
register location of `IsSame`, in line with Pavel's 2024 change.

Similar to how we see a request of the RA Reg from frame 0 after failing
to find an unwind location for the pc register, the same style of
special casing is needed when this is a function that was interrupted.

Without this change, we can find the pc of the frame that was executing
when it was interrupted, but we need $lr to find its caller, and we
don't descend down to the trap handler to get that value, truncating the
stack.

rdar://145614545

Added: 
    lldb/test/API/functionalities/unwind/frameless-faulted/Makefile
    
lldb/test/API/functionalities/unwind/frameless-faulted/TestUnwindFramelessFaulted.py
    
lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.c
    lldb/test/API/functionalities/unwind/frameless-faulted/main.c

Modified: 
    lldb/source/Target/RegisterContextUnwind.cpp

Removed: 
    


################################################################################
diff  --git a/lldb/source/Target/RegisterContextUnwind.cpp 
b/lldb/source/Target/RegisterContextUnwind.cpp
index 4c760b84e45a5..cf4b96c6eda9f 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -248,6 +248,7 @@ void RegisterContextUnwind::InitializeZerothFrame() {
     active_row =
         m_full_unwind_plan_sp->GetRowForFunctionOffset(m_current_offset);
     row_register_kind = m_full_unwind_plan_sp->GetRegisterKind();
+    PropagateTrapHandlerFlagFromUnwindPlan(m_full_unwind_plan_sp);
     if (active_row && log) {
       StreamString active_row_strm;
       active_row->Dump(active_row_strm, m_full_unwind_plan_sp.get(), &m_thread,
@@ -1375,6 +1376,7 @@ RegisterContextUnwind::SavedLocationForRegister(
         }
       }
 
+      // Check if the active_row has a register location listed.
       if (regnum.IsValid() && active_row &&
           
active_row->GetRegisterInfo(regnum.GetAsKind(unwindplan_registerkind),
                                       unwindplan_regloc)) {
@@ -1388,11 +1390,10 @@ RegisterContextUnwind::SavedLocationForRegister(
       // This is frame 0 and we're retrieving the PC and it's saved in a Return
       // Address register and it hasn't been saved anywhere yet -- that is,
       // it's still live in the actual register. Handle this specially.
-
       if (!have_unwindplan_regloc && return_address_reg.IsValid() &&
-          IsFrameZero()) {
-        if (return_address_reg.GetAsKind(eRegisterKindLLDB) !=
-            LLDB_INVALID_REGNUM) {
+          return_address_reg.GetAsKind(eRegisterKindLLDB) !=
+              LLDB_INVALID_REGNUM) {
+        if (IsFrameZero()) {
           lldb_private::UnwindLLDB::ConcreteRegisterLocation new_regloc;
           new_regloc.type = UnwindLLDB::ConcreteRegisterLocation::
               eRegisterInLiveRegisterContext;
@@ -1406,6 +1407,17 @@ RegisterContextUnwind::SavedLocationForRegister(
                        return_address_reg.GetAsKind(eRegisterKindLLDB),
                        return_address_reg.GetAsKind(eRegisterKindLLDB));
           return UnwindLLDB::RegisterSearchResult::eRegisterFound;
+        } else if (BehavesLikeZerothFrame()) {
+          // This function was interrupted asynchronously -- it faulted,
+          // an async interrupt, a timer fired, a debugger expression etc.
+          // The caller's pc is in the Return Address register, but the
+          // UnwindPlan for this function may have no location rule for
+          // the RA reg.
+          // This means that the caller's return address is in the RA reg
+          // when the function was interrupted--descend down one stack frame
+          // to retrieve it from the trap handler's saved context.
+          unwindplan_regloc.SetSame();
+          have_unwindplan_regloc = true;
         }
       }
 
@@ -1922,6 +1934,7 @@ void 
RegisterContextUnwind::PropagateTrapHandlerFlagFromUnwindPlan(
   }
 
   m_frame_type = eTrapHandlerFrame;
+  UnwindLogMsg("This frame is marked as a trap handler via its UnwindPlan");
 
   if (m_current_offset_backed_up_one != m_current_offset) {
     // We backed up the pc by 1 to compute the symbol context, but

diff  --git a/lldb/test/API/functionalities/unwind/frameless-faulted/Makefile 
b/lldb/test/API/functionalities/unwind/frameless-faulted/Makefile
new file mode 100644
index 0000000000000..fca47ae47491c
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/frameless-faulted/Makefile
@@ -0,0 +1,13 @@
+C_SOURCES := main.c
+
+interrupt-and-trap-funcs.o: interrupt-and-trap-funcs.c
+       $(CC) $(CFLAGS) -E -o interrupt-and-trap-funcs.s 
$(SRCDIR)/interrupt-and-trap-funcs.c
+       $(CC) $(CFLAGS) -c -o interrupt-and-trap-funcs.o 
interrupt-and-trap-funcs.s
+
+include Makefile.rules
+
+a.out: interrupt-and-trap-funcs.o
+
+# Needs to come after include
+OBJECTS += interrupt-and-trap-funcs.o
+

diff  --git 
a/lldb/test/API/functionalities/unwind/frameless-faulted/TestUnwindFramelessFaulted.py
 
b/lldb/test/API/functionalities/unwind/frameless-faulted/TestUnwindFramelessFaulted.py
new file mode 100644
index 0000000000000..a03d994045773
--- /dev/null
+++ 
b/lldb/test/API/functionalities/unwind/frameless-faulted/TestUnwindFramelessFaulted.py
@@ -0,0 +1,122 @@
+"""Test that lldb backtraces a frameless function that faults correctly."""
+
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test.decorators import *
+import shutil
+import os
+
+
+class TestUnwindFramelessFaulted(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    @skipIf(
+        oslist=no_match([lldbplatformutil.getDarwinOSTriples(), "linux"]),
+        archs=no_match(["aarch64", "arm64", "arm64e"]),
+    )
+    def test_frameless_faulted_unwind(self):
+        self.build()
+
+        (target, process, thread, bp) = lldbutil.run_to_name_breakpoint(
+            self, "main", only_one_thread=False
+        )
+
+        # The test program will have a backtrace like this at its deepest:
+        #
+        #   * frame #0: 0x0000000102adc468 a.out`break_to_debugger + 4
+        #     frame #1: 0x0000000102adc458 a.out`trap + 16
+        #     frame #2: 0x0000000102adc440 a.out`to_be_interrupted + 20
+        #     frame #3: 0x0000000102adc418 a.out`main at main.c:4:7
+        #     frame #4: 0x0000000193b7eb4c dyld`start + 6000
+
+        correct_frames = ["break_to_debugger", "trap", "to_be_interrupted", 
"main"]
+
+        # Keep track of when main has branch & linked, instruction step until 
we're
+        # back in main()
+        main_has_bl_ed = False
+
+        # Instruction step through the binary until we are in a function not
+        # listed in correct_frames.
+        frame = thread.GetFrameAtIndex(0)
+        step_count = 0
+        max_step_count = 200
+        while (
+            process.GetState() == lldb.eStateStopped
+            and frame.name in correct_frames
+            and step_count < max_step_count
+        ):
+            starting_index = 0
+            if self.TraceOn():
+                self.runCmd("bt")
+
+            # Find which index into correct_frames the current stack frame is
+            for idx, name in enumerate(correct_frames):
+                if frame.name == name:
+                    starting_index = idx
+
+            # Test that all frames after the current frame listed in
+            # correct_frames appears in the backtrace.
+            frame_idx = 0
+            for expected_frame in correct_frames[starting_index:]:
+                self.assertEqual(thread.GetFrameAtIndex(frame_idx).name, 
expected_frame)
+                frame_idx = frame_idx + 1
+
+            # When we're at our deepest level, test that register passing of
+            # x0 and x20 follow the by-hand UnwindPlan rules.
+            # In this test program, we can get x0 in the middle of the stack
+            # and we CAN'T get x20. The opposites of the normal AArch64 SysV
+            # ABI.
+            if frame.name == "break_to_debugger":
+                tbi_frame = thread.GetFrameAtIndex(2)
+                self.assertEqual(tbi_frame.name, "to_be_interrupted")
+                # The original argument to to_be_interrupted(), 10
+                # Normally can't get x0 mid-stack, but UnwindPlans have
+                # special rules to make this possible.
+                x0_reg = tbi_frame.register["x0"]
+                self.assertTrue(x0_reg.IsValid())
+                self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
+                # The incremented return value from to_be_interrupted(), 11
+                x24_reg = tbi_frame.register["x24"]
+                self.assertTrue(x24_reg.IsValid())
+                self.assertEqual(x24_reg.GetValueAsUnsigned(), 11)
+                # x20 can normally be fetched mid-stack, but the UnwindPlan
+                # has a rule saying it can't be fetched.
+                x20_reg = tbi_frame.register["x20"]
+                self.assertTrue(x20_reg.error.fail)
+
+                trap_frame = thread.GetFrameAtIndex(1)
+                self.assertEqual(trap_frame.name, "trap")
+                # Confirm that we can fetch x0 from trap() which
+                # is normally not possible w/ SysV AbI, but special
+                # UnwindPlans in use.
+                x0_reg = trap_frame.register["x0"]
+                self.assertTrue(x0_reg.IsValid())
+                self.assertEqual(x0_reg.GetValueAsUnsigned(), 10)
+                x1_reg = trap_frame.register["x1"]
+                self.assertTrue(x1_reg.error.fail)
+
+                main_frame = thread.GetFrameAtIndex(3)
+                self.assertEqual(main_frame.name, "main")
+                # x20 can normally be fetched mid-stack, but the UnwindPlan
+                # has a rule saying it can't be fetched.
+                x20_reg = main_frame.register["x20"]
+                self.assertTrue(x20_reg.error.fail)
+                # x21 can be fetched mid-stack.
+                x21_reg = main_frame.register["x21"]
+                self.assertTrue(x21_reg.error.success)
+
+            # manually move past the BRK instruction in
+            # break_to_debugger().  lldb-server doesn't
+            # advance past the builtin_debugtrap() BRK
+            # instruction.
+            if (
+                thread.GetStopReason() == lldb.eStopReasonException
+                and frame.name == "break_to_debugger"
+            ):
+                frame.SetPC(frame.GetPC() + 4)
+
+            if self.TraceOn():
+                print("StepInstruction")
+            thread.StepInstruction(False)
+            frame = thread.GetFrameAtIndex(0)
+            step_count = step_count + 1

diff  --git 
a/lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.c
 
b/lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.c
new file mode 100644
index 0000000000000..ba6537226f920
--- /dev/null
+++ 
b/lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.c
@@ -0,0 +1,126 @@
+// This is assembly code that needs to be run
+// through the preprocessor, for simplicity of
+// preprocessing it's named .c to start with.
+//
+// clang-format off
+
+
+#define DW_CFA_register 0x9
+#define ehframe_x0  0
+#define ehframe_x20  20
+#define ehframe_x22  22
+#define ehframe_x23  23
+#define ehframe_pc 32
+
+#if defined(__APPLE__)
+#define TO_BE_INTERRUPTED _to_be_interrupted
+#define TRAP _trap
+#define BREAK_TO_DEBUGGER _break_to_debugger
+#else
+#define TO_BE_INTERRUPTED to_be_interrupted
+#define TRAP trap
+#define BREAK_TO_DEBUGGER break_to_debugger
+#endif
+
+  .text
+//--------------------------------------
+// to_be_interrupted() a frameless function that does a non-ABI
+// function call to trap(), simulating an async 
signal/interrup/exception/fault.
+// Before it branches to trap(), put the return address in x23.
+// trap() knows to branch back to $x23 when it has finished.
+//--------------------------------------
+  .globl  TO_BE_INTERRUPTED
+TO_BE_INTERRUPTED:
+  .cfi_startproc
+
+  // This is a garbage entry to ensure that eh_frame is emitted.
+  // If there's no eh_frame, lldb can use the assembly emulation scan,
+  // which always includes a rule for $lr, and we won't replicate the
+  // bug we're testing for.
+  .cfi_escape DW_CFA_register, ehframe_x22, ehframe_x23
+  mov x24, x0
+  add x24, x24, #1
+
+#if defined(__APPLE__)
+  adrp x23, L_.return@PAGE        // put return address in x23
+  add x23, x23, L_.return@PAGEOFF
+#else
+  adrp x23, .L.return
+  add  x23, x23, :lo12:.L.return
+#endif
+
+  b TRAP                     // branch to trap handler, fake async interrupt
+
+#if defined(__APPLE__)
+L_.return:
+#else
+.L.return:
+#endif
+  mov x0, x24
+  ret
+  .cfi_endproc
+  
+
+
+//--------------------------------------
+// trap() trap handler function, sets up stack frame
+// with special unwind rule for the pc value of the
+// "interrupted" stack frame (it's in x23), then calls
+// break_to_debugger().
+//--------------------------------------
+  .globl  TRAP
+TRAP:
+  .cfi_startproc
+  .cfi_signal_frame
+
+  // The pc value when we were interrupted is in x23
+  .cfi_escape DW_CFA_register, ehframe_pc, ehframe_x23
+
+  // For fun, mark x0 as unmodified so the caller can
+  // retrieve the value if it wants.
+  .cfi_same_value ehframe_x0
+
+  // Mark x20 as undefined.  This is a callee-preserved
+  // (non-volatile) register by the SysV AArch64 ABI, but
+  // it'll be fun to see lldb not passing a value past this
+  // point on the stack.
+  .cfi_undefined ehframe_x20
+
+  // standard prologue save of fp & lr so we can call 
+  // break_to_debugger()
+  sub sp, sp, #32
+  stp x29, x30, [sp, #16]
+  add x29, sp, #16
+  .cfi_def_cfa w29, 16
+  .cfi_offset w30, -8
+  .cfi_offset w29, -16
+
+  bl BREAK_TO_DEBUGGER
+
+  ldp x29, x30, [sp, #16]
+  .cfi_same_value x29
+  .cfi_same_value x30
+  .cfi_def_cfa sp, 32
+  add sp, sp, #32
+  .cfi_same_value sp
+  .cfi_def_cfa sp, 0
+
+  // jump back to $x23 to resume execution of to_be_interrupted
+  br x23
+  .cfi_endproc
+
+//--------------------------------------
+// break_to_debugger() executes a BRK instruction
+//--------------------------------------
+  .globl BREAK_TO_DEBUGGER
+BREAK_TO_DEBUGGER:
+  .cfi_startproc
+
+  // For fun, mark x0 as unmodified so the caller can
+  // retrieve the value if it wants.
+  .cfi_same_value ehframe_x0
+
+  brk #0xf000   // __builtin_debugtrap aarch64 instruction
+
+  ret
+  .cfi_endproc

diff  --git a/lldb/test/API/functionalities/unwind/frameless-faulted/main.c 
b/lldb/test/API/functionalities/unwind/frameless-faulted/main.c
new file mode 100644
index 0000000000000..e5f690a21a45e
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/frameless-faulted/main.c
@@ -0,0 +1,7 @@
+int to_be_interrupted(int);
+
+int main() {
+  int c = 10;
+  c = to_be_interrupted(c);
+  return c;
+}


        
_______________________________________________
lldb-commits mailing list
lldb-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to