https://github.com/qiyao updated https://github.com/llvm/llvm-project/pull/205897
>From 81cb3df4b3f9a290f876f28af5f6c5b0b7b1570b Mon Sep 17 00:00:00 2001 From: Yao Qi <[email protected]> Date: Wed, 24 Jun 2026 17:34:40 +0100 Subject: [PATCH 1/3] [lldb][test] Add expedited-stack-memory backtrace/locals packet test Add a Darwin API test that checks which gdb-remote packets lldb sends while inspecting the stack at a public stop, using the gdb-remote packet log (the same approach as TestExpeditedThreadPCs.py). debugserver expedites the frame-pointer backchain in the `jThreadsInfo` reply at a public stop and lldb caches it, so: - A backtrace (`GetNumFrames` + `GetFrameAtIndex` over every frame) sends no packets at all. With the memory cache disabled it must read the backchain frame by frame, which confirms the test exercises the unwinder's memory reads. - Examining frame locals is not covered by the expedite, so it reads value memory. The reads are classified as stack vs heap using the variables' own addresses (known from main.c). Locals are examined two ways, modeling how an IDE inspects values at a stop: walking the whole stack but examining only the selected frame's locals (frame 0, as a variables view does on a stop), and examining every frame's locals (a "view all frames" mode). The stopped frame (`func_e`) carries scalar, aggregate, and pointer-to-heap locals so frame 0 alone reads both stack and heap memory; the outer frames keep locals so the all-frames case reads the same variety across several frames. Values are read with `use_dynamic=eDynamicCanRunTarget` to match how an IDE reads them. --- .../macosx/expedited-stack-memory/Makefile | 3 + .../TestExpeditedStackMemory.py | 281 ++++++++++++++++++ .../API/macosx/expedited-stack-memory/main.c | 86 ++++++ 3 files changed, 370 insertions(+) create mode 100644 lldb/test/API/macosx/expedited-stack-memory/Makefile create mode 100644 lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py create mode 100644 lldb/test/API/macosx/expedited-stack-memory/main.c diff --git a/lldb/test/API/macosx/expedited-stack-memory/Makefile b/lldb/test/API/macosx/expedited-stack-memory/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/macosx/expedited-stack-memory/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py new file mode 100644 index 0000000000000..7de5ca7b087f0 --- /dev/null +++ b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py @@ -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): + """Parse [addr, addr+len) ranges out of the memory-read packet lines.""" + ranges = [] + for line in sent: + m = cls.READ_RANGE_RE.search(line) + if m: + addr = int(m.group(1), 16) + length = int(m.group(2), 16) + ranges.append((addr, addr + length)) + return ranges + + @staticmethod + def overlaps(a, b): + return a[0] < b[1] and b[0] < a[1] + + @staticmethod + def contains(read_range, addr): + return read_range[0] <= addr < read_range[1] diff --git a/lldb/test/API/macosx/expedited-stack-memory/main.c b/lldb/test/API/macosx/expedited-stack-memory/main.c new file mode 100644 index 0000000000000..c3eb9372520af --- /dev/null +++ b/lldb/test/API/macosx/expedited-stack-memory/main.c @@ -0,0 +1,86 @@ +#include <stdlib.h> + +// A simple, deterministic, single-threaded nested call chain. We stop at the +// innermost function and walk the stack. +// +// The breakpoint is in the innermost frame (func_e), and that frame carries +// locals of every kind, so that examining *just the stopped frame* already +// exercises the various memory-read paths in one frame: +// - scalar locals (int / long / double) +// - aggregate locals (a struct and a fixed stack array) +// - pointer locals, including a pointer to heap memory +// +// The outer frames (func_d / func_c) also carry locals of these kinds, so that +// walking the whole stack and examining every frame reads the same variety of +// memory across several frames. +// +// The frame-pointer backchain is expedited at the public stop, so the backtrace +// itself is packet-free; reading the *values* of these locals is not expedited +// and must read memory from the stub. +// +// noinline + a real use of every value keeps every frame and local live (no +// inlining, no tail-call folding, nothing optimized away). + +#define HEAP_COUNT 8 + +// A volatile sink so the locals are observably used. +volatile long g_sink; + +struct Stats { + long sum; + long min; + long max; + double mean; +}; + +// The innermost frame, where we stop. It carries every kind of local: a +// scalar, aggregates (struct + array), and pointers (including one into heap +// memory). Examining this single frame on a stop reads both stack and +// heap memory. +static int __attribute__((noinline)) func_e(int depth) { + int i = depth + 1; + long l = (long)depth * 1000; + double d = depth + 0.5; + struct Stats stats = {.sum = i, .min = i - 1, .max = i + 1, .mean = d}; + long arr[4] = {i, i + 1, i + 2, i + 3}; + long *heap = (long *)malloc(sizeof(long) * HEAP_COUNT); + for (int k = 0; k < HEAP_COUNT; ++k) + heap[k] = (long)i + k; + const char *str = "hello from func_e"; + int *self = &i; + g_sink = i + l + (long)d + stats.sum + arr[3] + heap[HEAP_COUNT - 1] + + str[0] + *self; // break here + int r = i + (int)l + (int)d + (int)stats.sum + (int)arr[3] + + (int)heap[HEAP_COUNT - 1] + str[0] + *self; + free(heap); + return r; +} + +// Aggregate locals: a struct and a fixed-size stack array. +static int __attribute__((noinline)) func_d(int x) { + struct Stats stats = {.sum = x, .min = x - 1, .max = x + 1, .mean = x + 0.5}; + long arr[4] = {x, x + 1, x + 2, x + 3}; + int r = func_e(x); + return r + (int)stats.sum + (int)arr[3]; +} + +// Pointer locals, including a pointer into heap memory. +static int __attribute__((noinline)) func_c(int x) { + long *heap = (long *)malloc(sizeof(long) * HEAP_COUNT); + for (int i = 0; i < HEAP_COUNT; ++i) + heap[i] = (long)x + i; + const char *str = "hello from func_c"; + int *self = &x; + int r = func_d(x); + r += (int)heap[HEAP_COUNT - 1] + (int)str[0] + *self; + free(heap); + return r; +} + +static int __attribute__((noinline)) func_b(int x) { return func_c(x) + 1; } +static int __attribute__((noinline)) func_a(int x) { return func_b(x) + 1; } + +int main() { + g_sink = func_a(0); + return 0; +} >From c2b17532156215480417fa00866c461a1d4ccba9 Mon Sep 17 00:00:00 2001 From: Yao Qi <[email protected]> Date: Wed, 1 Jul 2026 11:03:03 +0100 Subject: [PATCH 2/3] [lldb][test] Classify expedited-stack-memory reads by memory region Instead of guessing which reads are stack vs heap from a variable's name and assumed layout, ask the process's own memory map: the stack region is whichever region the stack pointer points into, and the heap region is whichever region func_e's malloc'd `heap` pointer points into. A read is then classified by which region it falls in. Also add a variable-length array local in `func_e` so the test exercises dynamically-sized stack storage (like `alloca`), which the region-based classification handles as a stack read. Reuse the new `parse_memory_read_packet` helper in `MockGDBServerResponder`'s m/x handlers. --- .../Python/lldbsuite/test/gdbclientutils.py | 56 +++++++- .../TestExpeditedStackMemory.py | 128 +++++++++--------- .../API/macosx/expedited-stack-memory/main.c | 24 ++-- 3 files changed, 130 insertions(+), 78 deletions(-) diff --git a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py index 4c40299f3256d..bb7dd21b443d1 100644 --- a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py +++ b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py @@ -2,6 +2,7 @@ import ctypes import errno import io +import re import threading import socket import traceback @@ -77,11 +78,62 @@ def hex_decode_bytes(hex_bytes): return out +def parse_memory_read_packet(packet): + """ + Parse a memory-read packet ("m<addr>,<len>" or "x<addr>,<len>") into its + (addr, length) integers, or return None if it isn't a memory read. + + Accepts the raw packet contents, with or without a leading "$". + """ + if packet and packet[0] == "$": + packet = packet[1:] + if not packet or packet[0] not in ("m", "x"): + return None + try: + addr, length = [int(x, 16) for x in packet[1:].split(",")] + except ValueError: + return None + return addr, length + + class PacketDirection(Enum): RECV = "recv" SEND = "send" +# A line in a "log enable gdb-remote packets" file, e.g. +# < 22> send packet: $x100000,40#ad +# < 6> read packet: $OK#9a +# The leading "<...>" size column is optional; the body is the framed packet +# between "$" and the "#" checksum. +_PACKET_LOG_RE = re.compile( + r"(?P<direction>send|read) packet: \$(?P<body>.*)#[0-9a-fA-F]{2}" +) + + +def parse_packet_log(lines): + """ + Parse the lines of a "log enable gdb-remote packets" file into a list of + (PacketDirection, body) tuples, where body is the packet contents. + + "send packet:" lines (client -> stub) map to PacketDirection.SEND and + "read packet:" lines (stub -> client) map to PacketDirection.RECV, matching + the perspective of the client whose log this is. + """ + out = [] + for line in lines: + m = _PACKET_LOG_RE.search(line) + if not m: + continue + direction = ( + PacketDirection.SEND + if m.group("direction") == "send" + else PacketDirection.RECV + ) + out.append((direction, m.group("body"))) + return out + + class PacketLog: def __init__(self): self._packets: list[tuple[PacketDirection, str]] = [] @@ -170,10 +222,10 @@ def _respond_impl(self, packet) -> Union[Response, List[Response]]: register, value = packet[1:].split("=") return self.writeRegister(int(register, 16), value) if packet[0] == "m": - addr, length = [int(x, 16) for x in packet[1:].split(",")] + addr, length = parse_memory_read_packet(packet) return self.readMemory(addr, length) if packet[0] == "x": - addr, length = [int(x, 16) for x in packet[1:].split(",")] + addr, length = parse_memory_read_packet(packet) return self.x(addr, length) if packet[0] == "M": location, encoded_data = packet[1:].split(":") diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py index 7de5ca7b087f0..e8c8076699af7 100644 --- a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py +++ b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py @@ -20,24 +20,20 @@ """ import os -import re import lldb from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbutil +from lldbsuite.test.gdbclientutils import ( + PacketDirection, + parse_memory_read_packet, + parse_packet_log, +) 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.""" @@ -73,36 +69,50 @@ def check_memory_reads_when_examining_locals(self, examine_all_frames): 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 = [] + We have two regions and ask the process which one each read falls in: + * the stack region: whichever region the stack pointer points into. + * the heap region: whichever region func_e's `heap` local points + into.""" 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) + self.examine_locals(frame) 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") + # The process is still stopped after the walk; consult its memory map to + # classify the reads we just provoked. Frame 0 is func_e (where we + # stopped), so both the stack pointer and the `heap` local live there. + process = self.dbg.GetSelectedTarget().GetProcess() + frame0 = process.GetSelectedThread().GetFrameAtIndex(0) + + stack_region = lldb.SBMemoryRegionInfo() + self.assertTrue( + process.GetMemoryRegionInfo(frame0.GetSP(), stack_region).Success(), + "could not locate the stack memory region", + ) + + heap_ptr = frame0.FindVariable("heap").GetValueAsUnsigned(0) + self.assertNotEqual(heap_ptr, 0, "could not read func_e's 'heap' pointer") + heap_region = lldb.SBMemoryRegionInfo() + self.assertTrue( + process.GetMemoryRegionInfo(heap_ptr, heap_region).Success(), + "could not locate the heap memory region", + ) 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] + stack_reads = [r for r in reads if self.in_region(r, stack_region)] + heap_reads = [r for r in reads if self.in_region(r, heap_region)] + other_reads = [r for r in reads if r not in stack_reads and r not in heap_reads] breakdown = ( "memory reads while examining locals: " "stack=%d heap=%d other=%d (total=%d)\n" - " heap ranges: %s\n" + " stack region: [0x%x,0x%x)\n" + " heap region: [0x%x,0x%x)\n" " stack reads: %s\n" " heap reads: %s\n" " other reads: %s" @@ -111,7 +121,10 @@ def per_frame(idx, frame): len(heap_reads), len(other_reads), len(reads), - ", ".join("[0x%x,0x%x)" % h for h in heap_ranges), + stack_region.GetRegionBase(), + stack_region.GetRegionEnd(), + heap_region.GetRegionBase(), + heap_region.GetRegionEnd(), ", ".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), @@ -138,8 +151,9 @@ def per_frame(idx, frame): 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).""" + per_frame_fn(index, frame) on each. Returns the unframed contents of + every packet lldb sent to the stub during the walk (the backtrace + window).""" self.build() logfile = os.path.join( self.getBuildDir(), @@ -187,7 +201,11 @@ def cleanup(): f.seek(pos_before) window = f.read(pos_after - pos_before) - return [line.strip() for line in window.splitlines() if "send packet:" in line] + return [ + body + for direction, body in parse_packet_log(window.splitlines()) + if direction == PacketDirection.SEND + ] def check_packets_during_backtrace(self, disable_memory_cache): # An IDE-style backtrace: force a full unwind and resolve each frame's @@ -198,7 +216,7 @@ def resolve_only(idx, frame): 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)] + mem_reads = [p for p in sent if parse_memory_read_packet(p) is not None] if disable_memory_cache: # The unwinder must fetch the backchain from the stub frame by frame. @@ -218,16 +236,13 @@ def resolve_only(idx, frame): "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. + def examine_locals(self, frame): + """Read a frame's locals/arguments the way an IDE does: 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.""" + This only provokes the memory reads (stack-resident values and the heap + buffers behind pointers); the caller classifies the resulting reads.""" values = frame.GetVariables( True, True, False, True, lldb.eDynamicCanRunTarget ) # arguments, locals, statics, in_scope_only, use_dynamic (as an IDE does) @@ -236,11 +251,6 @@ def examine_locals(self, frame, stack_addrs, heap_ranges): 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() @@ -252,30 +262,18 @@ def examine_locals(self, frame, stack_addrs, heap_ranges): 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): - """Parse [addr, addr+len) ranges out of the memory-read packet lines.""" + @staticmethod + def read_ranges(sent): + """Parse [addr, addr+len) ranges out of the memory-read packets.""" ranges = [] - for line in sent: - m = cls.READ_RANGE_RE.search(line) - if m: - addr = int(m.group(1), 16) - length = int(m.group(2), 16) + for packet in sent: + parsed = parse_memory_read_packet(packet) + if parsed: + addr, length = parsed ranges.append((addr, addr + length)) return ranges @staticmethod - def overlaps(a, b): - return a[0] < b[1] and b[0] < a[1] - - @staticmethod - def contains(read_range, addr): - return read_range[0] <= addr < read_range[1] + def in_region(read_range, region): + """True if the read starts inside the given memory region.""" + return region.GetRegionBase() <= read_range[0] < region.GetRegionEnd() diff --git a/lldb/test/API/macosx/expedited-stack-memory/main.c b/lldb/test/API/macosx/expedited-stack-memory/main.c index c3eb9372520af..9d83500738fcd 100644 --- a/lldb/test/API/macosx/expedited-stack-memory/main.c +++ b/lldb/test/API/macosx/expedited-stack-memory/main.c @@ -8,6 +8,7 @@ // exercises the various memory-read paths in one frame: // - scalar locals (int / long / double) // - aggregate locals (a struct and a fixed stack array) +// - a variable-length array (dynamically sized stack storage, like alloca) // - pointer locals, including a pointer to heap memory // // The outer frames (func_d / func_c) also carry locals of these kinds, so that @@ -17,9 +18,6 @@ // The frame-pointer backchain is expedited at the public stop, so the backtrace // itself is packet-free; reading the *values* of these locals is not expedited // and must read memory from the stub. -// -// noinline + a real use of every value keeps every frame and local live (no -// inlining, no tail-call folding, nothing optimized away). #define HEAP_COUNT 8 @@ -37,27 +35,31 @@ struct Stats { // scalar, aggregates (struct + array), and pointers (including one into heap // memory). Examining this single frame on a stop reads both stack and // heap memory. -static int __attribute__((noinline)) func_e(int depth) { +static int func_e(int depth) { int i = depth + 1; long l = (long)depth * 1000; double d = depth + 0.5; struct Stats stats = {.sum = i, .min = i - 1, .max = i + 1, .mean = d}; long arr[4] = {i, i + 1, i + 2, i + 3}; + int n = i + 4; // a runtime bound, so vla is a true variable-length array + long vla[n]; // dynamically sized stack storage (like alloca) + for (int k = 0; k < n; ++k) + vla[k] = (long)i - k; long *heap = (long *)malloc(sizeof(long) * HEAP_COUNT); for (int k = 0; k < HEAP_COUNT; ++k) heap[k] = (long)i + k; const char *str = "hello from func_e"; int *self = &i; - g_sink = i + l + (long)d + stats.sum + arr[3] + heap[HEAP_COUNT - 1] + - str[0] + *self; // break here - int r = i + (int)l + (int)d + (int)stats.sum + (int)arr[3] + + g_sink = i + l + (long)d + stats.sum + arr[3] + vla[n - 1] + + heap[HEAP_COUNT - 1] + str[0] + *self; // break here + int r = i + (int)l + (int)d + (int)stats.sum + (int)arr[3] + (int)vla[n - 1] + (int)heap[HEAP_COUNT - 1] + str[0] + *self; free(heap); return r; } // Aggregate locals: a struct and a fixed-size stack array. -static int __attribute__((noinline)) func_d(int x) { +static int func_d(int x) { struct Stats stats = {.sum = x, .min = x - 1, .max = x + 1, .mean = x + 0.5}; long arr[4] = {x, x + 1, x + 2, x + 3}; int r = func_e(x); @@ -65,7 +67,7 @@ static int __attribute__((noinline)) func_d(int x) { } // Pointer locals, including a pointer into heap memory. -static int __attribute__((noinline)) func_c(int x) { +static int func_c(int x) { long *heap = (long *)malloc(sizeof(long) * HEAP_COUNT); for (int i = 0; i < HEAP_COUNT; ++i) heap[i] = (long)x + i; @@ -77,8 +79,8 @@ static int __attribute__((noinline)) func_c(int x) { return r; } -static int __attribute__((noinline)) func_b(int x) { return func_c(x) + 1; } -static int __attribute__((noinline)) func_a(int x) { return func_b(x) + 1; } +static int func_b(int x) { return func_c(x) + 1; } +static int func_a(int x) { return func_b(x) + 1; } int main() { g_sink = func_a(0); >From 948d91e9562a18e1b77ea584128118e4fbca38e4 Mon Sep 17 00:00:00 2001 From: Yao Qi <[email protected]> Date: Wed, 1 Jul 2026 11:49:16 +0100 Subject: [PATCH 3/3] Update comments in main.c --- lldb/test/API/macosx/expedited-stack-memory/main.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lldb/test/API/macosx/expedited-stack-memory/main.c b/lldb/test/API/macosx/expedited-stack-memory/main.c index 9d83500738fcd..57d219d944995 100644 --- a/lldb/test/API/macosx/expedited-stack-memory/main.c +++ b/lldb/test/API/macosx/expedited-stack-memory/main.c @@ -31,10 +31,10 @@ struct Stats { double mean; }; -// The innermost frame, where we stop. It carries every kind of local: a -// scalar, aggregates (struct + array), and pointers (including one into heap -// memory). Examining this single frame on a stop reads both stack and -// heap memory. +// The innermost frame, where we stop. It carries several kind of local: a +// scalar, aggregates (struct + array), pointers (including one into heap +// memory) and a variable-length array. Examining this single frame on a stop +// reads both stack and heap memory. static int func_e(int depth) { int i = depth + 1; long l = (long)depth * 1000; _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
