Files
clang-p2996/lldb/test/API/functionalities/breakpoint/step_over_breakpoint/TestStepOverBreakpoint.py
Jason Molenda b666ac3b63 [lldb] Change lldb's breakpoint handling behavior, reland (#126988)
lldb today has two rules: When a thread stops at a BreakpointSite, we
set the thread's StopReason to be "breakpoint hit" (regardless if we've
actually hit the breakpoint, or if we've merely stopped *at* the
breakpoint instruction/point and haven't tripped it yet). And second,
when resuming a process, any thread sitting at a BreakpointSite is
silently stepped over the BreakpointSite -- because we've already
flagged the breakpoint hit when we stopped there originally.

In this patch, I change lldb to only set a thread's stop reason to
breakpoint-hit when we've actually executed the instruction/triggered
the breakpoint. When we resume, we only silently step past a
BreakpointSite that we've registered as hit. We preserve this state
across inferior function calls that the user may do while stopped, etc.

Also, when a user adds a new breakpoint at $pc while stopped, or changes
$pc to be the address of a BreakpointSite, we will silently step past
that breakpoint when the process resumes. This is purely a UX call, I
don't think there's any person who wants to set a breakpoint at $pc and
then hit it immediately on resuming.

One non-intuitive UX from this change, butt is necessary: If you're
stopped at a BreakpointSite that has not yet executed, you `stepi`, you
will hit the breakpoint and the pc will not yet advance. This thread has
not completed its stepi, and the ThreadPlanStepInstruction is still on
the stack. If you then `continue` the thread, lldb will now stop and
say, "instruction step completed", one instruction past the
BreakpointSite. You can continue a second time to resume execution.

The bugs driving this change are all from lldb dropping the real stop
reason for a thread and setting it to breakpoint-hit when that was not
the case. Jim hit one where we have an aarch64 watchpoint that triggers
one instruction before a BreakpointSite. On this arch we are notified of
the watchpoint hit after the instruction has been unrolled -- we disable
the watchpoint, instruction step, re-enable the watchpoint and collect
the new value. But now we're on a BreakpointSite so the watchpoint-hit
stop reason is lost.

Another was reported by ZequanWu in
https://discourse.llvm.org/t/lldb-unable-to-break-at-start/78282 we
attach to/launch a process with the pc at a BreakpointSite and
misbehave. Caroline Tice mentioned it is also a problem they've had with
putting a breakpoint on _dl_debug_state.

The change to each Process plugin that does execution control is that

1. If we've stopped at a BreakpointSite that has not been executed yet,
we will call Thread::SetThreadStoppedAtUnexecutedBP(pc) to record that.
When the thread resumes, if the pc is still at the same site, we will
continue, hit the breakpoint, and stop again.

2. When we've actually hit a breakpoint (enabled for this thread or
not), the Process plugin should call
Thread::SetThreadHitBreakpointSite(). When we go to resume the thread,
we will push a step-over-breakpoint ThreadPlan before resuming.

The biggest set of changes is to StopInfoMachException where we
translate a Mach Exception into a stop reason. The Mach exception codes
differ in a few places depending on the target (unambiguously), and I
didn't want to duplicate the new code for each target so I've tested
what mach exceptions we get for each action on each target, and
reorganized StopInfoMachException::CreateStopReasonWithMachException to
document these possible values, and handle them without specializing
based on the target arch.

I first landed this patch in July 2024 via
https://github.com/llvm/llvm-project/pull/96260

but the CI bots and wider testing found a number of test case failures
that needed to be updated, I reverted it. I've fixed all of those issues
in separate PRs and this change should run cleanly on all the CI bots
now.

rdar://123942164
2025-02-13 11:30:10 -08:00

131 lines
5.6 KiB
Python

"""
Test that breakpoints do not affect stepping.
Check for correct StopReason when stepping to the line with breakpoint
which should be eStopReasonBreakpoint in general,
and eStopReasonPlanComplete when breakpoint's condition fails.
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class StepOverBreakpointsTestCase(TestBase):
def setUp(self):
TestBase.setUp(self)
self.build()
exe = self.getBuildArtifact("a.out")
src = lldb.SBFileSpec("main.cpp")
# Create a target by the debugger.
self.target = self.dbg.CreateTarget(exe)
self.assertTrue(self.target, VALID_TARGET)
# Setup four breakpoints, two of them with false condition
self.line1 = line_number("main.cpp", "breakpoint_1")
self.line4 = line_number("main.cpp", "breakpoint_4")
self.breakpoint1 = self.target.BreakpointCreateByLocation(src, self.line1)
self.assertTrue(
self.breakpoint1 and self.breakpoint1.GetNumLocations() == 1,
VALID_BREAKPOINT,
)
self.breakpoint2 = self.target.BreakpointCreateBySourceRegex(
"breakpoint_2", src
)
self.breakpoint2.GetLocationAtIndex(0).SetCondition("false")
self.breakpoint3 = self.target.BreakpointCreateBySourceRegex(
"breakpoint_3", src
)
self.breakpoint3.GetLocationAtIndex(0).SetCondition("false")
self.breakpoint4 = self.target.BreakpointCreateByLocation(src, self.line4)
# Start debugging
self.process = self.target.LaunchSimple(
None, None, self.get_process_working_directory()
)
self.assertIsNotNone(self.process, PROCESS_IS_VALID)
self.thread = lldbutil.get_one_thread_stopped_at_breakpoint(
self.process, self.breakpoint1
)
self.assertIsNotNone(self.thread, "Didn't stop at breakpoint 1.")
def test_step_instruction(self):
# Count instructions between breakpoint_1 and breakpoint_4
contextList = self.target.FindFunctions("main", lldb.eFunctionNameTypeAuto)
self.assertEqual(contextList.GetSize(), 1)
symbolContext = contextList.GetContextAtIndex(0)
function = symbolContext.GetFunction()
self.assertTrue(function)
instructions = function.GetInstructions(self.target)
addr_1 = self.breakpoint1.GetLocationAtIndex(0).GetAddress()
addr_4 = self.breakpoint4.GetLocationAtIndex(0).GetAddress()
# if third argument is true then the count will be the number of
# instructions on which a breakpoint can be set.
# start = addr_1, end = addr_4, canSetBreakpoint = True
steps_expected = instructions.GetInstructionsCount(addr_1, addr_4, True)
step_count = 0
# Step from breakpoint_1 to breakpoint_4
while True:
self.thread.StepInstruction(True)
step_count = step_count + 1
self.assertState(self.process.GetState(), lldb.eStateStopped)
self.assertTrue(
self.thread.GetStopReason() == lldb.eStopReasonPlanComplete
or self.thread.GetStopReason() == lldb.eStopReasonBreakpoint
)
if self.thread.GetStopReason() == lldb.eStopReasonBreakpoint:
# we should not stop on breakpoint_2 and _3 because they have false condition
self.assertEqual(
self.thread.GetFrameAtIndex(0).GetLineEntry().GetLine(), self.line4
)
# breakpoint_2 and _3 should not affect step count
self.assertGreaterEqual(step_count, steps_expected)
break
# We did a `stepi` when we hit our last breakpoint, and the stepi was not
# completed yet, so when we resume it will complete (running process.Continue()
# would have the same result - we step one instruction and stop again when
# our interrupted stepi completes).
self.thread.StepInstruction(True)
# Run the process until termination
self.process.Continue()
self.assertState(self.process.GetState(), lldb.eStateExited)
@skipIf(bugnumber="llvm.org/pr31972", hostoslist=["windows"])
def test_step_over(self):
self.thread.StepOver()
# We should be stopped at the breakpoint_2 line with stop plan complete reason
self.assertState(self.process.GetState(), lldb.eStateStopped)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonPlanComplete)
self.thread.StepOver()
# We should be stopped at the breakpoint_3 line with stop plan complete reason
self.assertState(self.process.GetState(), lldb.eStateStopped)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonPlanComplete)
self.thread.StepOver()
# We should be stopped at the breakpoint_4
self.assertState(self.process.GetState(), lldb.eStateStopped)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonBreakpoint)
thread1 = lldbutil.get_one_thread_stopped_at_breakpoint(
self.process, self.breakpoint4
)
self.assertEqual(self.thread, thread1, "Didn't stop at breakpoint 4.")
# Check that stepping does not affect breakpoint's hit count
self.assertEqual(self.breakpoint1.GetHitCount(), 1)
self.assertEqual(self.breakpoint2.GetHitCount(), 0)
self.assertEqual(self.breakpoint3.GetHitCount(), 0)
self.assertEqual(self.breakpoint4.GetHitCount(), 1)
# Run the process until termination
self.process.Continue()
self.assertState(self.process.GetState(), lldb.eStateExited)