blob: e8c8076699af760a699d58f03d6e09337917c569 [file] [edit]
"""
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 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
@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 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)
# 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),
)
)
# 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 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()