Skip to content

Commit a2f2417

Browse files
jasonmolendajoaosaffran
authored and
joaosaffran
committed
[lldb] Update ThreadPlanStepOut to handle new breakpoint behavior (llvm#126838)
I will be changing breakpoint hitting behavior soon, where currently lldb reports a breakpoint as being hit when a thread is *at* a BreakpointSite, but possibly has not executed the breakpoint instruction and trapped yet, to having lldb only report a breakpoint hit when the breakpoint instruction has actually been executed. One corner case bug with this change is that when you are stopped at a breakpoint (that has been hit) on the last instruction of a function, and you do `finish`, a ThreadPlanStepOut is pushed to the thread's plan stack to put a breakpoint on the return address and resume execution. And when the thread is asked to resume, it sees that it is at a BreakpointSite that has been hit, and pushes a ThreadPlanStepOverBreakpoint on the thread. The StepOverBreakpoint plan sees that the thread's state is eStateRunning (not eStateStepping), so it marks itself as "auto continue" -- so once the breakpoint has been stepped over, we will execution on the thread. With current lldb stepping behavior ("a thread *at* a BreakpointSite is said to have stopped with a breakpoint-hit stop reason, even if the breakpoint hasn't been executed yet"), `ThreadPlanStepOverBreakpoint::DoPlanExplainsStop` has a special bit of code which detects when the thread stops with a eStopReasonBreakpoint. It first checks if the pc is the same as when we started -- did our "step instruction" not actually step? -- says the stop reason is explained. Otherwise it sets auto-continue to false (because we've hit an *unexpected* breakpoint, and we have advanced past our original pc, and returns false - the stop reason is not explained. So we do the "finish", lldb instruction steps, we stop *at* the return-address breakpoint and lldb sets the thread's stop reason to breakpoint-hit. ThreadPlanStepOverBreakpoint sees an eStopReasonBreakpoint, sets its auto-continue to false, and says we stopped for osme reason other than this plan. (and it will also report `IsPlanStale()==true` so it will remove itself) Meanwhile the ThreadPlanStepOut sees that it has stopped in the StackID it wanted to run to, and return success. This all changes when stopping at a breakpoint site doesn't report breakpoint-hit until we actually execute the instruction. Now the ThraedPlanStepOverBreakpoint looks at the thread's stop reason, it's eStopReasonTrace (we've instruction stepped), and so it leaves its auto-continue to `true`. ThreadPlanStepOut sees that it has reached its goal StackID, removes its breakpoint, and says it is done. Thread::ShouldStop thinks the auto-continue == yes vote from ThreadPlanStepOverBreakpoint wins, and we lose control of the process. This patch changes ThreadPlanStepOut to require that *both* (1) we are at the StackID of the caller function, where we wanted to end up, and (2) we have actually hit the breakpoint that we inserted. This in effect means that now lldb instruction-steps over the breakpoint in the callee function, stops at the return address of the caller function. StepOverBreakpoint has completed. StepOut is still running, and we continue the thread again. We immediatley hit the breakpoint (that we're sitting at), and now ThreadPlanStepOut marks itself as completed, and we return control to the user. Jim suggests that ThreadPlanStepOverBreakpoint is a bit unusual because it's not something pushed on the stack by a higher-order thread plan that "owns" it, it is inserted by the Thread as it is about to resume, if we're at a BreakpointSite. It has no connection to the thread plans above it, but tries to set the auto-continue mode based on the state of the thread when it is inserted (and tries to detect an unexpected breakpoint and unset that auto-continue it previously decided on, because it now realizes it should not influence execution control any more). Instead maybe the ThreadPlanStepOverBreakpoint should be inserted as a child plan of whatever the lowest plan is on the stack at the point it is added. I added an API test that will catch this bug in the new thread breakpoint algorithm.
1 parent eb46056 commit a2f2417

File tree

4 files changed

+59
-2
lines changed

4 files changed

+59
-2
lines changed

lldb/source/Target/ThreadPlanStepOut.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,11 @@ bool ThreadPlanStepOut::ShouldStop(Event *event_ptr) {
364364
}
365365

366366
if (!done) {
367-
StackID frame_zero_id = GetThread().GetStackFrameAtIndex(0)->GetStackID();
368-
done = !(frame_zero_id < m_step_out_to_id);
367+
StopInfoSP stop_info_sp = GetPrivateStopInfo();
368+
if (stop_info && stop_info_sp->GetStopReason() == eStopReasonBreakpoint) {
369+
StackID frame_zero_id = GetThread().GetStackFrameAtIndex(0)->GetStackID();
370+
done = !(frame_zero_id < m_step_out_to_id);
371+
}
369372
}
370373

371374
// The normal step out computations think we are done, so all we need to do
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Test finish out of an empty function (may be one-instruction long)
3+
"""
4+
5+
import lldb
6+
from lldbsuite.test.decorators import *
7+
from lldbsuite.test.lldbtest import *
8+
from lldbsuite.test import lldbutil
9+
10+
11+
class FinishFromEmptyFunctionTestCase(TestBase):
12+
NO_DEBUG_INFO_TESTCASE = True
13+
14+
def test_finish_from_empty_function(self):
15+
"""Test that when stopped at a breakpoint in an empty function, finish leaves it correctly."""
16+
self.build()
17+
exe = self.getBuildArtifact("a.out")
18+
target, process, thread, _ = lldbutil.run_to_name_breakpoint(
19+
self, "done", exe_name=exe
20+
)
21+
if self.TraceOn():
22+
self.runCmd("bt")
23+
24+
correct_stepped_out_line = line_number("main.c", "leaving main")
25+
return_statement_line = line_number("main.c", "return 0")
26+
safety_bp = target.BreakpointCreateByLocation(
27+
lldb.SBFileSpec("main.c"), return_statement_line
28+
)
29+
self.assertTrue(safety_bp.IsValid())
30+
31+
error = lldb.SBError()
32+
thread.StepOut(error)
33+
self.assertTrue(error.Success())
34+
35+
if self.TraceOn():
36+
self.runCmd("bt")
37+
38+
frame = thread.GetSelectedFrame()
39+
self.assertEqual(
40+
frame.line_entry.GetLine(),
41+
correct_stepped_out_line,
42+
"Step-out lost control of execution, ran too far",
43+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <stdio.h>
2+
void done() {}
3+
int main() {
4+
puts("in main");
5+
done();
6+
puts("leaving main");
7+
return 0;
8+
}

0 commit comments

Comments
 (0)