Currently in some cases lldb reports stop reason as "step out" or "step over" (from thread plan completion) instead of "breakpoint", if the user breakpoint happens to be set on the same address.
The part of https://github.com/llvm/llvm-project/commit/f08f5c99262ff9eaa08956334accbb2614b0f7a2 seems to overwrite internal breakpoint detection logic, so that only the last breakpoint for the current stop address is considered.
Together with step-out plans not clearing its breakpoint until they are destrouyed, this creates a situation when there is a user breakpoint set for address, but internal breakpoint makes lldb report a plan completion stop reason instead of breakpoint.
This patch reverts that internal breakpoint detection logic to consider all breakpoints
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
The intent makes sense. We should stop and report user breakpoints triggered while trying to execute some internal stepping plan, even if they overlap what lldb was planning to do in the first place.
Not totally sure how the change achieves that, this is quite the function. + @jingham who wrote the original changes.
Currently in some cases lldb reports stop reason as "step out" or "step over" (from thread plan completion) over "breakpoint"
This would be clearer if you said "(from thread plan completion) instead of "breakpoint"". Took me a while to work out that it wasn't over meaning step over a breakpoint.
I think the test naming could be clearer. breakpoint/step_out_breakpoint/TestStepOutBreakpoint.py implies it's just about stepping out. How about breakpoint/thread_plan_user_breakpoint/TestThreadPlanUserBreakpoint.py ? Something that is clear we're testing the interaction of automatic internal stepping plans and breakpoints the user puts in.
Is it worth checking that an unconditional user breakpoint is also reported?
lldb/source/Target/StopInfo.cpp | ||
---|---|---|
2 | I think the key here is the m_should_stop check (the rest looks equivalent to what is there already). What exactly does that achieve? |
Renamed the test, added more tests for unconditional (enabled/disabled) breakpoints and breakpoints with callbacks
Jim Ingham really should chime in on this if possible as the thread plans and stop info are his area of expertise.
I think this patch is correct, but could be clearer - mostly because the original code didn't choose good enough names.
What's going on here is that the "internal_breakpoint" variable is getting used in two ways. In the loop over breakpoint locations we're using it to cache the result of IsInternal() for the current breakpoint location. But then outside the loop we really want it to mean "were all the breakpoint locations that said we should stop internal breakpoints?", which gets used in:
if ((!m_should_stop || internal_breakpoint) && thread_sp->CompletedPlanOverridesBreakpoint()) {
The two are equivalent when there aren't other active & matching breakpoints at the same site, but not otherwise, so you're right we need to fix that.
Your patch eliminates the first use and makes internal_breakpoint just follow the outer meaning. That's fine, but then internal_breakpoint ends up being a pretty confusing name - given we've just iterated over a bunch of breakpoints. Instead, it should have a name that indicates it is a value computed from ALL the breakpoint locations we looked at, something like all_stopping_locs_internal. That starts as true, and if you see any non-internal breakpoint say we should stop, then turning this to false will make total sense. And the outer check will also make more sense, it will be:
if ((!m_should_stop || all_stopping_locs_internal) && thread_sp->CompletedPlanOverridesBreakpoint()) {
BTW, I'm not insisting on this particular name, if you can think of a clearer one, that's fine too. But internal_breakpoint is confusing.
I also had a couple of clean-up suggestions for the test. These should be pretty simple cleanups.
lldb/test/API/functionalities/breakpoint/thread_plan_user_breakpoint/TestThreadPlanUserBreakpoint.py | ||
---|---|---|
32 | It's easier to use BreakpointCreateBySourceRegex for this, since you can do the search & set in one step. | |
35 | Why do you only check one of the breakpoints? | |
40 | You are making three breakpoints, but really the one in main is the "stop to start the test breakpoint" and the other two are the ones for the test. So this would all be more compact if you did: (target, process, thread, _) = lldbutil.run_to_source_breakpoint(self, "breakpoint_0", lldb.SBFileSpec("main.cpp") That will do all the jobs of making the target, setting the initial breakpoint, starting a process, and making sure that you hit the initial breakpoint. Then you can just make the other two breakpoints and continue on as you have done here. | |
76 | Step out ALWAYS stops directly on returning to the caller frame. You need that so that if you have: foo(bar(), baz()); and are stopped in bar below this code, then step-out followed by step-in will land you in baz. If step-out finished the source line, you would be past the call to baz before you got control back. The only architecture dependency here is whether the compiler needed to emit more code for a given source line after the return from the callee. |
LGTM, thanks for figuring this out!
lldb/source/Target/StopInfo.cpp | ||
---|---|---|
2 | The point is that once we've looked at all the locations that were in the site we stopped at, if all the breakpoints that said we should stop here were internal, then we must be implementing a thread plan and we should give the thread plan a chance to set the stop info. But if there were any non-internal breakpoints that say we should stop, then we should report the breakpoint stop reason instead. |
I think the key here is the m_should_stop check (the rest looks equivalent to what is there already). What exactly does that achieve?