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`.

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.

rdar://126629381
  • Loading branch information
adrian-prantl committed Aug 15, 2024
1 parent ae059a1 commit 6289b79
Show file tree
Hide file tree
Showing 17 changed files with 222 additions and 34 deletions.
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 should_filter = true,
const char *frame_marker = nullptr);

protected:
Expand Down
12 changes: 6 additions & 6 deletions lldb/include/lldb/Target/StackFrameRecognizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,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 +56,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,7 +75,6 @@ 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;
Expand Down Expand Up @@ -144,7 +145,6 @@ class StackFrameRecognizerManager {
/// 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
5 changes: 3 additions & 2 deletions lldb/include/lldb/Target/Thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -1128,11 +1128,12 @@ 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 should_filter, 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 should_filter);

// 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
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,
/*should_filter*/ false);
} 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, /*should_filter*/ false);
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, /*should_filter*/ false);
request.TryCompleteCurrentArg(std::to_string(thread_sp->GetID()),
strm.GetString());
}
Expand Down
28 changes: 26 additions & 2 deletions lldb/source/Commands/CommandObjectFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,30 @@ 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 (auto recognized_frame_sp = candidate_sp->GetRecognizedFrame())
if (recognized_frame_sp->ShouldHide())
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 Expand Up @@ -318,7 +342,7 @@ class CommandObjectFrameSelect : public CommandObjectParsed {
}
}
}
} else {
} else {
if (command.GetArgumentCount() > 1) {
result.AppendErrorWithFormat(
"too many arguments; expected frame-index, saw '%s'.\n",
Expand All @@ -341,7 +365,7 @@ class CommandObjectFrameSelect : public CommandObjectParsed {
frame_idx = 0;
}
}
}
}

bool success = thread->SetSelectedFrameByIndexNoisily(
frame_idx, result.GetOutputStream());
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
25 changes: 18 additions & 7 deletions lldb/source/Commands/CommandObjectThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,20 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
"invalid integer value for option '%c': %s", short_option,
option_arg.data());
break;
case 'e': {
case 'e':
case 'f': {
bool success;
m_extended_backtrace =
OptionArgParser::ToBoolean(option_arg, false, &success);
if (!success)
bool value = OptionArgParser::ToBoolean(option_arg, false, &success);
if (!success) {
error.SetErrorStringWithFormat(
"invalid boolean value for option '%c': %s", short_option,
option_arg.data());
break;
}
if (short_option == 'e')
m_extended_backtrace = value;
else
m_filtered_backtrace = value;
} break;
default:
llvm_unreachable("Unimplemented option");
Expand All @@ -99,6 +105,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 +116,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 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
3 changes: 3 additions & 0 deletions lldb/source/Commands/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,9 @@ 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_full : Option<"filtered", "f">, Group<1>,
Arg<"Boolean">,
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,
/*should_filter*/ false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/SectionLoadList.h"
#include "lldb/Target/StackFrame.h"
#include "lldb/Target/StackFrameRecognizer.h"
#include "lldb/Target/ThreadPlanRunToAddress.h"
#include "lldb/Target/ThreadPlanStepInRange.h"
#include "lldb/Utility/Timer.h"
Expand All @@ -40,8 +41,52 @@ static ConstString g_coro_frame = ConstString("__coro_frame");

char CPPLanguageRuntime::ID = 0;

/// A frame recognizer that is isntalled to hide libc++ implementation
/// details from the backtrace.
class LibCXXFrameRecognizer : public StackFrameRecognizer {
RegularExpression m_hidden_function_regex;
RecognizedStackFrameSP m_hidden_frame;

struct LibCXXHiddenFrame : public RecognizedStackFrame {
bool ShouldHide() override { return true; }
};

public:
LibCXXFrameRecognizer()
: m_hidden_function_regex(
R"(^std::__1::(__function.*::operator\(\)|__invoke))"
R"((\[.*\])?)" // ABI tag.
R"(( const)?$)"), // const.
m_hidden_frame(new LibCXXHiddenFrame()) {}

std::string GetName() override {
return "libc++ frame recognizer";
}

lldb::RecognizedStackFrameSP
RecognizeFrame(lldb::StackFrameSP frame_sp) override {
if (!frame_sp)
return {};
auto &sc =
frame_sp->GetSymbolContext(lldb::eSymbolContextFunction);
if (!sc.function)
return {};

if (m_hidden_function_regex.Execute(sc.function->GetNameNoArguments()))
return m_hidden_frame;

return {};
}
};

CPPLanguageRuntime::CPPLanguageRuntime(Process *process)
: LanguageRuntime(process) {}
: LanguageRuntime(process) {
if (process)
process->GetTarget().GetFrameRecognizerManager().AddRecognizer(
StackFrameRecognizerSP(new LibCXXFrameRecognizer()), {},
std::make_shared<RegularExpression>("^std::__1::"),
/*first_instruction_only*/ false);
}

bool CPPLanguageRuntime::IsAllowedRuntimeValue(ConstString name) {
return name == g_this || name == g_promise || name == g_coro_frame;
Expand Down
7 changes: 4 additions & 3 deletions lldb/source/Target/Process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5545,7 +5545,8 @@ Process::RunThreadPlan(ExecutionContext &exe_ctx,
// Print a backtrace into the log so we can figure out where we are:
StreamString s;
s.PutCString("Thread state after unsuccessful completion: \n");
thread->GetStackFrameStatus(s, 0, UINT32_MAX, true, UINT32_MAX);
thread->GetStackFrameStatus(s, 0, UINT32_MAX, true, UINT32_MAX,
/*should_filter*/ false);
log->PutString(s.GetString());
}
// Restore the thread state if we are going to discard the plan execution.
Expand Down Expand Up @@ -5819,8 +5820,8 @@ size_t Process::GetThreadStatus(Stream &strm,
continue;
}
thread_sp->GetStatus(strm, start_frame, num_frames,
num_frames_with_source,
stop_format);
num_frames_with_source, stop_format,
/*should_filter*/ num_frames > 1);
++num_thread_infos_dumped;
} else {
Log *log = GetLog(LLDBLog::Process);
Expand Down
15 changes: 13 additions & 2 deletions lldb/source/Target/StackFrameList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ StackFrameList::GetStackFrameSPForStackFramePtr(StackFrame *stack_frame_ptr) {
size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
uint32_t num_frames, bool show_frame_info,
uint32_t num_frames_with_source,
bool show_unique,
bool show_unique, bool should_filter,
const char *selected_frame_marker) {
size_t num_frames_displayed = 0;

Expand All @@ -950,8 +950,8 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
buffer.insert(buffer.begin(), len, ' ');
unselected_marker = buffer.c_str();
}
bool filtered = false;
const char *marker = nullptr;

for (frame_idx = first_frame; frame_idx < last_frame; ++frame_idx) {
frame_sp = GetFrameAtIndex(frame_idx);
if (!frame_sp)
Expand All @@ -963,6 +963,15 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
else
marker = unselected_marker;
}

// Hide uninteresting frames unless it's the selected frame.
if (should_filter && frame_sp != selected_frame_sp)
if (auto recognized_frame_sp = frame_sp->GetRecognizedFrame())
if (recognized_frame_sp->ShouldHide()) {
filtered = true;
continue;
}

// Check for interruption here. If we're fetching arguments, this loop
// can go slowly:
Debugger &dbg = m_thread.GetProcess()->GetTarget().GetDebugger();
Expand All @@ -979,6 +988,8 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
++num_frames_displayed;
}

if (filtered)
strm << "Note: Some frames were hidden by frame recognizers\n";
strm.IndentLess();
return num_frames_displayed;
}
Loading

0 comments on commit 6289b79

Please sign in to comment.