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