Skip to content

Commit

Permalink
[lldb] Extend frame recognizers to hide frames from backtraces (#104523)
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


Example output. (Yes, my proof-of-concept recognizer could hide even
more frames if we had a method that returned the function name without
the return type or I used something that isn't based off regex, but it's
really only meant as an example).

before:
```
(lldb) thread backtrace --filtered=false
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100001f04 a.out`foo(x=1, y=1) at main.cpp:4:10
    frame #1: 0x0000000100003a00 a.out`decltype(std::declval<int (*&)(int, int)>()(std::declval<int>(), std::declval<int>())) std::__1::__invoke[abi:se200000]<int (*&)(int, int), int, int>(__f=0x000000016fdff280, __args=0x000000016fdff224, __args=0x000000016fdff220) at invoke.h:149:25
    frame #2: 0x000000010000399c a.out`int std::__1::__invoke_void_return_wrapper<int, false>::__call[abi:se200000]<int (*&)(int, int), int, int>(__args=0x000000016fdff280, __args=0x000000016fdff224, __args=0x000000016fdff220) at invoke.h:216:12
    frame #3: 0x0000000100003968 a.out`std::__1::__function::__alloc_func<int (*)(int, int), std::__1::allocator<int (*)(int, int)>, int (int, int)>::operator()[abi:se200000](this=0x000000016fdff280, __arg=0x000000016fdff224, __arg=0x000000016fdff220) at function.h:171:12
    frame #4: 0x00000001000026bc a.out`std::__1::__function::__func<int (*)(int, int), std::__1::allocator<int (*)(int, int)>, int (int, int)>::operator()(this=0x000000016fdff278, __arg=0x000000016fdff224, __arg=0x000000016fdff220) at function.h:313:10
    frame #5: 0x0000000100003c38 a.out`std::__1::__function::__value_func<int (int, int)>::operator()[abi:se200000](this=0x000000016fdff278, __args=0x000000016fdff224, __args=0x000000016fdff220) const at function.h:430:12
    frame #6: 0x0000000100002038 a.out`std::__1::function<int (int, int)>::operator()(this= Function = foo(int, int) , __arg=1, __arg=1) const at function.h:989:10
    frame #7: 0x0000000100001f64 a.out`main(argc=1, argv=0x000000016fdff4f8) at main.cpp:9:10
    frame #8: 0x0000000183cdf154 dyld`start + 2476
(lldb) 
```

after

```
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100001f04 a.out`foo(x=1, y=1) at main.cpp:4:10
    frame #1: 0x0000000100003a00 a.out`decltype(std::declval<int (*&)(int, int)>()(std::declval<int>(), std::declval<int>())) std::__1::__invoke[abi:se200000]<int (*&)(int, int), int, int>(__f=0x000000016fdff280, __args=0x000000016fdff224, __args=0x000000016fdff220) at invoke.h:149:25
    frame #2: 0x000000010000399c a.out`int std::__1::__invoke_void_return_wrapper<int, false>::__call[abi:se200000]<int (*&)(int, int), int, int>(__args=0x000000016fdff280, __args=0x000000016fdff224, __args=0x000000016fdff220) at invoke.h:216:12
    frame #6: 0x0000000100002038 a.out`std::__1::function<int (int, int)>::operator()(this= Function = foo(int, int) , __arg=1, __arg=1) const at function.h:989:10
    frame #7: 0x0000000100001f64 a.out`main(argc=1, argv=0x000000016fdff4f8) at main.cpp:9:10
    frame #8: 0x0000000183cdf154 dyld`start + 2476
Note: Some frames were hidden by frame recognizers
```
  • Loading branch information
adrian-prantl authored Aug 20, 2024
1 parent 1e9d002 commit f01f80c
Show file tree
Hide file tree
Showing 32 changed files with 424 additions and 75 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
36 changes: 22 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,36 @@ 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
21 changes: 15 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 <cstdint>
#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,14 @@ class StackFrameRecognizerManager {
lldb::StackFrameRecognizerSP GetRecognizerForFrame(lldb::StackFrameSP frame);

lldb::RecognizedStackFrameSP RecognizeFrame(lldb::StackFrameSP frame);
/// Returns a number that 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 +146,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
15 changes: 13 additions & 2 deletions lldb/source/API/SBFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1195,13 +1195,24 @@ bool SBFrame::IsArtificial() const {
std::unique_lock<std::recursive_mutex> lock;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock);

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

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);

if (StackFrame *frame = exe_ctx.GetFramePtr())
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
24 changes: 24 additions & 0 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;
const unsigned max_depth = 12;
for (unsigned num_try = 0; num_try < max_depth; ++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
Loading

0 comments on commit f01f80c

Please sign in to comment.