[Lldb-commits] [lldb] [lldb] Put the thread until backstop in the caller of the concrete frame (PR #226075)

via lldb-commits lldb-commits at lists.llvm.org
Thu Sep 24 01:55:45 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Felipe de Azevedo Piovezan (felipepiovezan)

<details>
<summary>Changes</summary>

This commit fixes a bug with thread-until, and makes LLDB's behavior similar to GDB's in code with inlined functions.

For example, consider this backtrace:
```
      frame #<!-- -->0: 0x00000001000003c0 deep`sink(x=81) at deep.c:4:6 [opt]
      frame #<!-- -->1: 0x0000000100000418 deep`level3(a=48) at deep.c:11:3 [opt] [inlined]
      frame #<!-- -->2: 0x0000000100000404 deep`level2(b=37) at deep.c:20:3 [opt] [inlined]
      frame #<!-- -->3: 0x00000001000003f0 deep`level1(c=42) at deep.c:28:3 [opt] [inlined]
      frame #<!-- -->4: 0x00000001000003dc deep`main at deep.c:33:3 [opt]
```
This patch changes behavior as follows:

```
command                             gdb           lldb before    lldb after
until -f 1 <later line in frame 2>  that line     process exits  that line
until -f 2 <later line in frame 1>  that line     process exits  that line
until -f 1 <line that already ran>  main returns  process exits  main returns
```

TestInlineFrameUntil covers these cases.

`thread until -f N` put its return breakpoint at the PC of frame N+1. When frame N+1 is an inline frame, that PC is the start of the inlined code of frame N, which already ran; in other words, this breakpoint will never be hit and the process runs to exit.

Also, an until breakpoint counted as reached only in the inline scope of frame N, so a target in another inline scope of the same concrete frame was ignored.

The return breakpoint now goes in the first frame after N that is in a different concrete frame. An until breakpoint now counts as reached when frame zero has the CFA of frame N.

---
Full diff: https://github.com/llvm/llvm-project/pull/226075.diff


4 Files Affected:

- (modified) lldb/source/Target/ThreadPlanStepUntil.cpp (+13-3) 
- (added) lldb/test/API/functionalities/inline-frame-pc/Makefile (+3) 
- (added) lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py (+38) 
- (added) lldb/test/API/functionalities/inline-frame-pc/main.c (+28) 


``````````diff
diff --git a/lldb/source/Target/ThreadPlanStepUntil.cpp b/lldb/source/Target/ThreadPlanStepUntil.cpp
index 11def7a304f97..89b35cd993968 100644
--- a/lldb/source/Target/ThreadPlanStepUntil.cpp
+++ b/lldb/source/Target/ThreadPlanStepUntil.cpp
@@ -43,9 +43,17 @@ ThreadPlanStepUntil::ThreadPlanStepUntil(Thread &thread,
     // Find the return address and set a breakpoint there:
     // FIXME - can we do this more securely if we know first_insn?
 
-    StackFrameSP return_frame_sp(thread.GetStackFrameAtIndex(frame_idx + 1));
+    // Inline frames have the PC of their concrete frame, so the return
+    // breakpoint goes in the caller of the concrete frame. Artificial frames
+    // share the concrete frame index of the frame they return to.
+    uint32_t return_frame_idx = frame_idx + 1;
+    StackFrameSP return_frame_sp =
+        thread.GetStackFrameAtIndex(return_frame_idx);
+    while (return_frame_sp && !frame_sp->IsArtificial() &&
+           return_frame_sp->GetConcreteFrameIndex() ==
+               frame_sp->GetConcreteFrameIndex())
+      return_frame_sp = thread.GetStackFrameAtIndex(++return_frame_idx);
     if (return_frame_sp) {
-      // TODO: add inline functionality
       m_return_addr = return_frame_sp->GetStackID().GetPC();
       Breakpoint *return_bp =
           target_sp->CreateBreakpoint(m_return_addr, true, false).get();
@@ -197,7 +205,9 @@ void ThreadPlanStepUntil::AnalyzeStop() {
             StackID frame_zero_id =
                 thread.GetStackFrameAtIndex(0)->GetStackID();
 
-            if (frame_zero_id == m_stack_id)
+            // Inline frames have the CFA of their concrete frame.
+            if (frame_zero_id.GetCallFrameAddressWithoutMetadata() ==
+                m_stack_id.GetCallFrameAddressWithoutMetadata())
               done = true;
             else if (frame_zero_id.IsYoungerThan(m_stack_id))
               done = false;
diff --git a/lldb/test/API/functionalities/inline-frame-pc/Makefile b/lldb/test/API/functionalities/inline-frame-pc/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/inline-frame-pc/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py b/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
new file mode 100644
index 0000000000000..0bfb74f7816af
--- /dev/null
+++ b/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
@@ -0,0 +1,38 @@
+"""
+Test thread until from an inline frame.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestInlineFrameUntil(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def until_from_frame(self, frame_idx, marker):
+        self.build()
+        _, _, thread, _ = lldbutil.run_to_name_breakpoint(self, "sink")
+        self.runCmd(f"thread until -f {frame_idx} {line_number('main.c', marker)}")
+        return thread.GetFrameAtIndex(0)
+
+    def check_stops_at(self, frame_idx, marker):
+        frame = self.until_from_frame(frame_idx, marker)
+        self.assertEqual(frame.GetLineEntry().GetLine(), line_number("main.c", marker))
+
+    def test_until_in_same_inline_frame(self):
+        self.check_stops_at(1, "// until in level3")
+
+    def test_until_in_parent_inline_frame(self):
+        self.check_stops_at(2, "// until in level2")
+
+    def test_until_in_inlining_caller(self):
+        self.check_stops_at(1, "// until in level2")
+
+    def test_until_in_inlined_callee(self):
+        self.check_stops_at(2, "// until in level3")
+
+    def test_until_target_already_ran(self):
+        frame = self.until_from_frame(1, "// before sink")
+        self.assertEqual(frame.GetFunctionName(), "main")
diff --git a/lldb/test/API/functionalities/inline-frame-pc/main.c b/lldb/test/API/functionalities/inline-frame-pc/main.c
new file mode 100644
index 0000000000000..b9e765f204945
--- /dev/null
+++ b/lldb/test/API/functionalities/inline-frame-pc/main.c
@@ -0,0 +1,28 @@
+volatile int g;
+
+__attribute__((noinline)) void sink(int x) { g = x; }
+
+static inline __attribute__((always_inline)) void level3(int a) {
+  g = a; // before sink
+  sink(a);
+  g = a + 1; // break in level3
+  g = a + 2; // until in level3
+}
+
+static inline __attribute__((always_inline)) void level2(int b) {
+  g = b;
+  level3(b + 1);
+  g = b + 1; // until in level2
+}
+
+static inline __attribute__((always_inline)) void level1(int c) {
+  g = c;
+  level2(c + 1);
+}
+
+__attribute__((noinline)) void outer(void) { level1(42); }
+
+int main(void) {
+  outer();
+  return 0;
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/226075


More information about the lldb-commits mailing list