| """ | |
| 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_value_type_for_variable(self, var): | |
| name = var.GetName() | |
| if name == "_handler_one": | |
| return lldb.eValueTypeVariableLocal | lldb.eValueTypeSyntheticFlag | |
| if name == "variable_in_main": | |
| return lldb.eValueTypeVariableLocal | |
| 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 |