Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lldb/bindings/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
"plugins"
FILES
"${LLDB_SOURCE_DIR}/examples/python/templates/parsed_cmd.py"
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_frame_provider.py"
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_process.py"
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_platform.py"
"${LLDB_SOURCE_DIR}/examples/python/templates/operating_system.py"
Expand Down
147 changes: 147 additions & 0 deletions lldb/examples/python/templates/scripted_frame_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from abc import ABCMeta, abstractmethod

import lldb


class ScriptedFrameProvider(metaclass=ABCMeta):
"""
The base class for a scripted frame provider.

A scripted frame provider allows you to provide custom stack frames for a
thread, which can be used to augment or replace the standard unwinding
mechanism. This is useful for:

- Providing frames for custom calling conventions or languages
- Reconstructing missing frames from crash dumps or core files
- Adding diagnostic or synthetic frames for debugging
- Visualizing state machines or async execution contexts

Most of the base class methods are `@abstractmethod` that need to be
overwritten by the inheriting class.

Example usage:

.. code-block:: python

# Attach a frame provider to a thread
thread = process.GetSelectedThread()
error = lldb.SBError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error seems unused?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it's actually the return value of SBThread::SetScriptedFrameProvider. I'll update the doc.

thread.SetScriptedFrameProvider(
"my_module.MyFrameProvider",
lldb.SBStructuredData()
)
"""

@abstractmethod
def __init__(self, thread, args):
"""Construct a scripted frame provider.

Args:
thread (lldb.SBThread): The thread for which to provide frames.
args (lldb.SBStructuredData): A Dictionary holding arbitrary
key/value pairs used by the scripted frame provider.
"""
self.thread = None
self.args = None
self.target = None
self.process = None

if isinstance(thread, lldb.SBThread) and thread.IsValid():
self.thread = thread
self.process = thread.GetProcess()
if self.process and self.process.IsValid():
self.target = self.process.GetTarget()

if isinstance(args, lldb.SBStructuredData) and args.IsValid():
self.args = args

def get_merge_strategy(self):
"""Get the merge strategy for how scripted frames should be integrated.

The merge strategy determines how the scripted frames are combined with the
real unwound frames from the thread's normal unwinder.

Returns:
int: One of the following lldb.ScriptedFrameProviderMergeStrategy values:

- lldb.eScriptedFrameProviderMergeStrategyReplace: Replace the entire stack
with scripted frames. The thread will only show frames provided
by this provider.

- lldb.eScriptedFrameProviderMergeStrategyPrepend: Prepend scripted frames
before the real unwound frames. Useful for adding synthetic frames
at the top of the stack while preserving the actual callstack below.

- lldb.eScriptedFrameProviderMergeStrategyAppend: Append scripted frames
after the real unwound frames. Useful for showing additional context
after the actual callstack ends.

- lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex: Replace specific
frames at given indices with scripted frames, keeping other real frames
intact. The idx field in each frame dictionary determines which real
frame to replace (e.g., idx=0 replaces frame 0, idx=2 replaces frame 2).

The default implementation returns Replace strategy.

Example:

.. code-block:: python

def get_merge_strategy(self):
# Only show our custom frames
return lldb.eScriptedFrameProviderMergeStrategyReplace

def get_merge_strategy(self):
# Add diagnostic frames on top of real stack
return lldb.eScriptedFrameProviderMergeStrategyPrepend

def get_merge_strategy(self):
# Replace frame 0 and frame 2 with custom frames, keep others
return lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex
"""
return lldb.eScriptedFrameProviderMergeStrategyReplace

@abstractmethod
def get_stackframes(self):
"""Get the list of stack frames to provide.

This method is called when the thread's backtrace is requested
(e.g., via the 'bt' command). The returned frames will be integrated
with the real frames according to the mode returned by get_mode().

Returns:
List[Dict]: A list of frame dictionaries, where each dictionary
describes a single stack frame. Each dictionary should contain:

Required fields:
- idx (int): The frame index (0 for innermost/top frame)
- pc (int): The program counter address for this frame

Alternatively, you can return a list of ScriptedFrame objects
for more control over frame behavior.

Example:

.. code-block:: python

def get_stackframes(self):
frames = []

# Frame 0: Current function
frames.append({
"idx": 0,
"pc": 0x100001234,
})

# Frame 1: Caller
frames.append({
"idx": 1,
"pc": 0x100001000,
})

return frames

Note:
The frames are indexed from 0 (innermost/newest) to N (outermost/oldest).
"""
pass
41 changes: 29 additions & 12 deletions lldb/examples/python/templates/scripted_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def __init__(self, process, args):
key/value pairs used by the scripted thread.
"""
self.target = None
self.arch = None
self.originating_process = None
self.process = None
self.args = None
Expand All @@ -266,6 +267,9 @@ def __init__(self, process, args):
and process.IsValid()
):
self.target = process.target
triple = self.target.triple
if triple:
self.arch = triple.split("-")[0]
self.originating_process = process
self.process = self.target.GetProcess()
self.get_register_info()
Expand Down Expand Up @@ -352,17 +356,14 @@ def get_stackframes(self):
def get_register_info(self):
if self.register_info is None:
self.register_info = dict()
if "x86_64" in self.originating_process.arch:
if "x86_64" in self.arch:
self.register_info["sets"] = ["General Purpose Registers"]
self.register_info["registers"] = INTEL64_GPR
elif (
"arm64" in self.originating_process.arch
or self.originating_process.arch == "aarch64"
):
elif "arm64" in self.arch or self.arch == "aarch64":
self.register_info["sets"] = ["General Purpose Registers"]
self.register_info["registers"] = ARM64_GPR
else:
raise ValueError("Unknown architecture", self.originating_process.arch)
raise ValueError("Unknown architecture", self.arch)
return self.register_info

@abstractmethod
Expand Down Expand Up @@ -405,11 +406,12 @@ def __init__(self, thread, args):
"""Construct a scripted frame.

Args:
thread (ScriptedThread): The thread owning this frame.
thread (ScriptedThread/lldb.SBThread): The thread owning this frame.
args (lldb.SBStructuredData): A Dictionary holding arbitrary
key/value pairs used by the scripted frame.
"""
self.target = None
self.arch = None
self.originating_thread = None
self.thread = None
self.args = None
Expand All @@ -424,10 +426,14 @@ def __init__(self, thread, args):
or isinstance(thread, lldb.SBThread)
and thread.IsValid()
):
self.target = thread.target
self.process = thread.process
self.target = self.process.target
triple = self.target.triple
if triple:
self.arch = triple.split("-")[0]
tid = thread.tid if isinstance(thread, ScriptedThread) else thread.id
self.originating_thread = thread
self.thread = self.process.GetThreadByIndexID(thread.tid)
self.thread = self.process.GetThreadByIndexID(tid)
self.get_register_info()

@abstractmethod
Expand Down Expand Up @@ -508,7 +514,18 @@ def get_variables(self, filters):

def get_register_info(self):
if self.register_info is None:
self.register_info = self.originating_thread.get_register_info()
if isinstance(self.originating_thread, ScriptedThread):
self.register_info = self.originating_thread.get_register_info()
elif isinstance(self.originating_thread, lldb.SBThread):
self.register_info = dict()
if "x86_64" in self.arch:
self.register_info["sets"] = ["General Purpose Registers"]
self.register_info["registers"] = INTEL64_GPR
elif "arm64" in self.arch or self.arch == "aarch64":
self.register_info["sets"] = ["General Purpose Registers"]
self.register_info["registers"] = ARM64_GPR
else:
raise ValueError("Unknown architecture", self.arch)
return self.register_info

@abstractmethod
Expand Down Expand Up @@ -642,12 +659,12 @@ def get_stop_reason(self):

# TODO: Passthrough stop reason from driving process
if self.driving_thread.GetStopReason() != lldb.eStopReasonNone:
if "arm64" in self.originating_process.arch:
if "arm64" in self.arch:
stop_reason["type"] = lldb.eStopReasonException
stop_reason["data"]["desc"] = (
self.driving_thread.GetStopDescription(100)
)
elif self.originating_process.arch == "x86_64":
elif self.arch == "x86_64":
stop_reason["type"] = lldb.eStopReasonSignal
stop_reason["data"]["signal"] = signal.SIGTRAP
else:
Expand Down
5 changes: 5 additions & 0 deletions lldb/include/lldb/API/SBThread.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ class LLDB_API SBThread {

SBValue GetSiginfo();

SBError RegisterFrameProvider(const char *class_name,
SBStructuredData &args_data);

void ClearScriptedFrameProvider();

private:
friend class SBBreakpoint;
friend class SBBreakpointLocation;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//===-- ScriptedFrameProviderInterface.h ------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H

#include "lldb/lldb-private.h"

#include "ScriptedInterface.h"

namespace lldb_private {
class ScriptedFrameProviderInterface : public ScriptedInterface {
public:
virtual llvm::Expected<StructuredData::GenericSP>
CreatePluginObject(llvm::StringRef class_name, lldb::ThreadSP thread_sp,
StructuredData::DictionarySP args_sp) = 0;

/// Get the merge strategy for how scripted frames should be integrated with
/// real frames
virtual lldb::ScriptedFrameProviderMergeStrategy GetMergeStrategy() {
return lldb::eScriptedFrameProviderMergeStrategyReplace;
}

virtual StructuredData::ArraySP
GetStackFrames(lldb::StackFrameListSP real_frames) {
return {};
}
};
} // namespace lldb_private

#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
6 changes: 6 additions & 0 deletions lldb/include/lldb/Interpreter/ScriptInterpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "lldb/Host/StreamFile.h"
#include "lldb/Interpreter/Interfaces/OperatingSystemInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
Expand Down Expand Up @@ -536,6 +537,11 @@ class ScriptInterpreter : public PluginInterface {
return {};
}

virtual lldb::ScriptedFrameProviderInterfaceSP
CreateScriptedFrameProviderInterface() {
return {};
}

virtual lldb::ScriptedThreadPlanInterfaceSP
CreateScriptedThreadPlanInterface() {
return {};
Expand Down
Loading
Loading