diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h index 8637234c4fb95..47bcd729abcdd 100644 --- a/lldb/include/lldb/Target/ExecutionContext.h +++ b/lldb/include/lldb/Target/ExecutionContext.h @@ -13,10 +13,13 @@ #include "lldb/Host/ProcessRunLock.h" #include "lldb/Target/StackID.h" +#include "lldb/Target/SyntheticFrameProvider.h" #include "lldb/lldb-private.h" namespace lldb_private { +struct StoppedExecutionContext; + //===----------------------------------------------------------------------===// /// Execution context objects refer to objects in the execution of the program /// that is being debugged. The consist of one or more of the following @@ -270,9 +273,12 @@ class ExecutionContextRef { void ClearFrame() { m_stack_id.Clear(); - m_frame_list_wp.reset(); + m_frame_list_id.reset(); } + friend llvm::Expected + GetStoppedExecutionContext(const ExecutionContextRef *exe_ctx_ref_ptr); + protected: // Member variables lldb::TargetWP m_target_wp; ///< A weak reference to a target @@ -283,13 +289,10 @@ class ExecutionContextRef { /// backing object changes StackID m_stack_id; ///< The stack ID that this object refers to in case the ///< backing object changes - mutable lldb::StackFrameListWP - m_frame_list_wp; ///< Weak reference to the - ///< frame list that contains - ///< this frame. If we can create a valid - ///< StackFrameListSP from it, we must use it to resolve - ///< the StackID, otherwise, we should ask the Thread's - ///< StackFrameList. + /// A map of identifiers to scripted frame providers used in this thread. + mutable std::optional< + std::pair> + m_frame_list_id; }; /// \class ExecutionContext ExecutionContext.h diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h index 46922448d6e59..5cba9afe2a7e8 100644 --- a/lldb/include/lldb/Target/StackFrame.h +++ b/lldb/include/lldb/Target/StackFrame.h @@ -542,17 +542,17 @@ class StackFrame : public ExecutionContextScope, virtual lldb::RecognizedStackFrameSP GetRecognizedFrame(); - /// Get the StackFrameList that contains this frame. + /// Get the identifier of the StackFrameList that contains this frame. /// - /// Returns the StackFrameList that contains this frame, allowing + /// Returns the StackFrameList identifier that contains this frame, allowing /// frames to resolve execution contexts without calling /// Thread::GetStackFrameList(), which can cause circular dependencies /// during frame provider initialization. /// /// \return - /// The StackFrameList that contains this frame, or nullptr if not set. - virtual lldb::StackFrameListSP GetContainingStackFrameList() const { - return m_frame_list_wp.lock(); + /// The identifier of the containing StackFrameList + lldb::frame_list_id_t GetContainingStackFrameListIdentifier() const { + return m_frame_list_id; } protected: @@ -598,8 +598,8 @@ class StackFrame : public ExecutionContextScope, /// 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::frame_list_id_t m_frame_list_id = 0; lldb::VariableListSP m_variable_list_sp; - lldb::StackFrameListWP m_frame_list_wp; /// Value objects for each variable in m_variable_list_sp. ValueObjectList m_variable_list_value_objects; std::optional m_recognized_frame_sp; diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h index c096fe3ff61a0..715781abb83a3 100644 --- a/lldb/include/lldb/Target/StackFrameList.h +++ b/lldb/include/lldb/Target/StackFrameList.h @@ -24,7 +24,8 @@ class StackFrameList : public std::enable_shared_from_this { public: // Constructors and Destructors StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp, - bool show_inline_frames); + bool show_inline_frames, + lldb::frame_list_id_t provider_id = 0); virtual ~StackFrameList(); @@ -104,6 +105,9 @@ class StackFrameList : public std::enable_shared_from_this { /// Get the thread associated with this frame list. Thread &GetThread() const { return m_thread; } + /// Get the unique identifier for this frame list. + lldb::frame_list_id_t GetIdentifier() const { return m_identifier; } + protected: friend class Thread; friend class ScriptedFrameProvider; @@ -212,6 +216,9 @@ class StackFrameList : public std::enable_shared_from_this { /// Whether or not to show synthetic (inline) frames. Immutable. const bool m_show_inlined_frames; + /// Unique identifier for this frame list instance. + lldb::frame_list_id_t m_identifier = 0; + /// Returns true if fetching frames was interrupted, false otherwise. virtual bool FetchFramesUpTo(uint32_t end_idx, InterruptionControl allow_interrupt); @@ -244,7 +251,8 @@ class SyntheticStackFrameList : public StackFrameList { SyntheticStackFrameList(Thread &thread, lldb::StackFrameListSP input_frames, const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames, - lldb::SyntheticFrameProviderSP provider_sp); + lldb::SyntheticFrameProviderSP provider_sp, + uint64_t provider_id); protected: /// Override FetchFramesUpTo to lazily return frames from the provider diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h index bc1bec57bee5f..9cc86a37c63e5 100644 --- a/lldb/include/lldb/Target/Thread.h +++ b/lldb/include/lldb/Target/Thread.h @@ -19,6 +19,7 @@ #include "lldb/Target/ExecutionContextScope.h" #include "lldb/Target/RegisterCheckpoint.h" #include "lldb/Target/StackFrameList.h" +#include "lldb/Target/SyntheticFrameProvider.h" #include "lldb/Utility/Broadcaster.h" #include "lldb/Utility/CompletionRequest.h" #include "lldb/Utility/Event.h" @@ -26,6 +27,7 @@ #include "lldb/Utility/UnimplementedError.h" #include "lldb/Utility/UserID.h" #include "lldb/lldb-private.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/Support/MemoryBuffer.h" #define LLDB_THREAD_MAX_STOP_EXC_DATA 8 @@ -1297,12 +1299,18 @@ class Thread : public std::enable_shared_from_this, lldb::StackFrameListSP GetStackFrameList(); + /// Get a frame list by its unique identifier. + lldb::StackFrameListSP GetFrameListByIdentifier(lldb::frame_list_id_t id); + llvm::Error LoadScriptedFrameProvider(const ScriptedFrameProviderDescriptor &descriptor); + llvm::Expected + GetScriptedFrameProviderDescriptorForID(lldb::frame_list_id_t id) const; + void ClearScriptedFrameProvider(); - const llvm::SmallVector & + const llvm::DenseMap & GetFrameProviders() const { return m_frame_providers; } @@ -1384,6 +1392,8 @@ class Thread : public std::enable_shared_from_this, m_state_mutex; ///< Multithreaded protection for m_state. mutable std::recursive_mutex m_frame_mutex; ///< Multithreaded protection for m_state. + lldb::StackFrameListSP + m_unwinder_frames_sp; ///< The unwinder frame list (ID 0). lldb::StackFrameListSP m_curr_frames_sp; ///< The stack frames that get lazily ///populated after a thread stops. lldb::StackFrameListSP m_prev_frames_sp; ///< The previous stack frames from @@ -1410,8 +1420,23 @@ class Thread : public std::enable_shared_from_this, /// The Thread backed by this thread, if any. lldb::ThreadWP m_backed_thread; - /// The Scripted Frame Providers for this thread. - llvm::SmallVector m_frame_providers; + /// Map from frame list ID to its frame provider. + /// Cleared in ClearStackFrames(), repopulated in GetStackFrameList(). + llvm::DenseMap + m_frame_providers; + + /// Ordered chain of provider IDs. + /// Persists across ClearStackFrames() to maintain stable provider IDs. + std::vector> + m_provider_chain_ids; + + /// Map from frame list identifier to frame list weak pointer. + mutable llvm::DenseMap + m_frame_lists_by_id; + + /// Counter for assigning unique provider IDs. Starts at 1 since 0 is + /// reserved for normal unwinder frames. Persists across ClearStackFrames. + lldb::frame_list_id_t m_next_provider_id = 1; private: bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info diff --git a/lldb/include/lldb/lldb-defines.h b/lldb/include/lldb/lldb-defines.h index 52bf7c5cce947..8e1029387d2da 100644 --- a/lldb/include/lldb/lldb-defines.h +++ b/lldb/include/lldb/lldb-defines.h @@ -89,6 +89,7 @@ #define LLDB_INVALID_PROCESS_ID 0 #define LLDB_INVALID_THREAD_ID 0 #define LLDB_INVALID_FRAME_ID UINT32_MAX +#define LLDB_UNWINDER_FRAME_LIST_ID 0 #define LLDB_INVALID_SIGNAL_NUMBER INT32_MAX #define LLDB_INVALID_SYMBOL_ID UINT32_MAX #define LLDB_INVALID_OFFSET UINT64_MAX // Must match max of lldb::offset_t diff --git a/lldb/include/lldb/lldb-types.h b/lldb/include/lldb/lldb-types.h index e309fc8833ce9..bb4c34ef8e1f5 100644 --- a/lldb/include/lldb/lldb-types.h +++ b/lldb/include/lldb/lldb-types.h @@ -83,6 +83,7 @@ typedef uint64_t user_id_t; typedef uint64_t pid_t; typedef uint64_t tid_t; typedef uint64_t offset_t; +typedef uint32_t frame_list_id_t; typedef int32_t break_id_t; typedef int32_t watch_id_t; typedef uint32_t wp_resource_id_t; diff --git a/lldb/source/Target/ExecutionContext.cpp b/lldb/source/Target/ExecutionContext.cpp index b16ff26266c53..e4b2f07d8d8d1 100644 --- a/lldb/source/Target/ExecutionContext.cpp +++ b/lldb/source/Target/ExecutionContext.cpp @@ -160,6 +160,15 @@ lldb_private::GetStoppedExecutionContext( auto thread_sp = exe_ctx_ref_ptr->GetThreadSP(); auto frame_sp = exe_ctx_ref_ptr->GetFrameSP(); + + if (!frame_sp && exe_ctx_ref_ptr->m_frame_list_id) { + return llvm::createStringError( + "attempted to create a StoppedExecutionContext but " + "ScriptedFrameProvider (name = %s - id = %u) is no longer available", + exe_ctx_ref_ptr->m_frame_list_id->first.GetName().str().c_str(), + exe_ctx_ref_ptr->m_frame_list_id->second); + } + return StoppedExecutionContext(target_sp, process_sp, thread_sp, frame_sp, std::move(api_lock), std::move(stop_locker)); } @@ -466,12 +475,25 @@ operator=(const ExecutionContext &exe_ctx) { else m_tid = LLDB_INVALID_THREAD_ID; lldb::StackFrameSP frame_sp(exe_ctx.GetFrameSP()); - if (frame_sp) { - m_stack_id = frame_sp->GetStackID(); - m_frame_list_wp = frame_sp->GetContainingStackFrameList(); + + if (frame_sp && thread_sp) { + lldb::frame_list_id_t frame_list_id = + frame_sp->GetContainingStackFrameListIdentifier(); + auto frame_list_descriptor_or_err = + thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id); + if (frame_list_descriptor_or_err) { + m_stack_id = frame_sp->GetStackID(); + m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id}; + } else { + LLDB_LOG_ERROR(GetLog(LLDBLog::Process), + frame_list_descriptor_or_err.takeError(), + "Failed to fetch scripted frame provider descriptor: {0}"); + m_stack_id.Clear(); + m_frame_list_id.reset(); + } } else { m_stack_id.Clear(); - m_frame_list_wp.reset(); + m_frame_list_id.reset(); } return *this; } @@ -512,11 +534,25 @@ void ExecutionContextRef::SetThreadSP(const lldb::ThreadSP &thread_sp) { } void ExecutionContextRef::SetFrameSP(const lldb::StackFrameSP &frame_sp) { - if (frame_sp) { + if (!frame_sp) { + Clear(); + return; + } + + lldb::ThreadSP thread_sp = frame_sp->GetThread(); + lldb::frame_list_id_t frame_list_id = + frame_sp->GetContainingStackFrameListIdentifier(); + auto frame_list_descriptor_or_err = + thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id); + + if (frame_list_descriptor_or_err) { m_stack_id = frame_sp->GetStackID(); - m_frame_list_wp = frame_sp->GetContainingStackFrameList(); - SetThreadSP(frame_sp->GetThread()); + m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id}; + SetThreadSP(thread_sp); } else { + LLDB_LOG_ERROR(GetLog(LLDBLog::Process), + frame_list_descriptor_or_err.takeError(), + "Failed to fetch scripted frame provider descriptor: {0}"); ClearFrame(); ClearThread(); m_process_wp.reset(); @@ -641,21 +677,23 @@ lldb::ThreadSP ExecutionContextRef::GetThreadSP() const { } lldb::StackFrameSP ExecutionContextRef::GetFrameSP() const { - if (m_stack_id.IsValid()) { - // Try the remembered frame list first to avoid circular dependencies - // during frame provider initialization. - if (auto frame_list_sp = m_frame_list_wp.lock()) { + lldb::ThreadSP thread_sp(GetThreadSP()); + if (!thread_sp || !m_stack_id.IsValid()) + return lldb::StackFrameSP(); + + // Try the remembered frame list first to avoid circular dependencies + // during frame provider initialization. + if (m_frame_list_id) { + if (auto frame_list_sp = + thread_sp->GetFrameListByIdentifier(m_frame_list_id->second)) { if (auto frame_sp = frame_list_sp->GetFrameWithStackID(m_stack_id)) return frame_sp; } - - // Fallback: ask the thread, which might re-trigger the frame provider - // initialization. - lldb::ThreadSP thread_sp(GetThreadSP()); - if (thread_sp) - return thread_sp->GetFrameWithStackID(m_stack_id); } - return lldb::StackFrameSP(); + + // Fallback: ask the thread, which might re-trigger the frame provider + // initialization. + return thread_sp->GetFrameWithStackID(m_stack_id); } ExecutionContext diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp index 1ad269e8783cc..4f4b06f30460b 100644 --- a/lldb/source/Target/StackFrameList.cpp +++ b/lldb/source/Target/StackFrameList.cpp @@ -38,12 +38,13 @@ using namespace lldb_private; // StackFrameList constructor StackFrameList::StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp, - bool show_inline_frames) + bool show_inline_frames, + lldb::frame_list_id_t provider_id) : m_thread(thread), m_prev_frames_sp(prev_frames_sp), m_frames(), m_selected_frame_idx(), m_concrete_frames_fetched(0), m_current_inlined_depth(UINT32_MAX), m_current_inlined_pc(LLDB_INVALID_ADDRESS), - m_show_inlined_frames(show_inline_frames) { + m_show_inlined_frames(show_inline_frames), m_identifier(provider_id) { if (prev_frames_sp) { m_current_inlined_depth = prev_frames_sp->m_current_inlined_depth; m_current_inlined_pc = prev_frames_sp->m_current_inlined_pc; @@ -59,8 +60,8 @@ StackFrameList::~StackFrameList() { SyntheticStackFrameList::SyntheticStackFrameList( Thread &thread, lldb::StackFrameListSP input_frames, const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames, - lldb::SyntheticFrameProviderSP provider_sp) - : StackFrameList(thread, prev_frames_sp, show_inline_frames), + lldb::SyntheticFrameProviderSP provider_sp, uint64_t provider_id) + : StackFrameList(thread, prev_frames_sp, show_inline_frames, provider_id), m_input_frames(std::move(input_frames)), m_provider(std::move(provider_sp)) {} @@ -70,12 +71,25 @@ bool SyntheticStackFrameList::FetchFramesUpTo( size_t num_synthetic_frames = 0; // Use the provider to generate frames lazily. if (m_provider) { + // Get starting index under lock. + uint32_t start_idx = 0; + { + std::shared_lock guard(m_list_mutex); + start_idx = m_frames.size(); + } + // Keep fetching until we reach end_idx or the provider returns an error. - for (uint32_t idx = m_frames.size(); idx <= end_idx; idx++) { + for (uint32_t idx = start_idx; idx <= end_idx; idx++) { if (allow_interrupt && m_thread.GetProcess()->GetTarget().GetDebugger().InterruptRequested()) return true; + + // Call Python WITHOUT holding lock - prevents deadlock. auto frame_or_err = m_provider->GetFrameAtIndex(idx); + + // Acquire lock to modify m_frames. + std::unique_lock guard(m_list_mutex); + if (!frame_or_err) { // Provider returned error - we've reached the end. LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), frame_or_err.takeError(), @@ -89,7 +103,7 @@ bool SyntheticStackFrameList::FetchFramesUpTo( GetThread().GetProcess().get()); // Set the frame list weak pointer so ExecutionContextRef can resolve // the frame without calling Thread::GetStackFrameList(). - frame_sp->m_frame_list_wp = shared_from_this(); + frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(frame_sp); } @@ -375,7 +389,7 @@ void StackFrameList::SynthesizeTailCallFrames(StackFrame &next_frame) { m_thread.shared_from_this(), frame_idx, concrete_frame_idx, cfa, cfa_is_valid, pc, StackFrame::Kind::Regular, artificial, behaves_like_zeroth_frame, &sc); - synth_frame->m_frame_list_wp = shared_from_this(); + synth_frame->m_frame_list_id = GetIdentifier(); m_frames.push_back(synth_frame); LLDB_LOG(log, "Pushed frame {0} at {1:x}", callee->GetDisplayName(), pc); } @@ -410,6 +424,10 @@ bool StackFrameList::GetFramesUpTo(uint32_t end_idx, return false; } + // Release lock before FetchFramesUpTo which may call Python. + // FetchFramesUpTo will acquire locks as needed. + guard.unlock(); + // We're adding concrete and inlined frames now: was_interrupted = FetchFramesUpTo(end_idx, allow_interrupt); @@ -491,7 +509,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, unwind_frame_sp = std::make_shared( m_thread.shared_from_this(), m_frames.size(), idx, reg_ctx_sp, cfa, pc, behaves_like_zeroth_frame, nullptr); - unwind_frame_sp->m_frame_list_wp = shared_from_this(); + unwind_frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(unwind_frame_sp); } } else { @@ -526,7 +544,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, // although its concrete index will stay the same. SynthesizeTailCallFrames(*unwind_frame_sp.get()); - unwind_frame_sp->m_frame_list_wp = shared_from_this(); + unwind_frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(unwind_frame_sp); } @@ -551,7 +569,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, unwind_frame_sp->GetRegisterContextSP(), cfa, next_frame_address, behaves_like_zeroth_frame, &next_frame_sc)); - frame_sp->m_frame_list_wp = shared_from_this(); + frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(frame_sp); unwind_sc = next_frame_sc; curr_frame_address = next_frame_address; @@ -608,7 +626,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, prev_frame->UpdatePreviousFrameFromCurrentFrame(*curr_frame); // Now copy the fixed up previous frame into the current frames so the // pointer doesn't change. - prev_frame_sp->m_frame_list_wp = shared_from_this(); + prev_frame_sp->m_frame_list_id = GetIdentifier(); m_frames[curr_frame_idx] = prev_frame_sp; #if defined(DEBUG_STACK_FRAMES) diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp index 70d8650662348..2c95c2d209b45 100644 --- a/lldb/source/Target/Thread.cpp +++ b/lldb/source/Target/Thread.cpp @@ -29,7 +29,6 @@ #include "lldb/Target/ScriptedThreadPlan.h" #include "lldb/Target/StackFrameRecognizer.h" #include "lldb/Target/StopInfo.h" -#include "lldb/Target/SyntheticFrameProvider.h" #include "lldb/Target/SystemRuntime.h" #include "lldb/Target/Target.h" #include "lldb/Target/ThreadPlan.h" @@ -57,6 +56,8 @@ #include "lldb/ValueObject/ValueObjectConstResult.h" #include "lldb/lldb-enumerations.h" +#include "llvm/Support/MathExtras.h" + #include #include @@ -262,7 +263,10 @@ void Thread::DestroyThread() { std::lock_guard guard(m_frame_mutex); m_curr_frames_sp.reset(); m_prev_frames_sp.reset(); + m_unwinder_frames_sp.reset(); m_frame_providers.clear(); + m_provider_chain_ids.clear(); + m_frame_lists_by_id.clear(); m_prev_framezero_pc.reset(); } @@ -1465,16 +1469,15 @@ StackFrameListSP Thread::GetStackFrameList() { const auto &descriptors = target.GetScriptedFrameProviderDescriptors(); // Collect all descriptors that apply to this thread. - std::vector - applicable_descriptors; + std::vector thread_descriptors; for (const auto &entry : descriptors) { const ScriptedFrameProviderDescriptor &descriptor = entry.second; if (descriptor.IsValid() && descriptor.AppliesToThread(*this)) - applicable_descriptors.push_back(&descriptor); + thread_descriptors.push_back(&descriptor); } // Sort by priority (lower number = higher priority). - llvm::sort(applicable_descriptors, + llvm::sort(thread_descriptors, [](const ScriptedFrameProviderDescriptor *a, const ScriptedFrameProviderDescriptor *b) { // nullopt (no priority) sorts last (UINT32_MAX). @@ -1484,7 +1487,7 @@ StackFrameListSP Thread::GetStackFrameList() { }); // Load ALL matching providers in priority order. - for (const auto *descriptor : applicable_descriptors) { + for (const auto *descriptor : thread_descriptors) { if (llvm::Error error = LoadScriptedFrameProvider(*descriptor)) { LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), std::move(error), "Failed to load scripted frame provider: {0}"); @@ -1495,58 +1498,123 @@ StackFrameListSP Thread::GetStackFrameList() { } // Create the frame list based on whether we have providers. - if (!m_frame_providers.empty()) { + if (!m_provider_chain_ids.empty()) { // We have providers - use the last one in the chain. // The last provider has already been chained with all previous providers. - StackFrameListSP input_frames = m_frame_providers.back()->GetInputFrames(); - m_curr_frames_sp = std::make_shared( - *this, input_frames, m_prev_frames_sp, true, m_frame_providers.back()); + auto [last_desc, last_id] = m_provider_chain_ids.back(); + auto it = m_frame_providers.find(last_id); + if (it != m_frame_providers.end()) { + SyntheticFrameProviderSP last_provider = it->second; + StackFrameListSP input_frames = last_provider->GetInputFrames(); + m_curr_frames_sp = std::make_shared( + *this, input_frames, m_prev_frames_sp, true, last_provider, last_id); + } else { + LLDB_LOG(GetLog(LLDBLog::Thread), + "Missing frame provider (id = {0}) in Thread #{1:x}}", last_id, + GetID()); + } + } + + if (!m_curr_frames_sp) { + // No provider - use normal unwinder frames with stable ID = 0. + m_unwinder_frames_sp = std::make_shared( + *this, m_prev_frames_sp, true, /*provider_id=*/0); + m_curr_frames_sp = m_unwinder_frames_sp; } else { - // No provider - use normal unwinder frames. - m_curr_frames_sp = - std::make_shared(*this, m_prev_frames_sp, true); + // Register this frame list by its identifier for later lookup. + m_frame_lists_by_id.insert( + {m_curr_frames_sp->GetIdentifier(), m_curr_frames_sp}); } return m_curr_frames_sp; } +lldb::StackFrameListSP +Thread::GetFrameListByIdentifier(lldb::frame_list_id_t id) { + std::lock_guard guard(m_frame_mutex); + + // ID 0 is reserved for the unwinder frame list. Always return the unwinder + // frame list for ID 0. + if (id == 0) { + return m_unwinder_frames_sp; + } + + auto it = m_frame_lists_by_id.find(id); + if (it != m_frame_lists_by_id.end()) { + return it->second.lock(); + } + return nullptr; +} + llvm::Error Thread::LoadScriptedFrameProvider( const ScriptedFrameProviderDescriptor &descriptor) { std::lock_guard guard(m_frame_mutex); - // Create input frames for this provider: - // - If no providers exist yet, use real unwinder frames. - // - If providers exist, wrap the previous provider in a - // SyntheticStackFrameList. - // This creates the chain: each provider's OUTPUT becomes the next - // provider's INPUT. - StackFrameListSP new_provider_input_frames; + StackFrameListSP input_frames; if (m_frame_providers.empty()) { - // First provider gets real unwinder frames. - new_provider_input_frames = - std::make_shared(*this, m_prev_frames_sp, true); + // First provider gets real unwinder frames with stable ID = 0. + m_unwinder_frames_sp = + std::make_shared(*this, m_prev_frames_sp, true, + /*provider_id=*/0); + input_frames = m_unwinder_frames_sp; } else { - // Subsequent providers get the previous provider's OUTPUT. - // We create a SyntheticStackFrameList that wraps the previous provider. - SyntheticFrameProviderSP prev_provider = m_frame_providers.back(); - StackFrameListSP prev_provider_frames = prev_provider->GetInputFrames(); - new_provider_input_frames = std::make_shared( - *this, prev_provider_frames, m_prev_frames_sp, true, prev_provider); - } - - auto provider_or_err = SyntheticFrameProvider::CreateInstance( - new_provider_input_frames, descriptor); + // Subsequent providers wrap the previous provider. + auto [last_desc, last_id] = m_provider_chain_ids.back(); + auto it = m_frame_providers.find(last_id); + if (it == m_frame_providers.end()) + return llvm::createStringError("Previous frame provider not found"); + SyntheticFrameProviderSP last_provider = it->second; + StackFrameListSP last_provider_frames = last_provider->GetInputFrames(); + input_frames = std::make_shared( + *this, last_provider_frames, m_prev_frames_sp, true, last_provider, + last_id); + } + + auto provider_or_err = + SyntheticFrameProvider::CreateInstance(input_frames, descriptor); if (!provider_or_err) return provider_or_err.takeError(); - // Append to the chain. - m_frame_providers.push_back(*provider_or_err); + if (m_next_provider_id == std::numeric_limits::max()) + m_next_provider_id = 1; + else + m_next_provider_id++; + + lldb::frame_list_id_t provider_id = m_next_provider_id; + m_frame_providers.insert({provider_id, *provider_or_err}); + + // Add to the provider chain. + m_provider_chain_ids.push_back({descriptor, provider_id}); + return llvm::Error::success(); } +llvm::Expected +Thread::GetScriptedFrameProviderDescriptorForID( + lldb::frame_list_id_t id) const { + if (id == LLDB_UNWINDER_FRAME_LIST_ID) + return ScriptedFrameProviderDescriptor(); + + auto it = llvm::find_if( + m_provider_chain_ids, + [id](const std::pair &provider_id_pair) { + return provider_id_pair.second == id; + }); + + if (it == m_provider_chain_ids.end()) + return llvm::createStringError( + "Couldn't find ScriptedFrameProviderDescriptor for id = %u.", id); + + return it->first; +} + void Thread::ClearScriptedFrameProvider() { std::lock_guard guard(m_frame_mutex); m_frame_providers.clear(); + m_provider_chain_ids.clear(); + m_next_provider_id = 1; // Reset counter. + m_unwinder_frames_sp.reset(); m_curr_frames_sp.reset(); m_prev_frames_sp.reset(); } @@ -1570,8 +1638,13 @@ void Thread::ClearStackFrames() { if (m_curr_frames_sp && m_curr_frames_sp->WereAllFramesFetched()) m_prev_frames_sp.swap(m_curr_frames_sp); m_curr_frames_sp.reset(); + m_unwinder_frames_sp.reset(); + // Clear the provider instances, but keep the chain configuration + // (m_provider_chain_ids and m_next_provider_id) so provider IDs + // remain stable across ClearStackFrames() calls. m_frame_providers.clear(); + m_frame_lists_by_id.clear(); m_extended_info.reset(); m_extended_info_fetched = false; } diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py index 7dd74013b90f8..8397d60eedbb5 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -735,8 +735,11 @@ def test_get_values(self): """Test a frame that provides values.""" self.build() # Set the breakpoint after the variable_in_main variable exists and can be queried. - target, process, thread, bkpt = lldbutil.run_to_line_breakpoint( - self, lldb.SBFileSpec(self.source), 35, only_one_thread=False + target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, + "Breakpoint for variable tests", + lldb.SBFileSpec(self.source), + only_one_thread=False, ) # Get original frame count. @@ -771,7 +774,8 @@ def test_get_values(self): self.assertIsNotNone(frame0) # Get every variable visible at this point variables = frame0.GetVariables(True, True, True, False) - self.assertTrue(variables.IsValid() and variables.GetSize() == 1) + self.assertTrue(variables.IsValid()) + self.assertEqual(variables.GetSize(), 1) # Check that we can get values from paths. `_handler_one` is a special # value we provide through only our expression handler in the frame @@ -783,3 +787,326 @@ def test_get_values(self): self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned) varp1 = frame0.GetValueForVariablePath("variable_in_main + 1") self.assertEqual(varp1.unsigned, 124) + + def test_frame_validity_after_step(self): + """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping. + + This test verifies that ExecutionContextRef properly handles frame list identifiers + when the underlying stack changes. After stepping, old frame references should become + invalid gracefully without crashing. + """ + self.build() + target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False + ) + + # Import the test frame provider. + script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") + self.runCmd("command script import " + script_path) + + # Register a provider that prepends synthetic frames. + error = lldb.SBError() + provider_id = target.RegisterScriptedFrameProvider( + "test_frame_providers.PrependFrameProvider", + lldb.SBStructuredData(), + error, + ) + self.assertTrue(error.Success(), f"Failed to register provider: {error}") + + # Get frame references before stepping. + frame0_before = thread.GetFrameAtIndex(0) + frame1_before = thread.GetFrameAtIndex(1) + frame2_before = thread.GetFrameAtIndex(2) + + self.assertIsNotNone(frame0_before) + self.assertIsNotNone(frame1_before) + self.assertIsNotNone(frame2_before) + + # Verify frames are valid and have expected PCs. + self.assertTrue(frame0_before.IsValid(), "Frame 0 should be valid before step") + self.assertTrue(frame1_before.IsValid(), "Frame 1 should be valid before step") + self.assertTrue(frame2_before.IsValid(), "Frame 2 should be valid before step") + + pc0_before = frame0_before.GetPC() + pc1_before = frame1_before.GetPC() + pc2_before = frame2_before.GetPC() + + self.assertEqual(pc0_before, 0x9000, "Frame 0 should have synthetic PC 0x9000") + self.assertEqual(pc1_before, 0xA000, "Frame 1 should have synthetic PC 0xA000") + + # Step the thread, which will invalidate the old frame list. + thread.StepInstruction(False) + + # After stepping, the frame list has changed. Old frame references should + # detect this and become invalid, but shouldn't crash. + # The key here is that GetPC() and other operations should handle the + # "frame provider no longer available" case gracefully. + + # Try to access the old frames - they should either: + # 1. Return invalid/default values gracefully, or + # 2. Still work if the frame provider is re-applied. + + # Get new frames after stepping. + frame0_after = thread.GetFrameAtIndex(0) + self.assertIsNotNone(frame0_after) + self.assertTrue( + frame0_after.IsValid(), "New frame 0 should be valid after step" + ) + + # The old frame references might or might not be valid depending on whether + # the frame provider is still active. What's important is that accessing + # them doesn't crash and handles the situation gracefully. + # We'll just verify we can call methods on them without crashing. + try: + _ = frame0_before.GetPC() + _ = frame0_before.IsValid() + _ = frame0_before.GetFunctionName() + except Exception as e: + self.fail(f"Accessing old frame reference should not crash: {e}") + + def test_provider_lifecycle_with_frame_validity(self): + """Test provider registration/removal at breakpoints and SBFrame validity across lifecycle. + + This test verifies: + 1. Registering a provider while stopped at a breakpoint. + 2. SBFrame references from synthetic frames persist across continues. + 3. SBFrame references can access variables in real frames while provider is active. + 4. Removing a provider while stopped at a breakpoint. + 5. SBFrame references from removed provider don't crash when accessed. + """ + self.build() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target.IsValid(), "Target should be valid") + + # Import the test frame provider. + script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") + self.runCmd("command script import " + script_path) + + # Set up breakpoints at the return statements in foo, bar, and baz. + # This ensures local variables are initialized. + bp_foo = target.BreakpointCreateBySourceRegex( + "Break in foo", lldb.SBFileSpec(self.source) + ) + bp_bar = target.BreakpointCreateBySourceRegex( + "Break in bar", lldb.SBFileSpec(self.source) + ) + bp_baz = target.BreakpointCreateBySourceRegex( + "Break in baz", lldb.SBFileSpec(self.source) + ) + + self.assertTrue(bp_foo.IsValid(), "Breakpoint at foo should be valid") + self.assertTrue(bp_bar.IsValid(), "Breakpoint at bar should be valid") + self.assertTrue(bp_baz.IsValid(), "Breakpoint at baz should be valid") + + # Launch the process. + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + self.assertTrue(process.IsValid(), "Process should be valid") + + # We should hit the foo breakpoint first. + self.assertEqual( + process.GetState(), lldb.eStateStopped, "Process should be stopped at foo" + ) + thread = process.GetSelectedThread() + self.assertIsNotNone(thread, "Should have a selected thread") + + # Register the provider at foo breakpoint. + error = lldb.SBError() + provider_id = target.RegisterScriptedFrameProvider( + "test_frame_providers.PrependFrameProvider", + lldb.SBStructuredData(), + error, + ) + self.assertTrue(error.Success(), f"Failed to register provider: {error}") + self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero") + + # Get individual frames BEFORE getting the full backtrace. + # This tests accessing frames before forcing evaluation of all frames. + frame0 = thread.GetFrameAtIndex(0) + frame1 = thread.GetFrameAtIndex(1) + frame2 = thread.GetFrameAtIndex(2) + + self.assertIsNotNone(frame0, "Frame 0 should exist") + self.assertIsNotNone(frame1, "Frame 1 should exist") + self.assertIsNotNone(frame2, "Frame 2 should exist") + + # First two frames should be synthetic with expected PCs. + pc0 = frame0.GetPC() + pc1 = frame1.GetPC() + + self.assertEqual(pc0, 0x9000, "Frame 0 should have synthetic PC 0x9000") + self.assertEqual(pc1, 0xA000, "Frame 1 should have synthetic PC 0xA000") + + # Frame 2 should be the real foo frame. + self.assertIn("foo", frame2.GetFunctionName(), "Frame 2 should be in foo") + + # Save references to the synthetic frames. + saved_frames = [frame0, frame1, frame2] + + # Test accessing saved frames at foo BEFORE getting full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at foo before full backtrace should not crash: {e}" + ) + + # Now verify the provider is active by checking frame count. + # PrependFrameProvider adds 2 synthetic frames. + # This forces a full backtrace evaluation. + original_frame_count = thread.GetNumFrames() + self.assertGreater( + original_frame_count, + 2, + "Should have at least synthetic frames + real frames", + ) + + # Test accessing saved frames at foo AFTER getting full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at foo after full backtrace should not crash: {e}" + ) + + # Verify we can access variables in frame2 (real frame). + foo_local = frame2.FindVariable("foo_local") + self.assertTrue(foo_local.IsValid(), "Should find foo_local variable") + self.assertEqual( + foo_local.GetValueAsUnsigned(), 20, "foo_local should be 20 (10 * 2)" + ) + + # Continue to bar breakpoint. + threads = lldbutil.continue_to_breakpoint(process, bp_bar) + self.assertIsNotNone(threads, "Should have stopped at bar breakpoint") + self.assertEqual(len(threads), 1, "Should have one thread stopped at bar") + thread = threads[0] + + # Verify the saved frames are still accessible without crashing at bar. + # Do this BEFORE getting the full backtrace. + # Note: They might not be "valid" in the traditional sense since we've moved + # to a different execution context, but they shouldn't crash. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + except Exception as e: + self.fail( + f"Accessing saved frames at bar before full backtrace should not crash: {e}" + ) + + # Verify the provider is still active by getting frame count. + # This forces full backtrace evaluation. + current_frame_count = thread.GetNumFrames() + self.assertGreater( + current_frame_count, 2, "Should still have synthetic frames at bar" + ) + + # Access the saved frames again AFTER getting the full backtrace. + # This ensures that forcing a full backtrace evaluation doesn't break + # the saved frame references. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + except Exception as e: + self.fail( + f"Accessing saved frames at bar after full backtrace should not crash: {e}" + ) + + # Get current frames at bar. + bar_frame0 = thread.GetFrameAtIndex(0) + bar_frame1 = thread.GetFrameAtIndex(1) + bar_frame2 = thread.GetFrameAtIndex(2) + + # Verify current frames have synthetic PCs. + self.assertEqual( + bar_frame0.GetPC(), 0x9000, "Frame 0 at bar should have synthetic PC" + ) + self.assertEqual( + bar_frame1.GetPC(), 0xA000, "Frame 1 at bar should have synthetic PC" + ) + self.assertIn("bar", bar_frame2.GetFunctionName(), "Frame 2 should be in bar") + + # Verify we can access variables in the bar frame. + bar_local = bar_frame2.FindVariable("bar_local") + self.assertTrue(bar_local.IsValid(), "Should find bar_local variable") + self.assertEqual( + bar_local.GetValueAsUnsigned(), 25, "bar_local should be 25 (5 * 5)" + ) + + # Continue to baz breakpoint. + threads = lldbutil.continue_to_breakpoint(process, bp_baz) + self.assertIsNotNone(threads, "Should have stopped at baz breakpoint") + self.assertEqual(len(threads), 1, "Should have one thread stopped at baz") + thread = threads[0] + + # Verify the saved frames are still accessible without crashing at baz. + # Do this BEFORE getting the full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at baz before full backtrace should not crash: {e}" + ) + + # Get the frame count to force full backtrace evaluation at baz. + baz_frame_count = thread.GetNumFrames() + self.assertGreater( + baz_frame_count, 2, "Should still have synthetic frames at baz" + ) + + # Verify the saved frames are still accessible AFTER getting full backtrace at baz. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at baz after full backtrace should not crash: {e}" + ) + + # Now manually remove the provider. + result = target.RemoveScriptedFrameProvider(provider_id) + self.assertSuccess( + result, f"Should successfully remove provider with ID {provider_id}" + ) + # Verify frames no longer have synthetic frames. + final_frame_count = thread.GetNumFrames() + + # Without the provider, we should have fewer frames (no synthetic ones). + self.assertLess( + final_frame_count, + original_frame_count, + "Frame count should decrease after provider removal", + ) + + # First frame should now be the real baz frame (no synthetic frames). + baz_frame0 = thread.GetFrameAtIndex(0) + self.assertIn( + "baz", baz_frame0.GetFunctionName(), "Frame 0 should now be real baz frame" + ) + + # The synthetic PC values should no longer appear. + for i in range(final_frame_count): + frame = thread.GetFrameAtIndex(i) + pc = frame.GetPC() + self.assertNotEqual( + pc, 0x9000, f"Frame {i} should not have synthetic PC 0x9000" + ) + self.assertNotEqual( + pc, 0xA000, f"Frame {i} should not have synthetic PC 0xA000" + ) + + # Verify the originally saved frames are now truly invalid/stale. + # They should still not crash when accessed. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[0].IsValid() + _ = saved_frames[1].GetPC() + _ = saved_frames[1].IsValid() + except Exception as e: + self.fail(f"Accessing invalidated frames should not crash: {e}") diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp index e1d346c29052b..e17226a459033 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp +++ b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp @@ -10,6 +10,24 @@ std::condition_variable cv; int ready_count = 0; constexpr int NUM_THREADS = 2; +int foo(int x) { + int foo_local = x * 2; + int foo_result = foo_local + 1; + return foo_result; // Break in foo. +} + +int bar(int x) { + int bar_local = x * x; + int bar_result = bar_local - 3; + return bar_result; // Break in bar. +} + +int baz(int x) { + int baz_local = x + 7; + int baz_result = baz_local / 2; + return baz_result; // Break in baz. +} + void thread_func(int thread_num) { std::cout << "Thread " << thread_num << " started\n"; @@ -31,7 +49,11 @@ int main(int argc, char **argv) { // Used as an existing C++ variable we can anchor on. int variable_in_main = 123; - (void)variable_in_main; + (void)variable_in_main; // Breakpoint for variable tests. + + // Call foo for first breakpoint. + int result_foo = foo(10); + (void)result_foo; for (int i = 0; i < NUM_THREADS; i++) { threads[i] = std::thread(thread_func, i); @@ -49,6 +71,14 @@ int main(int argc, char **argv) { std::cout << "Main thread at barrier\n"; + // Call bar for second breakpoint. + int result_bar = bar(5); + (void)result_bar; + + // Call baz for third breakpoint. + int result_baz = baz(11); + (void)result_baz; + for (int i = 0; i < NUM_THREADS; i++) threads[i].join(); diff --git a/lldb/test/API/macosx/extended-backtrace-api/Makefile b/lldb/test/API/macosx/extended-backtrace-api/Makefile new file mode 100644 index 0000000000000..845553d5e3f2f --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/Makefile @@ -0,0 +1,3 @@ +OBJC_SOURCES := main.m + +include Makefile.rules diff --git a/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py new file mode 100644 index 0000000000000..0e9ee0755065e --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py @@ -0,0 +1,144 @@ +"""Test SBThread.GetExtendedBacktraceThread API with queue debugging.""" + +import os +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class TestExtendedBacktraceAPI(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def setUp(self): + TestBase.setUp(self) + self.main_source = "main.m" + + @skipUnlessDarwin + @add_test_categories(["objc", "pyapi"]) + def test_extended_backtrace_thread_api(self): + """Test GetExtendedBacktraceThread with queue debugging.""" + self.build() + exe = self.getBuildArtifact("a.out") + + # Get Xcode developer directory path. + # Try DEVELOPER_DIR environment variable first, then fall back to xcode-select. + xcode_dev_path = os.environ.get("DEVELOPER_DIR") + + if not xcode_dev_path: + import subprocess + + xcode_dev_path = ( + subprocess.check_output(["xcode-select", "-p"]).decode("utf-8").strip() + ) + + # Check for libBacktraceRecording.dylib. + libbtr_path = os.path.join( + xcode_dev_path, "usr/lib/libBacktraceRecording.dylib" + ) + + self.assertTrue( + os.path.isfile(libbtr_path), + f"libBacktraceRecording.dylib is not present at {libbtr_path}", + ) + + self.assertTrue( + os.path.isfile("/usr/lib/system/introspection/libdispatch.dylib"), + "introspection libdispatch dylib not installed.", + ) + + # Create launch info with environment variables for libBacktraceRecording. + launch_info = lldb.SBLaunchInfo(None) + launch_info.SetWorkingDirectory(self.get_process_working_directory()) + launch_info.SetEnvironmentEntries( + [ + f"DYLD_INSERT_LIBRARIES={libbtr_path}", + "DYLD_LIBRARY_PATH=/usr/lib/system/introspection", + ], + True, + ) + + # Launch the process and run to breakpoint. + target, process, thread, bp = lldbutil.run_to_name_breakpoint( + self, "do_work_level_5", launch_info=launch_info, bkpt_module="a.out" + ) + + self.assertTrue(target.IsValid(), VALID_TARGET) + self.assertTrue(process.IsValid(), PROCESS_IS_VALID) + self.assertTrue(thread.IsValid(), "Stopped thread is valid") + self.assertTrue(bp.IsValid(), VALID_BREAKPOINT) + + # Call GetNumQueues to ensure queue information is loaded. + num_queues = process.GetNumQueues() + + # Check that we can find the com.apple.main-thread queue. + main_thread_queue_found = False + for i in range(num_queues): + queue = process.GetQueueAtIndex(i) + if queue.GetName() == "com.apple.main-thread": + main_thread_queue_found = True + break + + # Verify we have at least 5 frames. + self.assertGreaterEqual( + thread.GetNumFrames(), + 5, + "Thread should have at least 5 frames in backtrace", + ) + + # Get frame 2 BEFORE calling GetExtendedBacktraceThread. + # This mimics what Xcode does - it has the frame objects ready. + frame2 = thread.GetFrameAtIndex(2) + self.assertTrue(frame2.IsValid(), "Frame 2 is valid") + + # Now test GetExtendedBacktraceThread. + # This is the critical part - getting the extended backtrace calls into + # libBacktraceRecording which does an inferior function call, and this + # invalidates/clears the unwinder state. + extended_thread = thread.GetExtendedBacktraceThread("libdispatch") + + # This should be valid since we injected libBacktraceRecording. + self.assertTrue( + extended_thread.IsValid(), + "Extended backtrace thread for 'libdispatch' should be valid with libBacktraceRecording loaded", + ) + + # The extended thread should have frames. + self.assertGreater( + extended_thread.GetNumFrames(), + 0, + "Extended backtrace thread should have at least one frame", + ) + + # Test frame 2 on the extended backtrace thread. + self.assertGreater( + extended_thread.GetNumFrames(), + 2, + "Extended backtrace thread should have at least 3 frames to access frame 2", + ) + + extended_frame2 = extended_thread.GetFrameAtIndex(2) + self.assertTrue(extended_frame2.IsValid(), "Extended thread frame 2 is valid") + + # NOW try to access variables from frame 2 of the ORIGINAL thread. + # This is the key test - after GetExtendedBacktraceThread() has executed + # an inferior function call, the unwinder state may be invalidated. + # Xcode exhibits this bug where variables show "register fp is not available" + # after extended backtrace retrieval. + + # Set frame 2 as the selected frame so expect_var_path works. + thread.SetSelectedFrame(2) + + variables = frame2.GetVariables(False, True, False, True) + self.assertGreater( + variables.GetSize(), 0, "Frame 2 should have at least one variable" + ) + + # Test all variables in frame 2, like Xcode does. + # Use expect_var_path to verify each variable is accessible without errors. + for i in range(variables.GetSize()): + var = variables.GetValueAtIndex(i) + var_name = var.GetName() + + # This will fail if the variable contains "not available" or has errors. + self.expect_var_path(var_name) diff --git a/lldb/test/API/macosx/extended-backtrace-api/main.m b/lldb/test/API/macosx/extended-backtrace-api/main.m new file mode 100644 index 0000000000000..8f2186845a651 --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/main.m @@ -0,0 +1,53 @@ +#include +#include +#include +#include + +void do_work_level_5(void) { + // Frame 0 will have these variables. + int frame0_var = 100; + const char *frame0_string = "frame_zero"; + float frame0_float = 1.5f; + + // This is where we'll set the breakpoint. + printf("Level 5 work executing\n"); // Break here. + while (1) + sleep(1); +} + +void do_work_level_4(void) { + // Frame 1 will have these variables. + int frame1_var = 200; + const char *frame1_string = "frame_one"; + long frame1_long = 9876543210L; + + do_work_level_5(); +} + +void do_work_level_3(void) { + // Frame 2 will have these variables. + int test_variable = 42; + const char *test_string = "test_value"; + double test_double = 3.14159; + + do_work_level_4(); +} + +void do_work_level_2(void) { do_work_level_3(); } + +void do_work_level_1(void *context) { do_work_level_2(); } + +int main(int argc, const char *argv[]) { + // Create a serial dispatch queue. + dispatch_queue_t worker_queue = + dispatch_queue_create("com.test.worker_queue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_t submitter_queue = + dispatch_queue_create("com.test.submitter_queue", DISPATCH_QUEUE_SERIAL); + + // Submit work from one queue to another to create extended backtrace. + dispatch_async_f(submitter_queue, &worker_queue, do_work_level_1); + + // Keep main thread alive. + dispatch_main(); + return 0; +}