Skip to content

Commit

Permalink
[lldb] Extend frame recognizers to hide frames from backtraces
Browse files Browse the repository at this point in the history
Compilers and language runtimes often use helper functions that are
fundamentally uninteresting when debugging anything but the
compiler/runtime itself. This patch introduces a user-extensible
mechanism that allows for these frames to be hidden from backtraces
and automatically skipped over when navigating the stack with `up` and
`down`, and when stepping out of the current frame.

This does not affect the numbering of frames, so `f <N>` will still
provide access to the hidden frames. The `bt` output will also print a
hint that frames have been hidden.

My primary motivation for this feature is to hide thunks in the Swift
programming language, but I'm including an example recognizer for
`std::function::operator()` that I wished for myself many times while
debugging LLDB.

The functionality is user-extensible via Python recognizers and
exposed through the SBAPI via SBFrame::IsHidden().

rdar://126629381
  • Loading branch information
adrian-prantl committed Aug 20, 2024
1 parent 5fcd059 commit 81af101
Show file tree
Hide file tree
Showing 32 changed files with 419 additions and 74 deletions.
18 changes: 17 additions & 1 deletion lldb/bindings/python/python-wrapper.swig
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ PythonObject lldb_private::python::SWIGBridge::LLDBSWIGPython_CreateFrameRecogni
}

PyObject *lldb_private::python::SWIGBridge::LLDBSwigPython_GetRecognizedArguments(
PyObject * implementor, const lldb::StackFrameSP &frame_sp) {
PyObject *implementor, const lldb::StackFrameSP &frame_sp) {
static char callee_name[] = "get_recognized_arguments";

PythonObject arg = SWIGBridge::ToSWIGWrapper(frame_sp);
Expand All @@ -824,6 +824,22 @@ PyObject *lldb_private::python::SWIGBridge::LLDBSwigPython_GetRecognizedArgument
return result;
}

bool lldb_private::python::SWIGBridge::LLDBSwigPython_ShouldHide(
PyObject *implementor, const lldb::StackFrameSP &frame_sp) {
static char callee_name[] = "should_hide";

PythonObject arg = SWIGBridge::ToSWIGWrapper(frame_sp);

PythonString str(callee_name);

PyObject *result =
PyObject_CallMethodObjArgs(implementor, str.get(), arg.get(), NULL);
bool ret_val = result ? PyObject_IsTrue(result) : false;
Py_XDECREF(result);

return result;
}

void *lldb_private::python::SWIGBridge::LLDBSWIGPython_GetDynamicSetting(
void *module, const char *setting, const lldb::TargetSP &target_sp) {
if (!module || !setting)
Expand Down
4 changes: 4 additions & 0 deletions lldb/include/lldb/API/SBFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class LLDB_API SBFrame {

bool IsArtificial() const;

/// Return whether a frame recognizer decided this frame should not
/// be displayes in backtraces etc.
bool IsHidden() const;

/// The version that doesn't supply a 'use_dynamic' value will use the
/// target's default.
lldb::SBValue EvaluateExpression(const char *expr);
Expand Down
5 changes: 5 additions & 0 deletions lldb/include/lldb/Interpreter/ScriptInterpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ class ScriptInterpreter : public PluginInterface {
return lldb::ValueObjectListSP();
}

virtual bool ShouldHide(const StructuredData::ObjectSP &implementor,
lldb::StackFrameSP frame_sp) {
return false;
}

virtual StructuredData::GenericSP
CreateScriptedBreakpointResolver(const char *class_name,
const StructuredDataImpl &args_data,
Expand Down
33 changes: 19 additions & 14 deletions lldb/include/lldb/Target/StackFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ class StackFrame : public ExecutionContextScope,
/// may have limited support for inspecting variables.
bool IsArtificial() const;

/// Query whether this frame should be hidden from backtraces. Frame
/// recognizers can customize this behavior and hide distracting
/// system implementation details this way.
bool IsHidden();

/// Query this frame to find what frame it is in this Thread's
/// StackFrameList.
///
Expand Down Expand Up @@ -518,33 +523,33 @@ class StackFrame : public ExecutionContextScope,
bool HasCachedData() const;

private:
// For StackFrame only
/// For StackFrame only.
lldb::ThreadWP m_thread_wp;
uint32_t m_frame_index;
uint32_t m_concrete_frame_index;
lldb::RegisterContextSP m_reg_context_sp;
StackID m_id;
Address m_frame_code_addr; // The frame code address (might not be the same as
// the actual PC for inlined frames) as a
// section/offset address
/// The frame code address (might not be the same as the actual PC
/// for inlined frames) as a section/offset address.
Address m_frame_code_addr;
SymbolContext m_sc;
Flags m_flags;
Scalar m_frame_base;
Status m_frame_base_error;
bool m_cfa_is_valid; // Does this frame have a CFA? Different from CFA ==
// LLDB_INVALID_ADDRESS
uint16_t m_frame_recognizer_generation;
/// Does this frame have a CFA? Different from CFA == LLDB_INVALID_ADDRESS.
bool m_cfa_is_valid;
Kind m_stack_frame_kind;

// Whether this frame behaves like the zeroth frame, in the sense
// that its pc value might not immediately follow a call (and thus might
// be the first address of its function). True for actual frame zero as
// well as any other frame with the same trait.
/// Whether this frame behaves like the zeroth frame, in the sense
/// that its pc value might not immediately follow a call (and thus might
/// be the first address of its function). True for actual frame zero as
/// well as any other frame with the same trait.
bool m_behaves_like_zeroth_frame;
lldb::VariableListSP m_variable_list_sp;
ValueObjectList m_variable_list_value_objects; // Value objects for each
// variable in
// m_variable_list_sp
lldb::RecognizedStackFrameSP m_recognized_frame_sp;
/// Value objects for each variable in m_variable_list_sp.
ValueObjectList m_variable_list_value_objects;
std::optional<lldb::RecognizedStackFrameSP> m_recognized_frame_sp;
StreamString m_disassembly;
std::recursive_mutex m_mutex;

Expand Down
2 changes: 1 addition & 1 deletion lldb/include/lldb/Target/StackFrameList.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class StackFrameList {

size_t GetStatus(Stream &strm, uint32_t first_frame, uint32_t num_frames,
bool show_frame_info, uint32_t num_frames_with_source,
bool show_unique = false,
bool show_unique = false, bool show_hidden = false,
const char *frame_marker = nullptr);

protected:
Expand Down
20 changes: 14 additions & 6 deletions lldb/include/lldb/Target/StackFrameRecognizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "lldb/lldb-private-forward.h"
#include "lldb/lldb-public.h"

#include <_types/_uint16_t.h>
#include <deque>
#include <optional>
#include <vector>
Expand All @@ -28,20 +29,23 @@ namespace lldb_private {
/// This class provides extra information about a stack frame that was
/// provided by a specific stack frame recognizer. Right now, this class only
/// holds recognized arguments (via GetRecognizedArguments).

class RecognizedStackFrame
: public std::enable_shared_from_this<RecognizedStackFrame> {
public:
virtual ~RecognizedStackFrame() = default;

virtual lldb::ValueObjectListSP GetRecognizedArguments() {
return m_arguments;
}
virtual lldb::ValueObjectSP GetExceptionObject() {
return lldb::ValueObjectSP();
}
virtual lldb::StackFrameSP GetMostRelevantFrame() { return nullptr; };
virtual ~RecognizedStackFrame() = default;
virtual lldb::StackFrameSP GetMostRelevantFrame() { return nullptr; }

std::string GetStopDescription() { return m_stop_desc; }
/// Controls whether this frame should be filtered out when
/// displaying backtraces, for example.
virtual bool ShouldHide() { return false; }

protected:
lldb::ValueObjectListSP m_arguments;
Expand All @@ -53,7 +57,6 @@ class RecognizedStackFrame
/// A base class for frame recognizers. Subclasses (actual frame recognizers)
/// should implement RecognizeFrame to provide a RecognizedStackFrame for a
/// given stack frame.

class StackFrameRecognizer
: public std::enable_shared_from_this<StackFrameRecognizer> {
public:
Expand All @@ -73,10 +76,10 @@ class StackFrameRecognizer
/// Python implementation for frame recognizers. An instance of this class
/// tracks a particular Python classobject, which will be asked to recognize
/// stack frames.

class ScriptedStackFrameRecognizer : public StackFrameRecognizer {
lldb_private::ScriptInterpreter *m_interpreter;
lldb_private::StructuredData::ObjectSP m_python_object_sp;

std::string m_python_class;

public:
Expand Down Expand Up @@ -123,8 +126,13 @@ class StackFrameRecognizerManager {
lldb::StackFrameRecognizerSP GetRecognizerForFrame(lldb::StackFrameSP frame);

lldb::RecognizedStackFrameSP RecognizeFrame(lldb::StackFrameSP frame);
/// This number changes whenever the list of recognizers has been modified.
uint16_t GetGeneration() const { return m_generation; }

private:
/// Increase the generation counter.
void BumpGeneration();

struct RegisteredEntry {
uint32_t recognizer_id;
lldb::StackFrameRecognizerSP recognizer;
Expand All @@ -137,14 +145,14 @@ class StackFrameRecognizerManager {
};

std::deque<RegisteredEntry> m_recognizers;
uint16_t m_generation;
};

/// \class ValueObjectRecognizerSynthesizedValue
///
/// ValueObject subclass that presents the passed ValueObject as a recognized
/// value with the specified ValueType. Frame recognizers should return
/// instances of this class as the returned objects in GetRecognizedArguments().

class ValueObjectRecognizerSynthesizedValue : public ValueObject {
public:
static lldb::ValueObjectSP Create(ValueObject &parent, lldb::ValueType type) {
Expand Down
4 changes: 2 additions & 2 deletions lldb/include/lldb/Target/Thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -1128,11 +1128,11 @@ class Thread : public std::enable_shared_from_this<Thread>,

size_t GetStatus(Stream &strm, uint32_t start_frame, uint32_t num_frames,
uint32_t num_frames_with_source, bool stop_format,
bool only_stacks = false);
bool show_hidden, bool only_stacks = false);

size_t GetStackFrameStatus(Stream &strm, uint32_t first_frame,
uint32_t num_frames, bool show_frame_info,
uint32_t num_frames_with_source);
uint32_t num_frames_with_source, bool show_hidden);

// We need a way to verify that even though we have a thread in a shared
// pointer that the object itself is still valid. Currently this won't be the
Expand Down
13 changes: 13 additions & 0 deletions lldb/source/API/SBFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,19 @@ bool SBFrame::IsArtificial() const {
return false;
}

bool SBFrame::IsHidden() const {
LLDB_INSTRUMENT_VA(this);

std::unique_lock<std::recursive_mutex> lock;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock);

StackFrame *frame = exe_ctx.GetFramePtr();
if (frame)
return frame->IsHidden();

return false;
}

const char *SBFrame::GetFunctionName() {
LLDB_INSTRUMENT_VA(this);

Expand Down
3 changes: 2 additions & 1 deletion lldb/source/API/SBThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,8 @@ bool SBThread::GetStatus(SBStream &status) const {
ExecutionContext exe_ctx(m_opaque_sp.get(), lock);

if (exe_ctx.HasThreadScope()) {
exe_ctx.GetThreadPtr()->GetStatus(strm, 0, 1, 1, true);
exe_ctx.GetThreadPtr()->GetStatus(strm, 0, 1, 1, true,
/*show_hidden*/ true);
} else
strm.PutCString("No status");

Expand Down
4 changes: 2 additions & 2 deletions lldb/source/Commands/CommandCompletions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ void CommandCompletions::ThreadIndexes(CommandInterpreter &interpreter,
lldb::ThreadSP thread_sp;
for (uint32_t idx = 0; (thread_sp = threads.GetThreadAtIndex(idx)); ++idx) {
StreamString strm;
thread_sp->GetStatus(strm, 0, 1, 1, true);
thread_sp->GetStatus(strm, 0, 1, 1, true, /*show_hidden*/ true);
request.TryCompleteCurrentArg(std::to_string(thread_sp->GetIndexID()),
strm.GetString());
}
Expand Down Expand Up @@ -835,7 +835,7 @@ void CommandCompletions::ThreadIDs(CommandInterpreter &interpreter,
lldb::ThreadSP thread_sp;
for (uint32_t idx = 0; (thread_sp = threads.GetThreadAtIndex(idx)); ++idx) {
StreamString strm;
thread_sp->GetStatus(strm, 0, 1, 1, true);
thread_sp->GetStatus(strm, 0, 1, 1, true, /*show_hidden*/ true);
request.TryCompleteCurrentArg(std::to_string(thread_sp->GetID()),
strm.GetString());
}
Expand Down
23 changes: 23 additions & 0 deletions lldb/source/Commands/CommandObjectFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,29 @@ class CommandObjectFrameSelect : public CommandObjectParsed {
if (frame_idx == UINT32_MAX)
frame_idx = 0;

// If moving up/down by one, skip over hidden frames.
if (*m_options.relative_frame_offset == 1 ||
*m_options.relative_frame_offset == -1) {
uint32_t candidate_idx = frame_idx;
for (unsigned num_try = 0; num_try < 12; ++num_try) {
if (candidate_idx == 0 && *m_options.relative_frame_offset == -1) {
candidate_idx = UINT32_MAX;
break;
}
candidate_idx += *m_options.relative_frame_offset;
if (auto candidate_sp = thread->GetStackFrameAtIndex(candidate_idx)) {
if (candidate_sp->IsHidden())
continue;
// Now candidate_idx is the first non-hidden frame.
break;
}
candidate_idx = UINT32_MAX;
break;
};
if (candidate_idx != UINT32_MAX)
m_options.relative_frame_offset = candidate_idx - frame_idx;
}

if (*m_options.relative_frame_offset < 0) {
if (static_cast<int32_t>(frame_idx) >=
-*m_options.relative_frame_offset)
Expand Down
3 changes: 2 additions & 1 deletion lldb/source/Commands/CommandObjectMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1570,7 +1570,8 @@ class CommandObjectMemoryHistory : public CommandObjectParsed {

const bool stop_format = false;
for (auto thread : thread_list) {
thread->GetStatus(*output_stream, 0, UINT32_MAX, 0, stop_format);
thread->GetStatus(*output_stream, 0, UINT32_MAX, 0, stop_format,
/*should_filter*/ false);
}

result.SetStatus(eReturnStatusSuccessFinishResult);
Expand Down
19 changes: 15 additions & 4 deletions lldb/source/Commands/CommandObjectThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
"invalid boolean value for option '%c': %s", short_option,
option_arg.data());
} break;
case 'u':
m_filtered_backtrace = false;
break;
default:
llvm_unreachable("Unimplemented option");
}
Expand All @@ -99,6 +102,7 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
m_count = UINT32_MAX;
m_start = 0;
m_extended_backtrace = false;
m_filtered_backtrace = true;
}

llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
Expand All @@ -109,6 +113,7 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
uint32_t m_count;
uint32_t m_start;
bool m_extended_backtrace;
bool m_filtered_backtrace;
};

CommandObjectThreadBacktrace(CommandInterpreter &interpreter)
Expand All @@ -121,7 +126,10 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
"call stacks.\n"
"Use 'settings set frame-format' to customize the printing of "
"frames in the backtrace and 'settings set thread-format' to "
"customize the thread header.",
"customize the thread header.\n"
"Customizable frame recognizers may filter out less interesting "
"frames, which results in gaps in the numbering. "
"Use '-u' to see all frames.",
nullptr,
eCommandRequiresProcess | eCommandRequiresThread |
eCommandTryTargetAPILock | eCommandProcessMustBeLaunched |
Expand Down Expand Up @@ -199,7 +207,8 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
strm.PutChar('\n');
if (ext_thread_sp->GetStatus(strm, m_options.m_start,
m_options.m_count,
num_frames_with_source, stop_format)) {
num_frames_with_source, stop_format,
!m_options.m_filtered_backtrace)) {
DoExtendedBacktrace(ext_thread_sp.get(), result);
}
}
Expand Down Expand Up @@ -228,7 +237,8 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
const uint32_t num_frames_with_source = 0;
const bool stop_format = true;
if (!thread->GetStatus(strm, m_options.m_start, m_options.m_count,
num_frames_with_source, stop_format, only_stacks)) {
num_frames_with_source, stop_format,
!m_options.m_filtered_backtrace, only_stacks)) {
result.AppendErrorWithFormat(
"error displaying backtrace for thread: \"0x%4.4x\"\n",
thread->GetIndexID());
Expand Down Expand Up @@ -1392,7 +1402,8 @@ class CommandObjectThreadException : public CommandObjectIterateOverThreads {
const uint32_t num_frames_with_source = 0;
const bool stop_format = false;
exception_thread_sp->GetStatus(strm, 0, UINT32_MAX,
num_frames_with_source, stop_format);
num_frames_with_source, stop_format,
/*filtered*/ false);
}

return true;
Expand Down
2 changes: 2 additions & 0 deletions lldb/source/Commands/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,8 @@ let Command = "thread backtrace" in {
Arg<"FrameIndex">, Desc<"Frame in which to start the backtrace">;
def thread_backtrace_extended : Option<"extended", "e">, Group<1>,
Arg<"Boolean">, Desc<"Show the extended backtrace, if available">;
def thread_backtrace_unfiltered : Option<"unfiltered", "u">, Group<1>,
Desc<"Filter out frames according to installed frame recognizers">;
}

let Command = "thread step scope" in {
Expand Down
3 changes: 2 additions & 1 deletion lldb/source/Core/Debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,8 @@ void Debugger::HandleThreadEvent(const EventSP &event_sp) {
ThreadSP thread_sp(
Thread::ThreadEventData::GetThreadFromEvent(event_sp.get()));
if (thread_sp) {
thread_sp->GetStatus(*GetAsyncOutputStream(), 0, 1, 1, stop_format);
thread_sp->GetStatus(*GetAsyncOutputStream(), 0, 1, 1, stop_format,
/*show_hidden*/ true);
}
}
}
Expand Down
Loading

0 comments on commit 81af101

Please sign in to comment.