| """ | |
| 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) and the stopped frame's stack memory 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 the stopped frame's locals is covered by the stack expedite, so | |
| it sends no stack memory-read packets; heap buffers behind pointers are not | |
| on the stack and are still read from the stub. Examining deeper frames' | |
| locals does produce stack 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 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 | |
| @requireDarwin | |
| 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) | |
| @requireDarwin | |
| 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) | |
| @requireDarwin | |
| 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, | |
| pointer-to-heap, and stack-passed-parameter locals. Its stack is | |
| expedited, so examining it reads heap memory but no stack memory.""" | |
| self.check_memory_reads_when_examining_locals( | |
| examine_all_frames=False, expect_stack_reads=False | |
| ) | |
| @requireDarwin | |
| def test_memory_reads_when_examining_all_frames_locals(self): | |
| """Model "view all frames": walk the whole stack and examine every | |
| frame's locals. Only the stopped frame's stack is expedited, so the | |
| deeper frames' locals still read stack memory.""" | |
| self.check_memory_reads_when_examining_locals( | |
| examine_all_frames=True, expect_stack_reads=True | |
| ) | |
| def check_memory_reads_when_examining_locals( | |
| self, examine_all_frames, expect_stack_reads | |
| ): | |
| """Examining frame locals reads value memory; classify those reads into | |
| stack vs heap and check the counts. | |
| The frame-pointer backchain and the stopped frame's stack are expedited, | |
| so frame 0's stack-resident locals are served from the cache while heap | |
| buffers behind pointers (and deeper frames' locals) are read from the | |
| stub. | |
| 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) | |
| sent = self.walk_stack(per_frame, disable_memory_cache=False) | |
| if not expect_stack_reads: | |
| # Only a stub that expedites frame 0's stack can serve its locals | |
| # from the cache. | |
| lldbutil.require_qsupported_capability(self, "ExpediteStack+") | |
| # 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) | |
| 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" | |
| " 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" | |
| % ( | |
| len(stack_reads), | |
| len(heap_reads), | |
| len(other_reads), | |
| len(reads), | |
| 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), | |
| ) | |
| ) | |
| if expect_stack_reads: | |
| # Deeper frames' stacks are not expedited. | |
| self.assertGreater( | |
| len(stack_reads), | |
| 0, | |
| "expected stack memory reads while examining deeper frames' " | |
| "stack-resident locals.\n" + breakdown, | |
| ) | |
| else: | |
| # The stopped frame's stack is expedited. | |
| self.assertEqual( | |
| len(stack_reads), | |
| 0, | |
| "expected NO stack memory reads for frame 0 (its stack is " | |
| "expedited in jThreadsInfo).\n" + breakdown, | |
| ) | |
| 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 unframed contents of | |
| every packet lldb sent to the stub 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 [ | |
| 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 | |
| # 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 = [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. | |
| 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): | |
| """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. | |
| 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) | |
| for i in range(values.GetSize()): | |
| v = values.GetValueAtIndex(i) | |
| if not v.IsValid(): | |
| continue | |
| 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() | |
| @staticmethod | |
| def read_ranges(sent): | |
| """Parse [addr, addr+len) ranges out of the memory-read packets.""" | |
| ranges = [] | |
| for packet in sent: | |
| parsed = parse_memory_read_packet(packet) | |
| if parsed: | |
| addr, length = parsed | |
| ranges.append((addr, addr + length)) | |
| return ranges | |
| @staticmethod | |
| def in_region(read_range, region): | |
| """True if the read starts inside the given memory region.""" | |
| return region.GetRegionBase() <= read_range[0] < region.GetRegionEnd() |