Skip to content

Commit ab4a9b8

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 c2765b7 commit ab4a9b8

39 files changed

+1442
-66
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"
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 = lldb.SBError()
29+
thread.SetScriptedFrameProvider(
30+
"my_module.MyFrameProvider",
31+
lldb.SBStructuredData()
32+
)
33+
"""
34+
35+
@abstractmethod
36+
def __init__(self, thread, args):
37+
"""Construct a scripted frame provider.
38+
39+
Args:
40+
thread (lldb.SBThread): The thread for which to provide frames.
41+
args (lldb.SBStructuredData): A Dictionary holding arbitrary
42+
key/value pairs used by the scripted frame provider.
43+
"""
44+
self.thread = None
45+
self.args = None
46+
self.target = None
47+
self.process = None
48+
49+
if isinstance(thread, lldb.SBThread) and thread.IsValid():
50+
self.thread = thread
51+
self.process = thread.GetProcess()
52+
if self.process and self.process.IsValid():
53+
self.target = self.process.GetTarget()
54+
55+
if isinstance(args, lldb.SBStructuredData) and args.IsValid():
56+
self.args = args
57+
58+
def get_merge_strategy(self):
59+
"""Get the merge strategy for how scripted frames should be integrated.
60+
61+
The merge strategy determines how the scripted frames are combined with the
62+
real unwound frames from the thread's normal unwinder.
63+
64+
Returns:
65+
int: One of the following lldb.ScriptedFrameProviderMergeStrategy values:
66+
67+
- lldb.eScriptedFrameProviderMergeStrategyReplace: Replace the entire stack
68+
with scripted frames. The thread will only show frames provided
69+
by this provider.
70+
71+
- lldb.eScriptedFrameProviderMergeStrategyPrepend: Prepend scripted frames
72+
before the real unwound frames. Useful for adding synthetic frames
73+
at the top of the stack while preserving the actual callstack below.
74+
75+
- lldb.eScriptedFrameProviderMergeStrategyAppend: Append scripted frames
76+
after the real unwound frames. Useful for showing additional context
77+
after the actual callstack ends.
78+
79+
- lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex: Replace specific
80+
frames at given indices with scripted frames, keeping other real frames
81+
intact. The idx field in each frame dictionary determines which real
82+
frame to replace (e.g., idx=0 replaces frame 0, idx=2 replaces frame 2).
83+
84+
The default implementation returns Replace strategy.
85+
86+
Example:
87+
88+
.. code-block:: python
89+
90+
def get_merge_strategy(self):
91+
# Only show our custom frames
92+
return lldb.eScriptedFrameProviderMergeStrategyReplace
93+
94+
def get_merge_strategy(self):
95+
# Add diagnostic frames on top of real stack
96+
return lldb.eScriptedFrameProviderMergeStrategyPrepend
97+
98+
def get_merge_strategy(self):
99+
# Replace frame 0 and frame 2 with custom frames, keep others
100+
return lldb.eScriptedFrameProviderMergeStrategyReplaceByIndex
101+
"""
102+
return lldb.eScriptedFrameProviderMergeStrategyReplace
103+
104+
@abstractmethod
105+
def get_stackframes(self):
106+
"""Get the list of stack frames to provide.
107+
108+
This method is called when the thread's backtrace is requested
109+
(e.g., via the 'bt' command). The returned frames will be integrated
110+
with the real frames according to the mode returned by get_mode().
111+
112+
Returns:
113+
List[Dict]: A list of frame dictionaries, where each dictionary
114+
describes a single stack frame. Each dictionary should contain:
115+
116+
Required fields:
117+
- idx (int): The frame index (0 for innermost/top frame)
118+
- pc (int): The program counter address for this frame
119+
120+
Alternatively, you can return a list of ScriptedFrame objects
121+
for more control over frame behavior.
122+
123+
Example:
124+
125+
.. code-block:: python
126+
127+
def get_stackframes(self):
128+
frames = []
129+
130+
# Frame 0: Current function
131+
frames.append({
132+
"idx": 0,
133+
"pc": 0x100001234,
134+
})
135+
136+
# Frame 1: Caller
137+
frames.append({
138+
"idx": 1,
139+
"pc": 0x100001000,
140+
})
141+
142+
return frames
143+
144+
Note:
145+
The frames are indexed from 0 (innermost/newest) to N (outermost/oldest).
146+
"""
147+
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/SBThread.h

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

230230
SBValue GetSiginfo();
231231

232+
SBError RegisterFrameProvider(const char *class_name,
233+
SBStructuredData &args_data);
234+
235+
void ClearScriptedFrameProvider();
236+
232237
private:
233238
friend class SBBreakpoint;
234239
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

lldb/include/lldb/Interpreter/ScriptInterpreter.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "lldb/Host/StreamFile.h"
2828
#include "lldb/Interpreter/Interfaces/OperatingSystemInterface.h"
2929
#include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
30+
#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
3031
#include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
3132
#include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
3233
#include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
@@ -536,6 +537,11 @@ class ScriptInterpreter : public PluginInterface {
536537
return {};
537538
}
538539

540+
virtual lldb::ScriptedFrameProviderInterfaceSP
541+
CreateScriptedFrameProviderInterface() {
542+
return {};
543+
}
544+
539545
virtual lldb::ScriptedThreadPlanInterfaceSP
540546
CreateScriptedThreadPlanInterface() {
541547
return {};

0 commit comments

Comments
 (0)