[Lldb-commits] [lldb] [lldb][Windows] Report all queued exceptions in one lldb-server stop (PR #227353)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Thu Oct 29 12:53:50 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/227353
>From 474631e80dc588ef5483e426abd24894e2feb473 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 13 Oct 2026 14:15:51 +0100
Subject: [PATCH 1/2] [lldb][Windows] Report debug events queued together in
one stop
---
.../lldb/Host/common/NativeProcessProtocol.h | 5 ++-
.../Process/Windows/Common/DebuggerThread.cpp | 37 ++++++++++++++++--
.../Process/Windows/Common/DebuggerThread.h | 4 ++
.../Process/Windows/Common/IDebugDelegate.h | 3 ++
.../Windows/Common/NativeProcessWindows.cpp | 38 ++++++++++++++++---
.../Windows/Common/NativeProcessWindows.h | 17 +++++++++
6 files changed, 94 insertions(+), 10 deletions(-)
diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index 209cb068aba215..da1fa26abefa9e 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -544,8 +544,11 @@ class NativeProcessProtocol {
NativeThreadProtocol *GetThreadByIDUnlocked(lldb::tid_t tid);
-private:
+ /// Notify the delegates of \p state. For plugins that set a state with
+ /// SetState(state, /*notify_delegates=*/false) and report it later.
void SynchronouslyNotifyProcessStateChanged(lldb::StateType state);
+
+private:
llvm::Expected<SoftwareBreakpoint>
EnableSoftwareBreakpoint(lldb::addr_t addr, uint32_t size_hint);
};
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index cb1e31b4334f54..6dd051e574885b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -262,6 +262,13 @@ void DebuggerThread::ContinueAsyncException(ExceptionResult result) {
m_exception_pred.SetValue(result, eBroadcastAlways);
}
+void DebuggerThread::ReportDeferredStopAndWaitForResume() {
+ m_dll_event_pred.SetValue(false, eBroadcastNever);
+ m_debug_delegate->ReportDeferredStop();
+ if (!m_is_shutting_down)
+ m_dll_event_pred.WaitForValueEqualTo(true);
+}
+
void DebuggerThread::ContinueAsyncDllEvent() {
m_dll_event_pred.SetValue(true, eBroadcastAlways);
}
@@ -279,10 +286,28 @@ void DebuggerThread::DebugLoop() {
Log *log = GetLog(WindowsLog::Event);
DEBUG_EVENT dbe = {};
bool should_debug = true;
+ constexpr unsigned max_drained_events = 64;
+ unsigned drained_events = 0;
LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEventEx loop");
while (should_debug) {
+ bool draining = !m_is_shutting_down && m_debug_delegate->HasDeferredStop();
+ if (draining && drained_events >= max_drained_events) {
+ LLDB_LOG(log, "reporting the stop after taking {0} queued debug events",
+ drained_events);
+ ReportDeferredStopAndWaitForResume();
+ draining = false;
+ }
+ if (!draining)
+ drained_events = 0;
LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEvent");
- BOOL wait_result = g_wait_for_debug_event(&dbe, INFINITE);
+ BOOL wait_result = g_wait_for_debug_event(&dbe, draining ? 0 : INFINITE);
+ if (draining) {
+ if (!wait_result) {
+ ReportDeferredStopAndWaitForResume();
+ continue;
+ }
+ ++drained_events;
+ }
if (wait_result) {
DWORD continue_status = DBG_CONTINUE;
bool shutting_down = m_is_shutting_down;
@@ -447,6 +472,8 @@ DebuggerThread::HandleExceptionEvent(const EXCEPTION_DEBUG_INFO &info,
// result is what the wait below returns.
if (result != ExceptionResult::BreakInDebugger)
ContinueAsyncException(result);
+ else
+ m_debug_delegate->ReportDeferredStop();
LLDB_LOG(log, "waiting for ExceptionPred != BreakInDebugger");
result = *m_exception_pred.WaitForValueNotEqualTo(
@@ -864,8 +891,10 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
if (info.hFile != nullptr)
::CloseHandle(info.hFile);
- if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load())
+ if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load()) {
+ m_debug_delegate->ReportDeferredStop();
m_dll_event_pred.WaitForValueEqualTo(true);
+ }
return DBG_CONTINUE;
}
@@ -879,8 +908,10 @@ DebuggerThread::HandleUnloadDllEvent(const UNLOAD_DLL_DEBUG_INFO &info,
m_dll_event_pred.SetValue(false, eBroadcastNever);
DllEventAction action = m_debug_delegate->OnUnloadDll(
reinterpret_cast<lldb::addr_t>(info.lpBaseOfDll), thread_id);
- if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load())
+ if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load()) {
+ m_debug_delegate->ReportDeferredStop();
m_dll_event_pred.WaitForValueEqualTo(true);
+ }
return DBG_CONTINUE;
}
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
index a9fa3ec0c7599d..cc2d3f740b77e9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
@@ -55,6 +55,10 @@ class DebuggerThread : public std::enable_shared_from_this<DebuggerThread> {
void ContinueAsyncDllEvent();
private:
+ /// Have the delegate report the stop it collected, then wait until the
+ /// client resumes before taking the next debug event.
+ void ReportDeferredStopAndWaitForResume();
+
void FreeProcessHandles();
void DebugLoop();
ExceptionResult HandleExceptionEvent(const EXCEPTION_DEBUG_INFO &info,
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index cecd58a15f9c2c..f5c9b53447f3b9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -37,6 +37,9 @@ class IDebugDelegate {
lldb::tid_t thread_id) = 0;
virtual DllEventAction OnUnloadDll(lldb::addr_t module_addr,
lldb::tid_t thread_id) = 0;
+ virtual bool HasDeferredStop() { return false; }
+ /// Report the stop the delegate holds, if any.
+ virtual void ReportDeferredStop() {}
virtual void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
uint16_t length_lower_word) = 0;
virtual void OnDebuggerError(const Status &error, uint32_t type) = 0;
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 9d2cdf9f58510c..6885ca024b06a7 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -533,6 +533,9 @@ void NativeProcessWindows::OnExitProcess(uint32_t exit_code) {
bool started = m_session_data && m_session_data->m_initial_stop_received;
ProcessDebugger::OnExitProcess(exit_code);
+ // The exit replaces a stop that has not been reported yet.
+ m_deferred_stop = false;
+
// No signal involved. It is just an exit event.
WaitStatus wait_status(WaitStatus::Exit, exit_code);
SetExitStatus(wait_status, started);
@@ -583,6 +586,29 @@ void NativeProcessWindows::OnDebuggerConnected(lldb::addr_t image_base) {
*this, m_session_data->m_debugger->GetMainThread()));
}
+void NativeProcessWindows::DeferStop() {
+ if (GetState() == eStateStopped) {
+ if (!m_deferred_stop)
+ LLDB_LOG(GetLog(WindowsLog::Exception),
+ "debug event on thread {0:x} while a reported stop is current",
+ GetCurrentThreadID());
+ return;
+ }
+ SetState(eStateStopped, /*notify_delegates=*/false);
+ m_deferred_stop = true;
+ m_deferred_stop_tid = GetCurrentThreadID();
+}
+
+void NativeProcessWindows::ReportDeferredStop() {
+ llvm::sys::ScopedLock lock(m_mutex);
+ if (!m_deferred_stop)
+ return;
+ m_deferred_stop = false;
+ if (GetThreadByID(m_deferred_stop_tid))
+ SetCurrentThreadID(m_deferred_stop_tid);
+ SynchronouslyNotifyProcessStateChanged(eStateStopped);
+}
+
bool NativeProcessWindows::RewindTrapOfRemovedBreakpoint(
const ExceptionRecord &record) {
if (!m_initial_stop_seen)
@@ -646,7 +672,7 @@ NativeProcessWindows::HandleSingleStepException(const ExceptionRecord &record) {
StopThread(record.GetThreadID(), StopReason::eStopReasonTrace);
}
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::MaskException;
}
@@ -665,7 +691,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
StopThread(thread_id, StopReason::eStopReasonBreakpoint);
// The current PC is AFTER the BP opcode, on all architectures.
reg_ctx.SetPC(reg_ctx.GetPC() - GetSoftwareBreakpointPCOffset());
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::MaskException;
}
@@ -692,7 +718,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
exception_addr)
.str();
StopThread(thread_id, StopReason::eStopReasonWatchpoint, desc);
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::MaskException;
}
}
@@ -750,7 +776,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
SetCurrentThreadID(thread_id);
if (NativeThreadWindows *injected = GetThreadByID(thread_id))
injected->SetStopReason(signal_info, "interrupt");
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::BreakInDebugger;
}
@@ -767,7 +793,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
record.GetExceptionValue(), exception_addr)
.str();
StopThread(thread_id, StopReason::eStopReasonException, std::move(desc));
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::MaskException;
}
@@ -793,7 +819,7 @@ NativeProcessWindows::HandleGenericException(bool first_chance,
StopThread(record.GetThreadID(), StopReason::eStopReasonException,
std::move(desc));
- SetState(eStateStopped, true);
+ DeferStop();
return ExceptionResult::BreakInDebugger;
}
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index e79596fecc9f80..79046dd6846a46 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -140,6 +140,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
lldb::tid_t thread_id) override;
void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
uint16_t length_lower_word) override;
+ bool HasDeferredStop() const { return m_deferred_stop; }
+ void ReportDeferredStop();
protected:
NativeThreadWindows *GetThreadByID(lldb::tid_t thread_id);
@@ -167,6 +169,9 @@ class NativeProcessWindows : public NativeProcessProtocol,
NativeProcessWindows(lldb::pid_t pid, int terminal_fd,
NativeDelegate &delegate, llvm::Error &E);
+ /// Stop the process without notifying the delegate yet.
+ void DeferStop();
+
/// A breakpoint trap whose debug event was only delivered after the client
/// removed that breakpoint: the thread executed the trap before the stop
/// that removed it, but its event was held back until it ran again. Moves
@@ -196,6 +201,14 @@ class NativeProcessWindows : public NativeProcessProtocol,
/// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
bool m_pending_halt = false;
+ /// Set by DeferStop(), cleared once the stop is reported or the process
+ /// exits.
+ bool m_deferred_stop = false;
+
+ /// The thread whose debug event started the deferred stop. Reported as the
+ /// stop's thread even if later events made another thread current.
+ lldb::tid_t m_deferred_stop_tid = LLDB_INVALID_THREAD_ID;
+
bool m_client_supports_libraries_read = false;
/// PseudoConsole for the lldb-server stdio-forwarding path.
@@ -264,6 +277,10 @@ class NativeDebugDelegate : public IDebugDelegate {
return m_process.OnDebuggerError(error, type);
}
+ bool HasDeferredStop() override { return m_process.HasDeferredStop(); }
+
+ void ReportDeferredStop() override { m_process.ReportDeferredStop(); }
+
private:
NativeProcessWindows &m_process;
};
>From 57be145fe4937394f146c32278b2905567740be2 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 29 Oct 2026 12:51:22 -0700
Subject: [PATCH 2/2] Address review comments: document deferred stop state and
split up the draining logic
---
.../source/Plugins/Process/Windows/Common/DebuggerThread.cpp | 5 +++++
lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h | 4 ++++
.../Plugins/Process/Windows/Common/NativeProcessWindows.h | 5 +++--
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 6dd051e574885b..aa88b3a86ba01b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -291,16 +291,20 @@ void DebuggerThread::DebugLoop() {
LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEventEx loop");
while (should_debug) {
bool draining = !m_is_shutting_down && m_debug_delegate->HasDeferredStop();
+
if (draining && drained_events >= max_drained_events) {
LLDB_LOG(log, "reporting the stop after taking {0} queued debug events",
drained_events);
ReportDeferredStopAndWaitForResume();
draining = false;
}
+
if (!draining)
drained_events = 0;
+
LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEvent");
BOOL wait_result = g_wait_for_debug_event(&dbe, draining ? 0 : INFINITE);
+
if (draining) {
if (!wait_result) {
ReportDeferredStopAndWaitForResume();
@@ -308,6 +312,7 @@ void DebuggerThread::DebugLoop() {
}
++drained_events;
}
+
if (wait_result) {
DWORD continue_status = DBG_CONTINUE;
bool shutting_down = m_is_shutting_down;
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index f5c9b53447f3b9..3a567994819a28 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -37,6 +37,10 @@ class IDebugDelegate {
lldb::tid_t thread_id) = 0;
virtual DllEventAction OnUnloadDll(lldb::addr_t module_addr,
lldb::tid_t thread_id) = 0;
+ /// Whether the delegate has a stop that it recorded but has not reported
+ /// to its client yet. While this is true, DebuggerThread keeps taking the
+ /// debug events that are already queued, so they are reported in the same
+ /// stop.
virtual bool HasDeferredStop() { return false; }
/// Report the stop the delegate holds, if any.
virtual void ReportDeferredStop() {}
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index 79046dd6846a46..035f1a43c47e93 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -201,8 +201,9 @@ class NativeProcessWindows : public NativeProcessProtocol,
/// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
bool m_pending_halt = false;
- /// Set by DeferStop(), cleared once the stop is reported or the process
- /// exits.
+ /// Whether the process is stopped but the stop has not been reported to the
+ /// delegate yet. Set by DeferStop(), cleared once the stop is reported or the
+ /// process exits.
bool m_deferred_stop = false;
/// The thread whose debug event started the deferred stop. Reported as the
More information about the lldb-commits
mailing list