================
@@ -0,0 +1,281 @@
+"""
+Tests about which gdb-remote packets lldb sends while inspecting the stack at a
+public stop.
+
+On Darwin, debugserver expedites the frame-pointer backchain (up to 256 frames,
+for every thread) in the jThreadsInfo response at a public stop, and lldb seeds
+those bytes into its memory cache. Consequences exercised here:
+
+ * A backtrace (GetNumFrames() / GetFrameAtIndex() for every frame) is
+ satisfied entirely from the expedited/cached backchain and sends no
packets.
+ With the cache disabled it must read the backchain frame by frame, which
+ confirms the test is really exercising the unwinder's memory reads.
+
+ * Examining frame local variables the way an IDE does is NOT covered by the
+ expedite: the values live at addresses that were never sent up, so reading
+ them produces memory-read packets. This is checked two ways, mirroring
+ an IDE: examining only the selected frame's locals (frame 0, what a
+ variables view does on a stop) and examining every frame's locals (the
+ "view all frames" case).
+"""
+
+import os
+import re
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestExpeditedStackMemory(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ # A gdb-remote memory read is "$x<addr>,<len>" or "$m<addr>,<len>"; the
comma
+ # after the hex address distinguishes them from other packets.
+ MEM_READ_RE = re.compile(r"send packet: \$[xm][0-9a-fA-F]+,[0-9a-fA-F]+")
+ READ_RANGE_RE = re.compile(r"send packet:
\$[xm]([0-9a-fA-F]+),([0-9a-fA-F]+)")
+
+ # Must match HEAP_COUNT in main.c.
+ HEAP_COUNT = 8
+
+ @skipUnlessDarwin
+ def test_no_packets_during_backtrace(self):
+ """With the memory cache on, the backtrace sends no packets at all."""
+ self.check_packets_during_backtrace(disable_memory_cache=False)
+
+ @skipUnlessDarwin
+ def test_memory_reads_during_backtrace_without_cache(self):
+ """With the memory cache off, the backtrace reads the backchain from
the
+ stub, producing memory-read packets."""
+ self.check_packets_during_backtrace(disable_memory_cache=True)
+
+ @skipUnlessDarwin
+ def test_memory_reads_when_examining_frame0_locals(self):
+ """Model an IDE stop: walk the whole stack (a backtrace / debug
+ navigator) but examine the locals of only the selected frame 0.
+ Frame 0 (func_e in main.c) carries scalar, aggregate, and
+ pointer-to-heap locals, so examining it alone reads both stack and
+ heap memory."""
+ self.check_memory_reads_when_examining_locals(examine_all_frames=False)
+
+ @skipUnlessDarwin
+ def test_memory_reads_when_examining_all_frames_locals(self):
+ """Model "view all frames": walk the whole stack and examine every
+ frame's locals. This reads the same variety of memory across several
+ frames."""
+ self.check_memory_reads_when_examining_locals(examine_all_frames=True)
+
+ def check_memory_reads_when_examining_locals(self, examine_all_frames):
+ """Examining frame locals reads value memory that is not expedited.
+ Classify those reads into stack vs heap and check the counts.
+
+ The frame-pointer backchain is expedited, but the locals' *values* are
+ not, so both stack-resident locals and heap buffers behind pointers are
+ read from the stub today.
+
+ We know from main.c which storage is stack and which is heap, so we
+ classify each read by the variables' own addresses."""
+ stack_addrs = []
+ heap_ranges = []
+
+ def per_frame(idx, frame):
+ # An IDE always walks the full stack for the backtrace, but only
+ # examines the locals of the selected frame (frame 0) unless the
user
+ # asks to view all frames.
+ if examine_all_frames or idx == 0:
+ self.examine_locals(frame, stack_addrs, heap_ranges)
+
+ sent = self.walk_stack(per_frame, disable_memory_cache=False)
+
+ self.assertTrue(stack_addrs, "expected to find stack-resident locals")
+ self.assertTrue(heap_ranges, "expected to find the 'heap' pointer
local")
+
+ reads = self.read_ranges(sent)
+ heap_reads = [r for r in reads if any(self.overlaps(r, h) for h in
heap_ranges)]
+ stack_reads = [
+ r
+ for r in reads
+ if r not in heap_reads and any(self.contains(r, a) for a in
stack_addrs)
+ ]
+ other_reads = [r for r in reads if r not in heap_reads and r not in
stack_reads]
+
+ breakdown = (
+ "memory reads while examining locals: "
+ "stack=%d heap=%d other=%d (total=%d)\n"
+ " heap ranges: %s\n"
+ " stack reads: %s\n"
+ " heap reads: %s\n"
+ " other reads: %s"
+ % (
+ len(stack_reads),
+ len(heap_reads),
+ len(other_reads),
+ len(reads),
+ ", ".join("[0x%x,0x%x)" % h for h in heap_ranges),
+ ", ".join("[0x%x,0x%x)" % r for r in stack_reads),
+ ", ".join("[0x%x,0x%x)" % r for r in heap_reads),
+ ", ".join("[0x%x,0x%x)" % r for r in other_reads),
+ )
+ )
+
+ # Examining locals reads both stack and heap memory.
+ self.assertGreater(
+ len(stack_reads),
+ 0,
+ "expected stack memory reads while examining stack-resident "
+ "locals.\n" + breakdown,
+ )
+ # Heap reads come from disclosing the pointer-to-heap local; a stack
+ # expedite would NOT remove these.
+ self.assertGreater(
+ len(heap_reads),
+ 0,
+ "expected a heap memory read from disclosing the 'heap' pointer.\n"
+ + breakdown,
+ )
+
+ # ---- shared scaffolding -------------------------------------------------
+
+ def walk_stack(self, per_frame_fn, disable_memory_cache):
+ """Stop at the breakpoint, then walk every frame calling
+ per_frame_fn(index, frame) on each. Returns the list of 'send packet:'
+ log lines emitted during the walk (the backtrace window)."""
+ self.build()
+ logfile = os.path.join(
+ self.getBuildDir(),
+ "stack-walk-packets-" + self.getArchitecture() + ".txt",
+ )
+
+ # Enable packet logging from the start, so we can window the log by
byte
+ # offset around the walk. (The log file is unbuffered, so the size on
+ # disk reflects every packet written so far.)
+ self.runCmd("log enable -f %s gdb-remote packets" % logfile)
+
+ def cleanup():
+ self.runCmd("log disable gdb-remote packets")
+ if os.path.exists(logfile):
+ os.unlink(logfile)
+
+ self.addTearDownHook(cleanup)
+
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec("main.c")
+ )
+
+ # The public stop has happened, the backchain is now in the cache.
+ # Configure whether the upcoming walk is allowed to use that cache,
then
+ # mark where the log is before we touch the stack.
+ self.runCmd(
+ "settings set target.process.disable-memory-cache %s"
+ % ("true" if disable_memory_cache else "false")
+ )
+ self.assertTrue(os.path.exists(logfile), "packet log was created")
+ pos_before = os.path.getsize(logfile)
+
+ num_frames = thread.GetNumFrames()
+ self.assertGreater(
+ num_frames, 5, "the nested call chain should produce several
frames"
+ )
+ for i in range(num_frames):
+ frame = thread.GetFrameAtIndex(i)
+ self.assertTrue(frame.IsValid())
+ per_frame_fn(i, frame)
+
+ pos_after = os.path.getsize(logfile)
+
+ with open(logfile, "r") as f:
+ f.seek(pos_before)
+ window = f.read(pos_after - pos_before)
+
+ return [line.strip() for line in window.splitlines() if "send packet:"
in line]
+
+ def check_packets_during_backtrace(self, disable_memory_cache):
+ # An IDE-style backtrace: force a full unwind and resolve each frame's
+ # symbol/line/module info. Deliberately no variable APIs.
+ def resolve_only(idx, frame):
+ frame.GetFunctionName()
+ frame.GetLineEntry()
+ frame.GetModule()
+
+ sent = self.walk_stack(resolve_only, disable_memory_cache)
+ mem_reads = [line for line in sent if self.MEM_READ_RE.search(line)]
+
+ if disable_memory_cache:
+ # The unwinder must fetch the backchain from the stub frame by
frame.
+ self.assertNotEqual(
+ mem_reads,
+ [],
+ "with the memory cache disabled the backtrace should read
stack "
+ "memory from the stub, but no memory-read packets were sent",
+ )
+ else:
+ # The expedited / cached backchain satisfies the whole walk.
+ self.assertEqual(
+ sent,
+ [],
+ "the backtrace should send no packets to the stub "
+ "(the frame-pointer backchain is expedited and cached); "
+ "unexpected packets during the walk:\n %s" % "\n
".join(sent),
+ )
+
+ def examine_locals(self, frame, stack_addrs, heap_ranges):
+ """Read a frame's locals/arguments: GetVariables() then
+ per value GetValue()/GetSummary()/GetValueDidChange(), disclosing one
+ level of children for aggregates and pointers.
+
+ Records, for classifying the resulting memory reads:
+ * stack_addrs: the load address of each local (its stack storage),
so a
+ read covering one of these addresses is a stack read.
+ * heap_ranges: the address range of the 'heap' pointer's buffer, so a
+ read overlapping it is a heap read."""
+ values = frame.GetVariables(
+ True, True, False, True, lldb.eDynamicCanRunTarget
+ ) # arguments, locals, statics, in_scope_only, use_dynamic (as an IDE
does)
+ for i in range(values.GetSize()):
+ v = values.GetValueAtIndex(i)
+ if not v.IsValid():
+ continue
+
+ # The variable's own storage is on the stack (we wrote main.c).
+ addr = v.GetLoadAddress()
+ if addr != lldb.LLDB_INVALID_ADDRESS:
+ stack_addrs.append(addr)
+
+ v.GetValue()
+ v.GetSummary()
+ v.GetValueDidChange()
+
+ # Disclose one level, as a turned-down aggregate/pointer row would.
+ if v.MightHaveChildren():
+ for c_idx in range(min(v.GetNumChildren(), 16)):
+ child = v.GetChildAtIndex(c_idx,
lldb.eDynamicCanRunTarget, False)
+ if child.IsValid():
+ child.GetValue()
+
+ # Remember the heap buffer's range so the caller can classify reads
+ # that hit heap (vs stack) memory.
+ if v.GetName() == "heap" and v.GetType().IsPointerType():
+ err = lldb.SBError()
+ ptr = v.GetValueAsUnsigned(err, 0)
+ if err.Success() and ptr != 0:
+ heap_ranges.append((ptr, ptr + self.HEAP_COUNT * 8))
+
+ @classmethod
+ def read_ranges(cls, sent):
----------------
jimingham wrote:
We probably already have code that does this in gdbclientutils.py. If we
don't, it belongs there.
https://github.com/llvm/llvm-project/pull/205897
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits