blob: f9f673f80abcf6e5f87c3112a1d02f610897a018 [file]
"""
Test frame providers for scripted frame provider functionality.
These providers demonstrate various merge strategies:
- Replace: Replace entire stack
- Prepend: Add frames before real stack
- Append: Add frames after real stack
It also shows the ability to mix a dictionary, a ScriptedFrame or an SBFrame
index to create stackframes
"""
import os
import lldb
from lldb.plugins.scripted_process import ScriptedFrame
from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
class ReplaceFrameProvider(ScriptedFrameProvider):
"""Replace entire stack with custom frames."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
self.frames = [
{
"idx": 0,
"pc": 0x1000,
},
0,
{
"idx": 2,
"pc": 0x3000,
},
]
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Replace entire stack with 3 custom frames"
def get_frame_at_index(self, index):
if index >= len(self.frames):
return None
return self.frames[index]
class PrependFrameProvider(ScriptedFrameProvider):
"""Prepend synthetic frames before real stack."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Prepend 2 synthetic frames before real stack"
def get_frame_at_index(self, index):
if index == 0:
return {"pc": 0x9000}
elif index == 1:
return {"pc": 0xA000}
elif index - 2 < len(self.input_frames):
return index - 2 # Return real frame index.
return None
class AppendFrameProvider(ScriptedFrameProvider):
"""Append synthetic frames after real stack."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Append 1 synthetic frame after real stack"
def get_frame_at_index(self, index):
if index < len(self.input_frames):
return index # Return real frame index.
elif index == len(self.input_frames):
return {
"idx": 1,
"pc": 0x10,
}
return None
class CustomScriptedFrame(ScriptedFrame):
"""Custom scripted frame with full control over frame behavior."""
def __init__(self, thread, idx, pc, function_name):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
self.pc = pc
self.function_name = function_name
def get_id(self):
"""Return the frame index."""
return self.idx
def get_pc(self):
"""Return the program counter."""
return self.pc
def get_function_name(self):
"""Return the function name."""
return self.function_name
def is_artificial(self):
"""Mark as artificial frame."""
return False
def is_hidden(self):
"""Not hidden."""
return False
def get_register_context(self):
"""No register context for this test."""
return None
class ScriptedFrameObjectProvider(ScriptedFrameProvider):
"""Provider that returns ScriptedFrame objects instead of dictionaries."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Provider returning custom ScriptedFrame objects"
def get_frame_at_index(self, index):
"""Return ScriptedFrame objects or dictionaries based on index."""
if index == 0:
return CustomScriptedFrame(
self.thread, 0, 0x5000, "custom_scripted_frame_0"
)
elif index == 1:
return {"pc": 0x6000}
elif index == 2:
return CustomScriptedFrame(
self.thread, 2, 0x7000, "custom_scripted_frame_2"
)
elif index == 3:
return len(self.input_frames) - 2 # Real frame index.
elif index == 4:
return len(self.input_frames) - 1 # Real frame index.
return None
class ThreadFilterFrameProvider(ScriptedFrameProvider):
"""Provider that only applies to thread with ID 1."""
@staticmethod
def applies_to_thread(thread):
"""Only apply to thread with index ID 1."""
return thread.GetIndexID() == 1
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Provider that only applies to thread ID 1"
def get_frame_at_index(self, index):
"""Return a single synthetic frame."""
if index == 0:
return {"pc": 0xFFFF}
return None
class CircularDependencyTestProvider(ScriptedFrameProvider):
"""
Provider that tests the circular dependency fix.
This provider accesses input_frames during __init__ and calls methods
on those frames. Before the fix, this would cause a circular dependency:
- Thread::GetStackFrameList() creates provider
- Provider's __init__ accesses input_frames[0]
- SBFrame::GetPC() tries to resolve ExecutionContextRef
- ExecutionContextRef::GetFrameSP() calls Thread::GetStackFrameList()
- Re-enters initialization -> circular dependency!
With the fix, ExecutionContextRef remembers the frame list, so it doesn't
re-enter Thread::GetStackFrameList().
"""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
# This would cause circular dependency before the fix!
# Accessing frames and calling methods on them during __init__
self.original_frame_count = len(input_frames)
self.original_pcs = []
# Call GetPC() on each input frame - this triggers ExecutionContextRef resolution.
for i in range(min(3, len(input_frames))):
frame = input_frames[i]
if frame.IsValid():
pc = frame.GetPC()
self.original_pcs.append(pc)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Provider that tests circular dependency fix by accessing frames in __init__"
def get_frame_at_index(self, index):
"""Prepend a synthetic frame, then pass through original frames."""
if index == 0:
# Synthetic frame at index 0.
return {"pc": 0xDEADBEEF}
elif index - 1 < self.original_frame_count:
# Pass through original frames at indices 1, 2, 3, ...
return index - 1
return None
class PythonSourceFrame(ScriptedFrame):
"""Scripted frame that points to Python source code."""
def __init__(self, thread, idx, function_name, python_file, line_number):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
self.function_name = function_name
self.python_file = python_file
self.line_number = line_number
def get_id(self):
"""Return the frame index."""
return self.idx
def get_pc(self):
"""PC-less frame - return invalid address."""
return lldb.LLDB_INVALID_ADDRESS
def get_function_name(self):
"""Return the function name."""
return self.function_name
def get_symbol_context(self):
"""Return a symbol context with LineEntry pointing to Python source."""
# Create a LineEntry pointing to the Python source file
line_entry = lldb.SBLineEntry()
line_entry.SetFileSpec(lldb.SBFileSpec(self.python_file, True))
line_entry.SetLine(self.line_number)
line_entry.SetColumn(0)
# Create a symbol context with the line entry
sym_ctx = lldb.SBSymbolContext()
sym_ctx.SetLineEntry(line_entry)
return sym_ctx
def is_artificial(self):
"""Not artificial."""
return False
def is_hidden(self):
"""Not hidden."""
return False
def get_register_context(self):
"""No register context for PC-less frames."""
return None
class PythonSourceFrameProvider(ScriptedFrameProvider):
"""
Provider that demonstrates Python source display in scripted frames.
This provider prepends frames pointing to Python source code, showing
that PC-less frames can display Python source files with proper line
numbers and module/compile unit information.
"""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
# Find the python_helper.py file
current_dir = os.path.dirname(os.path.abspath(__file__))
self.python_file = os.path.join(current_dir, "python_helper.py")
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Provider that prepends frames pointing to Python source"
def get_frame_at_index(self, index):
"""Return Python source frames followed by original frames."""
if index == 0:
# Frame pointing to compute_fibonacci function (line 7)
return PythonSourceFrame(
self.thread, 0, "compute_fibonacci", self.python_file, 7
)
elif index == 1:
# Frame pointing to process_data function (line 16)
return PythonSourceFrame(
self.thread, 1, "process_data", self.python_file, 16
)
elif index == 2:
# Frame pointing to main function (line 27)
return PythonSourceFrame(self.thread, 2, "main", self.python_file, 27)
elif index - 3 < len(self.input_frames):
# Pass through original frames
return index - 3
return None
class ValidPCNoModuleFrame(ScriptedFrame):
"""Scripted frame with a valid PC but no associated module."""
def __init__(self, thread, idx, pc, function_name):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
self.pc = pc
self.function_name = function_name
def get_id(self):
"""Return the frame index."""
return self.idx
def get_pc(self):
"""Return the program counter."""
return self.pc
def get_function_name(self):
"""Return the function name."""
return self.function_name
def is_artificial(self):
"""Not artificial."""
return False
def is_hidden(self):
"""Not hidden."""
return False
def get_register_context(self):
"""No register context."""
return None
class ValidPCNoModuleFrameProvider(ScriptedFrameProvider):
"""
Provider that demonstrates frames with valid PC but no module.
This tests that backtrace output handles frames that have a valid
program counter but cannot be resolved to any loaded module.
"""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Provider that prepends frames with valid PC but no module"
def get_frame_at_index(self, index):
"""Return frames with valid PCs but no module information."""
if index == 0:
# Frame with valid PC (0x1234000) but no module
return ValidPCNoModuleFrame(self.thread, 0, 0x1234000, "unknown_function_1")
elif index == 1:
# Another frame with valid PC (0x5678000) but no module
return ValidPCNoModuleFrame(self.thread, 1, 0x5678000, "unknown_function_2")
elif index - 2 < len(self.input_frames):
# Pass through original frames
return index - 2
return None
class AddFooFrameProvider(ScriptedFrameProvider):
"""Add a single 'foo' frame at the beginning."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Add 'foo' frame at beginning"
@staticmethod
def get_priority():
"""Return priority 10 (runs first in chain)."""
return 10
def get_frame_at_index(self, index):
if index == 0:
# Return synthetic "foo" frame
return CustomScriptedFrame(self.thread, 0, 0xF00, "foo")
elif index - 1 < len(self.input_frames):
# Pass through input frames (shifted by 1)
return index - 1
return None
class AddBarFrameProvider(ScriptedFrameProvider):
"""Add a single 'bar' frame at the beginning."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Add 'bar' frame at beginning"
@staticmethod
def get_priority():
"""Return priority 20 (runs second in chain)."""
return 20
def get_frame_at_index(self, index):
if index == 0:
# Return synthetic "bar" frame
return CustomScriptedFrame(self.thread, 0, 0xBAA, "bar")
elif index - 1 < len(self.input_frames):
# Pass through input frames (shifted by 1)
return index - 1
return None
class AddBazFrameProvider(ScriptedFrameProvider):
"""Add a single 'baz' frame at the beginning."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Add 'baz' frame at beginning"
@staticmethod
def get_priority():
"""Return priority 30 (runs last in chain)."""
return 30
def get_frame_at_index(self, index):
if index == 0:
# Return synthetic "baz" frame
return CustomScriptedFrame(self.thread, 0, 0xBAC, "baz")
elif index - 1 < len(self.input_frames):
# Pass through input frames (shifted by 1)
return index - 1
return None
class ThreadValidatingFrame(ScriptedFrame):
"""Frame that validates thread member is correctly set after init.
This catches the regression where ScriptedFrame.__init__ used
GetThreadByIndexID(tid) instead of GetThreadByID(tid). The thread ID
and index ID are different, so using the wrong API produces an invalid
thread. The validation result is encoded in the function name so the
test can verify it through the SB API.
"""
def __init__(self, thread, idx):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
# Store validation results after base class init
self.thread_valid = self.thread is not None and self.thread.IsValid()
self.thread_id = self.thread.GetThreadID() if self.thread_valid else 0
def get_id(self):
return self.idx
def get_function_name(self):
# Encode validation in function name so test can verify via SB API
if self.thread_valid:
return f"thread_valid_id_{self.thread_id:#x}"
return "thread_INVALID"
def is_artificial(self):
return False
def is_hidden(self):
return False
def get_register_context(self):
return None
class ThreadValidatingFrameProvider(ScriptedFrameProvider):
"""Provider that validates ScriptedFrame.thread member initialization.
Uses ThreadValidatingFrame to verify that self.thread is correctly set
via GetThreadByID (not GetThreadByIndexID) after ScriptedFrame.__init__.
"""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
return "Validate ScriptedFrame.thread member initialization"
def get_frame_at_index(self, index):
if index == 0:
return ThreadValidatingFrame(self.thread, 0)
elif index - 1 < len(self.input_frames):
return index - 1
return None
class ValueProvidingFrame(ScriptedFrame):
"""Scripted frame with a valid PC but no associated module."""
def __init__(self, thread, idx, pc, function_name, variable):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
self.pc = pc
self.function_name = function_name
self.variable = variable
def get_id(self):
"""Return the frame index."""
return self.idx
def get_pc(self):
"""Return the program counter."""
return self.pc
def get_function_name(self):
"""Return the function name."""
return self.function_name
def is_artificial(self):
"""Not artificial."""
return False
def is_hidden(self):
"""Not hidden."""
return False
def get_register_context(self):
"""No register context."""
return None
def get_variables(self):
""""""
out = lldb.SBValueList()
out.Append(self.variable)
# Produce a fake value to be displayed.
out.Append(
self.variable.CreateValueFromExpression("_handler_one", "(uint32_t)1")
)
return out
def get_value_for_variable_expression(self, expr, options, error: lldb.SBError):
out = lldb.SBValue()
if expr == "_handler_one":
out = self.variable.CreateValueFromExpression("_handler_one", "(uint32_t)1")
elif self.variable.name in expr:
out = self.variable.CreateValueFromExpression("_expr", expr)
if out.IsValid():
return out
error.SetErrorString(f"expression {expr} failed")
return None
class ValueProvidingFrameProvider(ScriptedFrameProvider):
"""Add a single 'value-provider' frame at the beginning."""
def __init__(self, input_frames, args):
super().__init__(input_frames, args)
@staticmethod
def get_description():
"""Return a description of this provider."""
return "Add 'value-provider' frame at beginning"
def get_frame_at_index(self, index):
if index == 0:
f = self.input_frames.GetFrameAtIndex(index)
# Find some variable we can give to the frame.
variable = f.FindVariable("variable_in_main")
# Return synthetic "value-provider" frame
return ValueProvidingFrame(
self.thread, 0, 0xF00, "value-provider", variable
)
elif index - 1 < len(self.input_frames):
# Pass through input frames (shifted by 1)
return index - 1
return None