https://github.com/qiyao created 
https://github.com/llvm/llvm-project/pull/216165

debugserver expedites memory in two replies and no test asserts any of it is
there.

`test_stop_reply_expedites_frame_pointer_backchain` checks the stop reply: every
`memory:` entry carries `2 * ptrsize` bytes, and there are at most two, the cap
that keeps the reply small.  The count is not pinned to exactly two, how far the
walk gets depends on where the backchain terminates.

`test_threads_info_expedites_stopped_frame_stack` checks `jThreadsInfo` by chunk
size: every thread carries at least one backchain entry, the stopped thread
carries one or two chunks that are not, and no other thread carries any.  The
addresses are not checked, debugserver anchors frame 0's window at `$fp` or at
`$sp` depending on the inferior's prologue.

Sizes come from `qProcessInfo`, not a hard-coded 16, so they hold on a 32-bit
target.  `gather_threads_info` is split out of `gather_threads_info_pcs`.
debugserver only.


>From eb76a2b053a35b66fed6c84e77c382247f85b6d9 Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Thu, 13 Aug 2026 19:01:31 +0100
Subject: [PATCH] [lldb][test] Check debugserver's expedited memory in its
 replies

debugserver expedites memory in two replies and no test asserts any of it is
there.

`test_stop_reply_expedites_frame_pointer_backchain` checks the stop reply: every
`memory:` entry carries `2 * ptrsize` bytes, and there are at most two, the cap
that keeps the reply small.  The count is not pinned to exactly two, how far the
walk gets depends on where the backchain terminates.

`test_threads_info_expedites_stopped_frame_stack` checks `jThreadsInfo` by chunk
size: every thread carries at least one backchain entry, the stopped thread
carries one or two chunks that are not, and no other thread carries any.  The
addresses are not checked, debugserver anchors frame 0's window at `$fp` or at
`$sp` depending on the inferior's prologue.

Sizes come from `qProcessInfo`, not a hard-coded 16, so they hold on a 32-bit
target.  `gather_threads_info` is split out of `gather_threads_info_pcs`.
debugserver only.
---
 .../TestGdbRemoteThreadsInStopReply.py        | 82 ++++++++++++++++++-
 1 file changed, 78 insertions(+), 4 deletions(-)

diff --git a/lldb/test/API/tools/lldb-server/TestGdbRemoteThreadsInStopReply.py 
b/lldb/test/API/tools/lldb-server/TestGdbRemoteThreadsInStopReply.py
index d0bbd70329354..dc568c277e63c 100644
--- a/lldb/test/API/tools/lldb-server/TestGdbRemoteThreadsInStopReply.py
+++ b/lldb/test/API/tools/lldb-server/TestGdbRemoteThreadsInStopReply.py
@@ -34,6 +34,7 @@ def gather_stop_reply_fields(self, thread_count, field_names):
         result = dict()
         result["pc_register"] = hw_info["pc_register"]
         result["little_endian"] = hw_info["little_endian"]
+        result["ptrsize"] = hw_info["ptrsize"]
         for key_field in field_names:
             result[key_field] = kv_dict.get(key_field)
 
@@ -85,9 +86,11 @@ def parse_hw_info(self, context):
         hw_info = dict()
         hw_info["pc_register"] = pc_lldb_reg_index
         hw_info["little_endian"] = endian == "little"
+        hw_info["ptrsize"] = int(process_info.get("ptrsize", 0))
         return hw_info
 
-    def gather_threads_info_pcs(self, pc_register, little_endian):
+    def gather_threads_info(self):
+        """The parsed jThreadsInfo reply: one dict per thread."""
         self.reset_test_sequence()
         self.test_sequence.add_log_lines(
             [
@@ -104,10 +107,12 @@ def gather_threads_info_pcs(self, pc_register, 
little_endian):
         context = self.expect_gdbremote_sequence()
         self.assertIsNotNone(context)
         threads_info = context.get("threads_info")
+        # A literal '}' is escaped as '}]' on the wire.
+        return json.loads(re.sub(r"}]", "}", threads_info))
+
+    def gather_threads_info_pcs(self, pc_register, little_endian):
+        jthreads_info = self.gather_threads_info()
         register = str(pc_register)
-        # The jThreadsInfo response is not valid JSON data, so we have to
-        # clean it up first.
-        jthreads_info = json.loads(re.sub(r"}]", "}", threads_info))
         thread_pcs = dict()
         for thread_info in jthreads_info:
             tid = thread_info["tid"]
@@ -176,6 +181,75 @@ def test_stop_reply_reports_correct_threads(self):
         for tid in threads:
             self.assertIn(tid, stop_reply_threads)
 
+    @add_test_categories(["debugserver"])
+    def test_stop_reply_expedites_frame_pointer_backchain(self):
+        """The stop reply expedites at most the two innermost backchain
+        entries, one `memory:` entry each of 2 * ptrsize bytes."""
+        self.build()
+        self.set_inferior_startup_launch()
+        results = self.gather_stop_reply_fields(1, ["memory"])
+
+        memory = results["memory"]
+        self.assertIsNotNone(memory, "the stop reply carried no memory: entry")
+        # parse_key_val_dict promotes a repeated key to a list.
+        entries = memory if isinstance(memory, list) else [memory]
+
+        # A backchain entry is 2 pointers at $fp: the previous FP and PC.
+        backchain_entry_size = 2 * results["ptrsize"]
+        for entry in entries:
+            addr, sep, hex_bytes = entry.partition("=")
+            self.assertEqual(sep, "=", "malformed memory entry %r" % entry)
+            self.assertEqual(
+                len(hex_bytes),
+                2 * backchain_entry_size,
+                "memory:%s= should carry %d bytes (2 * ptrsize), got %d: %r"
+                % (addr, backchain_entry_size, len(hex_bytes) // 2, entry),
+            )
+        # The stop reply caps the backchain at 2 entries.  How many it actually
+        # walks depends on where the backchain terminates.
+        self.assertLessEqual(len(entries), 2, "expected at most 2 backchain 
entries")
+
+    @add_test_categories(["debugserver"])
+    def test_threads_info_expedites_stopped_frame_stack(self):
+        """jThreadsInfo expedites every thread's frame pointer backchain, plus
+        frame 0's stack memory for the stopped thread only."""
+        self.build()
+        self.set_inferior_startup_launch()
+        results = self.gather_stop_reply_fields(5, ["thread"])
+        backchain_entry_size = 2 * results["ptrsize"]
+        stopped_tid = int(results["thread"], 16)
+
+        # Chunk sizes tell the two apart: a backchain entry is 2 pointers,
+        # frame 0's stack memory is a larger range.
+        def classify(thread_info):
+            sizes = [len(m["bytes"]) // 2 for m in thread_info.get("memory", 
[])]
+            stack_backchain = [n for n in sizes if n == backchain_entry_size]
+            frame_0_stack_memory = [n for n in sizes if n != 
backchain_entry_size]
+            return sizes, stack_backchain, frame_0_stack_memory
+
+        saw_stopped = False
+        for thread_info in self.gather_threads_info():
+            sizes, stack_backchain, frame_0_stack_memory = 
classify(thread_info)
+            where = "thread %x, chunk sizes %s" % (thread_info["tid"], sizes)
+            self.assertGreaterEqual(
+                len(stack_backchain), 1, "no backchain entry for %s" % where
+            )
+            if thread_info["tid"] == stopped_tid:
+                saw_stopped = True
+                # One chunk, or two when $fp is usable.
+                self.assertIn(
+                    len(frame_0_stack_memory),
+                    (1, 2),
+                    "no frame 0 stack memory for stopped %s" % where,
+                )
+            else:
+                self.assertEqual(
+                    frame_0_stack_memory,
+                    [],
+                    "unexpected frame 0 stack memory for %s" % where,
+                )
+        self.assertTrue(saw_stopped, "no jThreadsInfo entry for the stopped 
thread")
+
     @skipIfNetBSD
     @skipIfWindows  # Flaky on Windows
     def test_stop_reply_contains_thread_pcs(self):

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to