[Lldb-commits] [lldb] b957cc0 - [lldb] Provide lr value in faulting frame on arm64 (#138805)
Jason Molenda via lldb-commits
lldb-commits at lists.llvm.org
Sun May 11 23:39:42 PDT 2025
Author: Jason Molenda
Date: 2025-05-11T23:39:35-07:00
New Revision: b957cc0c7e8eb5895ca519c9cc09e099878f5fbb
URL: https://github.com/llvm/llvm-project/commit/b957cc0c7e8eb5895ca519c9cc09e099878f5fbb
DIFF: https://github.com/llvm/llvm-project/commit/b957cc0c7e8eb5895ca519c9cc09e099878f5fbb.diff
LOG: [lldb] Provide lr value in faulting frame on arm64 (#138805)
Re-landing this patch with small tweaks to address CI bot failures
as it was run on many different configurations. I think the test
may run on aarch64 Linux systems now.
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.s
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 f56dda187e12a..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,
@@ -279,7 +280,7 @@ void RegisterContextUnwind::InitializeZerothFrame() {
call_site_unwind_plan = func_unwinders_sp->GetUnwindPlanAtCallSite(
process->GetTarget(), m_thread);
- if (call_site_unwind_plan != nullptr) {
+ if (call_site_unwind_plan.get() != nullptr) {
m_fallback_unwind_plan_sp = call_site_unwind_plan;
if (TryFallbackUnwindPlan())
cfa_status = true;
@@ -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;
}
}
@@ -1722,10 +1734,10 @@ RegisterContextUnwind::SavedLocationForRegister(
// tricky frame and our usual techniques can continue to be used.
bool RegisterContextUnwind::TryFallbackUnwindPlan() {
- if (m_fallback_unwind_plan_sp == nullptr)
+ if (m_fallback_unwind_plan_sp.get() == nullptr)
return false;
- if (m_full_unwind_plan_sp == nullptr)
+ if (m_full_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp.get() == m_fallback_unwind_plan_sp.get() ||
@@ -1773,7 +1785,7 @@ bool RegisterContextUnwind::TryFallbackUnwindPlan() {
// fallback UnwindPlan. We checked if m_fallback_unwind_plan_sp was nullptr
// at the top -- the only way it became nullptr since then is via
// SavedLocationForRegister().
- if (m_fallback_unwind_plan_sp == nullptr)
+ if (m_fallback_unwind_plan_sp.get() == nullptr)
return true;
// Switch the full UnwindPlan to be the fallback UnwindPlan. If we decide
@@ -1862,10 +1874,10 @@ bool RegisterContextUnwind::TryFallbackUnwindPlan() {
}
bool RegisterContextUnwind::ForceSwitchToFallbackUnwindPlan() {
- if (m_fallback_unwind_plan_sp == nullptr)
+ if (m_fallback_unwind_plan_sp.get() == nullptr)
return false;
- if (m_full_unwind_plan_sp == nullptr)
+ if (m_full_unwind_plan_sp.get() == nullptr)
return false;
if (m_full_unwind_plan_sp.get() == m_fallback_unwind_plan_sp.get() ||
@@ -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..954c184d433ec
--- /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.s
+ $(CC) $(CFLAGS) -E -o interrupt-and-trap-funcs.s $(SRCDIR)/interrupt-and-trap-funcs.s
+ $(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..03af87fcf32f6
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/frameless-faulted/TestUnwindFramelessFaulted.py
@@ -0,0 +1,128 @@
+"""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"]))
+ @skipIf(archs=no_match(["aarch64", "arm64", "arm64e"]))
+
+ # The static linker in Xcode 15.0-15.2 on macOS 14 will mislink
+ # the eh_frame addresses; ld-classic in those tools is one workaround.
+ # This issue was fixed in Xcode 15.3, but it's not straightforward
+ # to test for the linker version or Xcode version so tie this to
+ # macOS 15 which uses Xcode 16 and does not have the issues.
+ @skipIf(macos_version=["<", "15.0"])
+
+ 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.s b/lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.s
new file mode 100644
index 0000000000000..13bb4734f9794
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/frameless-faulted/interrupt-and-trap-funcs.s
@@ -0,0 +1,135 @@
+// 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
+#if defined(__APPLE__)
+ .p2align 2
+#endif
+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 at PAGE // put return address in x23
+ add x23, x23, L_.return at 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
+#if defined(__APPLE__)
+ .p2align 2
+#endif
+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
+#if defined(__APPLE__)
+ .p2align 2
+#endif
+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;
+}
More information about the lldb-commits
mailing list