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

Felipe de Azevedo Piovezan via lldb-commits lldb-commits at lists.llvm.org
Thu Sep 24 01:54:55 PDT 2026


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

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.

>From 292af005a272aab971fbbca0583c616e1ffadf80 Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Wed, 23 Sep 2026 17:29:12 +0100
Subject: [PATCH] [lldb] Put the thread until backstop in the caller of the
 concrete frame

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.
---
 lldb/source/Target/ThreadPlanStepUntil.cpp    | 16 ++++++--
 .../functionalities/inline-frame-pc/Makefile  |  3 ++
 .../inline-frame-pc/TestInlineFrameUntil.py   | 38 +++++++++++++++++++
 .../functionalities/inline-frame-pc/main.c    | 28 ++++++++++++++
 4 files changed, 82 insertions(+), 3 deletions(-)
 create mode 100644 lldb/test/API/functionalities/inline-frame-pc/Makefile
 create mode 100644 lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
 create mode 100644 lldb/test/API/functionalities/inline-frame-pc/main.c

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;
+}



More information about the lldb-commits mailing list