This is an archive of the discontinued LLVM Phabricator instance.

[lldb] Consider all breakpoints in breakpoint detection
ClosedPublic

Authored by kpdev42 on Dec 19 2022, 8:41 PM.

Details

Summary

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

Diff Detail

Event Timeline

kpdev42 created this revision.Dec 19 2022, 8:41 PM
Herald added a project: Restricted Project. · View Herald TranscriptDec 19 2022, 8:41 PM
kpdev42 requested review of this revision.Dec 19 2022, 8:41 PM
DavidSpickett added a subscriber: jingham.

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?

kpdev42 updated this revision to Diff 484745.Dec 21 2022, 11:26 PM

Renamed the test, added more tests for unconditional (enabled/disabled) breakpoints and breakpoints with callbacks

kpdev42 edited the summary of this revision. (Show Details)Dec 21 2022, 11:27 PM

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?

Fixed, please take a look

Jim Ingham really should chime in on this if possible as the thread plans and stop info are his area of expertise.

@jingham
May I kindly ask you to take a look at this patch?

jingham requested changes to this revision.EditedJan 9 2023, 6:24 PM

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.

This revision now requires changes to proceed.Jan 9 2023, 6:24 PM
kpdev42 updated this revision to Diff 488154.Jan 11 2023, 4:30 AM

Renaming and cleanup according to review

kpdev42 marked 5 inline comments as done.Jan 11 2023, 4:33 AM
jingham accepted this revision.Jan 11 2023, 11:23 AM

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.

This revision is now accepted and ready to land.Jan 11 2023, 11:23 AM
This revision was automatically updated to reflect the committed changes.