https://github.com/alexey-gusarov updated https://github.com/llvm/llvm-project/pull/215522
>From ac0b0571d82e7855d021096ae27e967e132588ef Mon Sep 17 00:00:00 2001 From: Alexey Gusarov <[email protected]> Date: Tue, 11 Aug 2026 09:49:17 +0200 Subject: [PATCH] [lldb] Don't consult the plan stack for a stop an auto-continue will discard `Thread::ShouldStop()` walks the plan stack from the current plan. When a plan's `ShouldAutoContinue()` returned true, the subsequent plans were still asked `ShouldStop()`, and their answers were then discarded. Asking is not free: a plan that reports `MischiefManaged()` is popped and tears its state down. `ThreadPlanRunToAddress` deletes the breakpoint it was running to, so a plan queued while the thread sat on a breakpoint site lost its target on the very first stop, the single step off the site. Tighten the `ShouldAutoContinue()` contract in `ThreadPlan.h`: an auto-continuing plan short-circuits the `ShouldStop()` of the subsequent plans. `Thread::ShouldStop()` pops it and resumes without consulting them. With this change, from a stop on a breakpoint site: RunToAddress(next instruction) before after directly, or queued by a scripted plan runs past it stops there onto a user breakpoint hit reported hit reported Part (2) of #215189. Assisted-by: Claude Code (Claude Opus 5) --- lldb/include/lldb/Target/ThreadPlan.h | 4 +- lldb/source/Target/Thread.cpp | 3 + .../step_over_breakpoint_site/Makefile | 3 + .../TestStepOverBreakpointSite.py | 96 +++++++++++++++++++ .../step_over_breakpoint_site/main.c | 10 ++ .../run_to_address_plan.py | 20 ++++ 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/Makefile create mode 100644 lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/TestStepOverBreakpointSite.py create mode 100644 lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/main.c create mode 100644 lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/run_to_address_plan.py diff --git a/lldb/include/lldb/Target/ThreadPlan.h b/lldb/include/lldb/Target/ThreadPlan.h index a7bac8cc5ecf6..39ac0ed7b841e 100644 --- a/lldb/include/lldb/Target/ThreadPlan.h +++ b/lldb/include/lldb/Target/ThreadPlan.h @@ -380,8 +380,8 @@ class ThreadPlan : public std::enable_shared_from_this<ThreadPlan>, /// subsequently processed plans. /// /// When processing the thread plan stack, this function gives plans the - /// ability to continue - even when subsequent plans return true from - /// `ShouldStop`. \see Thread::ShouldStop + /// ability to continue. If it returns true, the `ShouldStop` of + /// subsequently processed plans is not consulted. \see Thread::ShouldStop virtual bool ShouldAutoContinue(Event *event_ptr) { return false; } // Whether a "stop class" event should be reported to the "outside world". diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp index b8bfc774b81b3..b6885285e4bd0 100644 --- a/lldb/source/Target/Thread.cpp +++ b/lldb/source/Target/Thread.cpp @@ -944,6 +944,9 @@ bool Thread::ShouldStop(Event *event_ptr) { override_stop = true; LLDB_LOGF(log, "Plan %s auto-continue: true.", current_plan->GetName()); + // The ShouldAutoContinue contract: subsequent plans are not asked. + PopPlan(); + break; } // If a Controlling Plan wants to stop, we let it. Otherwise, see if diff --git a/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/Makefile b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/TestStepOverBreakpointSite.py b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/TestStepOverBreakpointSite.py new file mode 100644 index 0000000000000..97f81ce07823a --- /dev/null +++ b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/TestStepOverBreakpointSite.py @@ -0,0 +1,96 @@ +""" +Test thread plans queued while the thread sits on an enabled breakpoint site. +lldb first steps off the site with a ThreadPlanStepOverBreakpoint; that plan +auto-continues without consulting the plans already on the stack. +""" + +import lldb +import lldbsuite.test.lldbutil as lldbutil +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class StepOverBreakpointSiteTestCase(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def run_to_breakpoint(self): + self.build() + target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, "Set a breakpoint here", lldb.SBFileSpec("main.c") + ) + return target, process, thread + + def pc_after(self, target, thread, count): + """Return the load address `count` instructions after the current one.""" + pc = thread.GetFrameAtIndex(0).GetPCAddress() + instructions = target.ReadInstructions(pc, count + 1) + self.assertEqual(instructions.GetSize(), count + 1) + return ( + instructions.GetInstructionAtIndex(count) + .GetAddress() + .GetLoadAddress(target) + ) + + def run_to_next_instruction(self, target, process, thread): + next_pc = self.pc_after(target, thread, 1) + thread.RunToAddress(next_pc) + self.assertState(process.GetState(), lldb.eStateStopped) + self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), next_pc) + + def test_run_to_address_from_a_breakpoint_site(self): + """RunToAddress with the PC on a breakpoint site stops at the target.""" + target, process, thread = self.run_to_breakpoint() + self.run_to_next_instruction(target, process, thread) + + def test_run_to_address_off_a_breakpoint_site(self): + """RunToAddress with the PC off the breakpoint site stops at the target.""" + target, process, thread = self.run_to_breakpoint() + thread.StepInstruction(False) + self.assertState(process.GetState(), lldb.eStateStopped) + self.run_to_next_instruction(target, process, thread) + + def test_run_to_address_two_instructions_from_a_breakpoint_site(self): + """RunToAddress reaches a target beyond the single step off the site.""" + target, process, thread = self.run_to_breakpoint() + target_pc = self.pc_after(target, thread, 2) + thread.RunToAddress(target_pc) + self.assertState(process.GetState(), lldb.eStateStopped) + self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), target_pc) + + def test_user_breakpoint_at_the_target_is_still_reported(self): + """A user breakpoint at the RunToAddress target is reported as hit.""" + target, process, thread = self.run_to_breakpoint() + next_pc = self.pc_after(target, thread, 1) + user_bp = target.BreakpointCreateByAddress(next_pc) + self.assertTrue(user_bp.GetNumLocations() > 0, VALID_BREAKPOINT) + thread.RunToAddress(next_pc) + self.assertState(process.GetState(), lldb.eStateStopped) + self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), next_pc) + self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) + self.assertEqual(user_bp.GetHitCount(), 1) + + def test_the_stepped_over_breakpoint_is_hit_again(self): + """The breakpoint stepped over before RunToAddress resumes is hit again.""" + target, process, thread = self.run_to_breakpoint() + breakpoint = target.GetBreakpointAtIndex(0) + self.assertEqual(breakpoint.GetHitCount(), 1) + self.run_to_next_instruction(target, process, thread) + process.Continue() + self.assertState(process.GetState(), lldb.eStateStopped) + self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) + self.assertEqual(breakpoint.GetHitCount(), 2) + + def test_scripted_plan_queueing_the_run_to_address(self): + """A scripted plan that queues RunToAddress from a breakpoint site stops + at the target.""" + target, process, thread = self.run_to_breakpoint() + self.runCmd("command script import run_to_address_plan.py") + next_pc = self.pc_after(target, thread, 1) + args = lldb.SBStructuredData() + args.SetFromJSON('{"addr":%d}' % next_pc) + err = thread.StepUsingScriptedThreadPlan( + "run_to_address_plan.RunToAddress", args, True + ) + self.assertSuccess(err) + self.assertState(process.GetState(), lldb.eStateStopped) + self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), next_pc) diff --git a/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/main.c b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/main.c new file mode 100644 index 0000000000000..0390a53881454 --- /dev/null +++ b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/main.c @@ -0,0 +1,10 @@ +static int f(int x) { + int y = x + 1; // Set a breakpoint here. + return y; +} + +int main() { + int a = f(1); + a += f(2); + return a; +} diff --git a/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/run_to_address_plan.py b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/run_to_address_plan.py new file mode 100644 index 0000000000000..8f5e862d57349 --- /dev/null +++ b/lldb/test/API/functionalities/thread_plan/step_over_breakpoint_site/run_to_address_plan.py @@ -0,0 +1,20 @@ +import lldb + + +class RunToAddress: + """Queues a run-to-address sub-plan for the address it is handed.""" + + def __init__(self, thread_plan, args_data): + self.thread_plan = thread_plan + target = thread_plan.GetThread().GetProcess().GetTarget() + addr = args_data.GetValueForKey("addr").GetUnsignedIntegerValue() + self.sub_plan = thread_plan.QueueThreadPlanForRunToAddress( + lldb.SBAddress(addr, target), lldb.SBError() + ) + + def explains_stop(self, event): + return False + + def should_stop(self, event): + self.thread_plan.SetPlanComplete(True) + return True _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
