Skip to content

Commit 5b21aa8

Browse files
committed
[lldb] Introduce ScriptedFrameProvider for real threads
This patch introduces a new scripting affordance: `ScriptedFrameProvider`. This allows users to provide custom stack frames for real native threads, augmenting or replacing 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 The frame provider supports four merge strategies: - Replace: Replace entire stack with scripted frames - Prepend: Add scripted frames before real stack - Append: Add scripted frames after real stack - ReplaceByIndex: Replace specific frames by index With this change, frames can be synthesized from different sources: - Either from a dictionary containing a PC address and frame index - Or by creating a ScriptedFrame python object for full control To use it, first register the scripted frame provider then use existing commands: (lldb) frame provider register -C my_module.MyFrameProvider or (lldb) script thread.RegisterFrameProvider("my_module.MyFrameProvider", lldb.SBStructuredData()) then (lldb) bt See examples/python/templates/scripted_frame_provider.py for details. Architecture changes: - Moved ScriptedFrame from `Plugins` to `Interpreter` to avoid layering violations - Moved `RegisterContextMemory` from `Plugins` to `Target` as it only depends on Target and Utility layers - Added `ScriptedFrameProvider` C++ wrapper and Python interface - Updated `Thread::GetStackFrameList` to apply merge strategies rdar://161834688 Signed-off-by: Med Ismail Bennani <[email protected]>
1 parent 3d734e9 commit 5b21aa8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1535
-77
lines changed

lldb/bindings/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
107107
"plugins"
108108
FILES
109109
"${LLDB_SOURCE_DIR}/examples/python/templates/parsed_cmd.py"
110+
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_frame_provider.py"
110111
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_process.py"
111112
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_platform.py"
112113
"${LLDB_SOURCE_DIR}/examples/python/templates/operating_system.py"

lldb/bindings/python/python-swigsafecast.swig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ PythonObject SWIGBridge::ToSWIGWrapper(lldb::ThreadPlanSP thread_plan_sp) {
3737
SWIGTYPE_p_lldb__SBThreadPlan);
3838
}
3939

40+
PythonObject SWIGBridge::ToSWIGWrapper(lldb::StackFrameListSP frames_sp) {
41+
return ToSWIGHelper(new lldb::SBFrameList(std::move(frames_sp)),
42+
SWIGTYPE_p_lldb__SBFrameList);
43+
}
44+
4045
PythonObject SWIGBridge::ToSWIGWrapper(lldb::BreakpointSP breakpoint_sp) {
4146
return ToSWIGHelper(new lldb::SBBreakpoint(std::move(breakpoint_sp)),
4247
SWIGTYPE_p_lldb__SBBreakpoint);

lldb/bindings/python/python-wrapper.swig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,18 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyOb
532532
return sb_ptr;
533533
}
534534

535+
void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data) {
536+
lldb::SBFrameList *sb_ptr = NULL;
537+
538+
int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
539+
SWIGTYPE_p_lldb__SBFrameList, 0);
540+
541+
if (valid_cast == -1)
542+
return NULL;
543+
544+
return sb_ptr;
545+
}
546+
535547
bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
536548
const char *python_function_name, const char *session_dictionary_name,
537549
lldb::DebuggerSP debugger, const char *args,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
import lldb
4+
5+
6+
class ScriptedFrameProvider(metaclass=ABCMeta):
7+
"""
8+
The base class for a scripted frame provider.
9+
10+
A scripted frame provider allows you to provide custom stack frames for a
11+
thread, which can be used to augment or replace the standard unwinding
12+
mechanism. This is useful for:
13+
14+
- Providing frames for custom calling conventions or languages
15+
- Reconstructing missing frames from crash dumps or core files
16+
- Adding diagnostic or synthetic frames for debugging
17+
- Visualizing state machines or async execution contexts
18+
19+
Most of the base class methods are `@abstractmethod` that need to be
20+
overwritten by the inheriting class.
21+
22+
Example usage:
23+
24+
.. code-block:: python
25+
26+
# Attach a frame provider to a thread
27+
thread = process.GetSelectedThread()
28+
error = thread.SetScriptedFrameProvider(
29+
"my_module.MyFrameProvider",
30+
lldb.SBStructuredData()
31+
)
32+
"""
33+
34+
@abstractmethod
35+
def __init__(self, thread, args):
36+
"""Construct a scripted frame provider.
37+
38+
Args:
39+
thread (lldb.SBThread): The thread for which to provide frames.
40+
args (lldb.SBStructuredData): A Dictionary holding arbitrary
41+
key/value pairs used by the scripted frame provider.
42+
"""
43+
self.thread = None
44+
self.args = None
45+
self.target = None
46+
self.process = None
47+
48+
if isinstance(thread, lldb.SBThread) and thread.IsValid():
49+
self.thread = thread
50+
self.process = thread.GetProcess()
51+
if self.process and self.process.IsValid():
52+
self.target = self.process.GetTarget()
53+
54+
if isinstance(args, lldb.SBStructuredData) and args.IsValid():
55+
self.args = args
56+
57+
def get_merge_strategy(self):
58+
"""Get the merge strategy for how scripted frames should be integrated.
59+
60+
The merge strategy determines how the scripted frames are combined with the
61+
real unwound frames from the thread's normal unwinder.
62+
63+
Returns:
64+
int: One of the following lldb.ScriptedFrameProviderMergeStrategy values:
65+
66+
- lldb.eScriptedFrameProviderMergeStrategyReplace: Replace the entire stack
67+
with scripted frames. The thread will only show frames provided
68+
by this provider.
69+
70+
- lldb.eScriptedFrameProviderMergeStrategyPrepend: Prepend scripted frames
71+
before the real unwound frames. Useful for adding synthetic frames
72+
at the top of the stack while preserving the actual callstack below.
73+
74+
- lldb.eScriptedFrameProviderMergeStrategyAppend: Append scripted frames
75+
after the real unwound frames. Useful for showing additional context
76+
after the actual callstack ends.
77+
78+
- lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex: Replace specific
79+
frames at given indices with scripted frames, keeping other real frames
80+
intact. The idx field in each frame dictionary determines which real
81+
frame to replace (e.g., idx=0 replaces frame 0, idx=2 replaces frame 2).
82+
83+
The default implementation returns Replace strategy.
84+
85+
Example:
86+
87+
.. code-block:: python
88+
89+
def get_merge_strategy(self):
90+
# Only show our custom frames
91+
return lldb.eScriptedFrameProviderMergeStrategyReplace
92+
93+
def get_merge_strategy(self):
94+
# Add diagnostic frames on top of real stack
95+
return lldb.eScriptedFrameProviderMergeStrategyPrepend
96+
97+
def get_merge_strategy(self):
98+
# Replace frame 0 and frame 2 with custom frames, keep others
99+
return lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex
100+
"""
101+
return lldb.eScriptedFrameProviderMergeStrategyReplace
102+
103+
@abstractmethod
104+
def get_stackframes(self, real_frames):
105+
"""Get the list of stack frames to provide.
106+
107+
This method is called when the thread's backtrace is requested
108+
(e.g., via the 'bt' command). The returned frames will be integrated
109+
with the real frames according to the mode returned by get_mode().
110+
111+
Args:
112+
real_frames (lldb.SBFrameList): The actual unwound frames from the
113+
thread's normal unwinder. This allows you to iterate, filter,
114+
and selectively replace frames. The frames are materialized
115+
lazily as you access them.
116+
117+
Returns:
118+
List[Dict]: A list of frame dictionaries, where each dictionary
119+
describes a single stack frame. Each dictionary should contain:
120+
121+
Required fields:
122+
- idx (int): The frame index (0 for innermost/top frame)
123+
- pc (int): The program counter address for this frame
124+
125+
Alternatively, you can return a list of ScriptedFrame objects
126+
for more control over frame behavior.
127+
128+
Example:
129+
130+
.. code-block:: python
131+
132+
def get_stackframes(self, real_frames):
133+
frames = []
134+
135+
# Iterate over real frames and filter/augment them
136+
for i, frame in enumerate(real_frames):
137+
if self.should_include_frame(frame):
138+
frames.append({
139+
"idx": i,
140+
"pc": frame.GetPC(),
141+
})
142+
143+
# Or create custom frames
144+
frames.append({
145+
"idx": 0,
146+
"pc": 0x100001234,
147+
})
148+
149+
return frames
150+
151+
Note:
152+
The frames are indexed from 0 (innermost/newest) to N (outermost/oldest).
153+
"""
154+
pass

lldb/examples/python/templates/scripted_process.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def __init__(self, process, args):
245245
key/value pairs used by the scripted thread.
246246
"""
247247
self.target = None
248+
self.arch = None
248249
self.originating_process = None
249250
self.process = None
250251
self.args = None
@@ -266,6 +267,9 @@ def __init__(self, process, args):
266267
and process.IsValid()
267268
):
268269
self.target = process.target
270+
triple = self.target.triple
271+
if triple:
272+
self.arch = triple.split("-")[0]
269273
self.originating_process = process
270274
self.process = self.target.GetProcess()
271275
self.get_register_info()
@@ -352,17 +356,14 @@ def get_stackframes(self):
352356
def get_register_info(self):
353357
if self.register_info is None:
354358
self.register_info = dict()
355-
if "x86_64" in self.originating_process.arch:
359+
if "x86_64" in self.arch:
356360
self.register_info["sets"] = ["General Purpose Registers"]
357361
self.register_info["registers"] = INTEL64_GPR
358-
elif (
359-
"arm64" in self.originating_process.arch
360-
or self.originating_process.arch == "aarch64"
361-
):
362+
elif "arm64" in self.arch or self.arch == "aarch64":
362363
self.register_info["sets"] = ["General Purpose Registers"]
363364
self.register_info["registers"] = ARM64_GPR
364365
else:
365-
raise ValueError("Unknown architecture", self.originating_process.arch)
366+
raise ValueError("Unknown architecture", self.arch)
366367
return self.register_info
367368

368369
@abstractmethod
@@ -405,11 +406,12 @@ def __init__(self, thread, args):
405406
"""Construct a scripted frame.
406407
407408
Args:
408-
thread (ScriptedThread): The thread owning this frame.
409+
thread (ScriptedThread/lldb.SBThread): The thread owning this frame.
409410
args (lldb.SBStructuredData): A Dictionary holding arbitrary
410411
key/value pairs used by the scripted frame.
411412
"""
412413
self.target = None
414+
self.arch = None
413415
self.originating_thread = None
414416
self.thread = None
415417
self.args = None
@@ -424,10 +426,14 @@ def __init__(self, thread, args):
424426
or isinstance(thread, lldb.SBThread)
425427
and thread.IsValid()
426428
):
427-
self.target = thread.target
428429
self.process = thread.process
430+
self.target = self.process.target
431+
triple = self.target.triple
432+
if triple:
433+
self.arch = triple.split("-")[0]
434+
tid = thread.tid if isinstance(thread, ScriptedThread) else thread.id
429435
self.originating_thread = thread
430-
self.thread = self.process.GetThreadByIndexID(thread.tid)
436+
self.thread = self.process.GetThreadByIndexID(tid)
431437
self.get_register_info()
432438

433439
@abstractmethod
@@ -508,7 +514,18 @@ def get_variables(self, filters):
508514

509515
def get_register_info(self):
510516
if self.register_info is None:
511-
self.register_info = self.originating_thread.get_register_info()
517+
if isinstance(self.originating_thread, ScriptedThread):
518+
self.register_info = self.originating_thread.get_register_info()
519+
elif isinstance(self.originating_thread, lldb.SBThread):
520+
self.register_info = dict()
521+
if "x86_64" in self.arch:
522+
self.register_info["sets"] = ["General Purpose Registers"]
523+
self.register_info["registers"] = INTEL64_GPR
524+
elif "arm64" in self.arch or self.arch == "aarch64":
525+
self.register_info["sets"] = ["General Purpose Registers"]
526+
self.register_info["registers"] = ARM64_GPR
527+
else:
528+
raise ValueError("Unknown architecture", self.arch)
512529
return self.register_info
513530

514531
@abstractmethod
@@ -642,12 +659,12 @@ def get_stop_reason(self):
642659

643660
# TODO: Passthrough stop reason from driving process
644661
if self.driving_thread.GetStopReason() != lldb.eStopReasonNone:
645-
if "arm64" in self.originating_process.arch:
662+
if "arm64" in self.arch:
646663
stop_reason["type"] = lldb.eStopReasonException
647664
stop_reason["data"]["desc"] = (
648665
self.driving_thread.GetStopDescription(100)
649666
)
650-
elif self.originating_process.arch == "x86_64":
667+
elif self.arch == "x86_64":
651668
stop_reason["type"] = lldb.eStopReasonSignal
652669
stop_reason["data"]["signal"] = signal.SIGTRAP
653670
else:

lldb/include/lldb/API/SBFrameList.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111

1212
#include "lldb/API/SBDefines.h"
1313

14+
namespace lldb_private {
15+
class ScriptInterpreter;
16+
namespace python {
17+
class SWIGBridge;
18+
}
19+
namespace lua {
20+
class SWIGBridge;
21+
}
22+
} // namespace lldb_private
23+
1424
namespace lldb {
1525

1626
class LLDB_API SBFrameList {
@@ -42,6 +52,10 @@ class LLDB_API SBFrameList {
4252
protected:
4353
friend class SBThread;
4454

55+
friend class lldb_private::python::SWIGBridge;
56+
friend class lldb_private::lua::SWIGBridge;
57+
friend class lldb_private::ScriptInterpreter;
58+
4559
private:
4660
SBFrameList(const lldb::StackFrameListSP &frame_list_sp);
4761

lldb/include/lldb/API/SBThread.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ class LLDB_API SBThread {
231231

232232
SBValue GetSiginfo();
233233

234+
SBError RegisterFrameProvider(const char *class_name,
235+
SBStructuredData &args_data);
236+
237+
void ClearScriptedFrameProvider();
238+
234239
private:
235240
friend class SBBreakpoint;
236241
friend class SBBreakpointLocation;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===-- ScriptedFrameProviderInterface.h ------------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
10+
#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
11+
12+
#include "lldb/lldb-private.h"
13+
14+
#include "ScriptedInterface.h"
15+
16+
namespace lldb_private {
17+
class ScriptedFrameProviderInterface : public ScriptedInterface {
18+
public:
19+
virtual llvm::Expected<StructuredData::GenericSP>
20+
CreatePluginObject(llvm::StringRef class_name, lldb::ThreadSP thread_sp,
21+
StructuredData::DictionarySP args_sp) = 0;
22+
23+
/// Get the merge strategy for how scripted frames should be integrated with
24+
/// real frames
25+
virtual lldb::ScriptedFrameProviderMergeStrategy GetMergeStrategy() {
26+
return lldb::eScriptedFrameProviderMergeStrategyReplace;
27+
}
28+
29+
virtual StructuredData::ArraySP
30+
GetStackFrames(lldb::StackFrameListSP real_frames) {
31+
return {};
32+
}
33+
};
34+
} // namespace lldb_private
35+
36+
#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H

0 commit comments

Comments
 (0)