[Lldb-commits] [lldb] [llvm] [lldb][Windows] Support OutputDebugString (PR #196395)
via lldb-commits
lldb-commits at lists.llvm.org
Mon May 11 04:42:53 PDT 2026
https://github.com/Nerixyz updated https://github.com/llvm/llvm-project/pull/196395
>From 75d0fa835d306d945c872fde8def69e987eee9c2 Mon Sep 17 00:00:00 2001
From: Nerixyz <nerixdev at outlook.de>
Date: Thu, 7 May 2026 20:38:54 +0200
Subject: [PATCH 1/4] [lldb][Windows] Support OutputDebugString
Co-authored-by: Alvin Wong <alvin at alvinhc.com>
---
.../Process/Windows/Common/DebuggerThread.cpp | 19 ++--
.../Process/Windows/Common/IDebugDelegate.h | 3 +-
.../Windows/Common/LocalDebugDelegate.cpp | 6 +-
.../Windows/Common/LocalDebugDelegate.h | 3 +-
.../Windows/Common/NativeProcessWindows.h | 5 +-
.../Windows/Common/ProcessDebugger.cpp | 6 +-
.../Process/Windows/Common/ProcessDebugger.h | 3 +-
.../Process/Windows/Common/ProcessWindows.cpp | 91 ++++++++++++++++++-
.../Process/Windows/Common/ProcessWindows.h | 7 +-
.../Process/Windows/output_debug_string.cpp | 57 ++++++++++++
10 files changed, 182 insertions(+), 18 deletions(-)
create mode 100644 lldb/test/Shell/Process/Windows/output_debug_string.cpp
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 6359a63dfef91..15cba5d2b51d2 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -173,7 +173,7 @@ Status DebuggerThread::StopDebugging(bool terminate) {
// If we're stuck waiting for an exception to continue (e.g. the user is at a
// breakpoint messing around in the debugger), continue it now. But only
// AFTER calling TerminateProcess to make sure that the very next call to
- // WaitForDebugEvent is an exit process event.
+ // WaitForDebugEventEx is an exit process event.
if (m_active_exception.get()) {
LLDB_LOG(log, "masking active exception");
ContinueAsyncException(ExceptionResult::MaskException);
@@ -233,10 +233,10 @@ void DebuggerThread::DebugLoop() {
Log *log = GetLog(WindowsLog::Event);
DEBUG_EVENT dbe = {};
bool should_debug = true;
- LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEvent loop");
+ LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEventEx loop");
while (should_debug) {
- LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEvent");
- BOOL wait_result = WaitForDebugEvent(&dbe, INFINITE);
+ LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEventEx");
+ BOOL wait_result = WaitForDebugEventEx(&dbe, INFINITE);
if (wait_result) {
DWORD continue_status = DBG_CONTINUE;
bool shutting_down = m_is_shutting_down;
@@ -312,9 +312,9 @@ void DebuggerThread::DebugLoop() {
// detaching with leaving breakpoint exception event on the queue may
// cause target process to crash so process events as possible since
// target threads are running at this time, there is possibility to
- // have some breakpoint exception between last WaitForDebugEvent and
+ // have some breakpoint exception between last WaitForDebugEventEx and
// DebugActiveProcessStop but ignore for now.
- while (WaitForDebugEvent(&dbe, 0)) {
+ while (WaitForDebugEventEx(&dbe, 0)) {
continue_status = DBG_CONTINUE;
if (dbe.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
!(dbe.u.Exception.ExceptionRecord.ExceptionCode ==
@@ -337,7 +337,7 @@ void DebuggerThread::DebugLoop() {
should_debug = false;
}
} else {
- LLDB_LOG(log, "returned FALSE from WaitForDebugEvent. Error = {0}",
+ LLDB_LOG(log, "returned FALSE from WaitForDebugEventEx. Error = {0}",
::GetLastError());
should_debug = false;
@@ -345,7 +345,7 @@ void DebuggerThread::DebugLoop() {
}
FreeProcessHandles();
- LLDB_LOG(log, "WaitForDebugEvent loop completed, exiting.");
+ LLDB_LOG(log, "WaitForDebugEventEx loop completed, exiting.");
::SetEvent(m_debugging_ended_event);
}
@@ -567,6 +567,9 @@ DebuggerThread::HandleUnloadDllEvent(const UNLOAD_DLL_DEBUG_INFO &info,
DWORD
DebuggerThread::HandleODSEvent(const OUTPUT_DEBUG_STRING_INFO &info,
DWORD thread_id) {
+ m_debug_delegate->OnDebugString(
+ llvm::bit_cast<lldb::addr_t>(info.lpDebugStringData),
+ info.fUnicode == TRUE, info.nDebugStringLength);
return DBG_CONTINUE;
}
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index aff2dd610eea0..842fa4a6464b5 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -35,7 +35,8 @@ class IDebugDelegate {
virtual void OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr) = 0;
virtual void OnUnloadDll(lldb::addr_t module_addr) = 0;
- virtual void OnDebugString(const std::string &string) = 0;
+ 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/LocalDebugDelegate.cpp b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
index dfa1d76d35fa0..647502ddaac25 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
@@ -56,9 +56,11 @@ void LocalDebugDelegate::OnUnloadDll(lldb::addr_t module_addr) {
process->OnUnloadDll(module_addr);
}
-void LocalDebugDelegate::OnDebugString(const std::string &string) {
+void LocalDebugDelegate::OnDebugString(lldb::addr_t debug_string_addr,
+ bool is_unicode,
+ uint16_t length_lower_word) {
if (ProcessWindowsSP process = GetProcessPointer())
- process->OnDebugString(string);
+ process->OnDebugString(debug_string_addr, is_unicode, length_lower_word);
}
void LocalDebugDelegate::OnDebuggerError(const Status &error, uint32_t type) {
diff --git a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
index 7494dbbb0cfb6..d602771b3a7a4 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
@@ -51,7 +51,8 @@ class LocalDebugDelegate : public IDebugDelegate {
void OnLoadDll(const lldb_private::ModuleSpec &module_spec,
lldb::addr_t module_addr) override;
void OnUnloadDll(lldb::addr_t module_addr) override;
- void OnDebugString(const std::string &message) override;
+ void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word) override;
void OnDebuggerError(const Status &error, uint32_t type) override;
private:
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index cfba787a3d220..75add16ce6099 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -173,8 +173,9 @@ class NativeDebugDelegate : public IDebugDelegate {
m_process.OnUnloadDll(module_addr);
}
- void OnDebugString(const std::string &string) override {
- m_process.OnDebugString(string);
+ void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word) override {
+ m_process.OnDebugString(debug_string_addr, is_unicode, length_lower_word);
}
void OnDebuggerError(const Status &error, uint32_t type) override {
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
index 51b2dfcb74d86..ac89f146e2847 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
@@ -543,7 +543,11 @@ void ProcessDebugger::OnUnloadDll(lldb::addr_t module_addr) {
// Do nothing by default
}
-void ProcessDebugger::OnDebugString(const std::string &string) {}
+void ProcessDebugger::OnDebugString(lldb::addr_t debug_string_addr,
+ bool is_unicode,
+ uint16_t length_lower_word) {
+ // Do nothing by default
+}
void ProcessDebugger::OnDebuggerError(const Status &error, uint32_t type) {
llvm::sys::ScopedLock lock(m_mutex);
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
index a4db76455ef21..25c877d5b4f17 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
@@ -59,7 +59,8 @@ class ProcessDebugger {
virtual void OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr);
virtual void OnUnloadDll(lldb::addr_t module_addr);
- virtual void OnDebugString(const std::string &string);
+ virtual void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word);
virtual void OnDebuggerError(const Status &error, uint32_t type);
protected:
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 28b5554069c90..9d3c2e8e31ba4 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -36,6 +36,7 @@
#include "lldb/Utility/State.h"
#include "llvm/Support/ConvertUTF.h"
+#include "llvm/Support/ErrorExtras.h"
#include "llvm/Support/Format.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/raw_ostream.h"
@@ -870,7 +871,95 @@ void ProcessWindows::OnUnloadDll(lldb::addr_t module_addr) {
dyld->OnUnloadModule(module_addr);
}
-void ProcessWindows::OnDebugString(const std::string &string) {}
+void ProcessWindows::OnDebugString(lldb::addr_t debug_string_addr,
+ bool is_unicode,
+ uint16_t length_lower_word) {
+ Log *log = GetLog(WindowsLog::Process);
+
+ llvm::SmallVector<char, 256> buffer;
+ llvm::Error err =
+ ReadDebugString(debug_string_addr, is_unicode, length_lower_word, buffer);
+ if (err) {
+ LLDB_LOG_ERROR(log, std::move(err),
+ "Failed to read debug string at {1:x} (size & 0xffff={2}, "
+ "unicode={3}): {0}",
+ debug_string_addr, length_lower_word, is_unicode);
+ return;
+ }
+ if (buffer.empty())
+ return;
+
+ if (is_unicode) {
+ if (buffer.size() % 2 != 0) {
+ LLDB_LOG(log, "Read debug string had uneven size: {0}", buffer.size());
+ return;
+ }
+ llvm::ArrayRef<unsigned short> utf16(
+ reinterpret_cast<const unsigned short *>(buffer.data()),
+ buffer.size() / 2);
+ std::string out;
+ if (!llvm::convertUTF16ToUTF8String(utf16, out)) {
+ LLDB_LOG(log, "Debug string is not valid Utf 16");
+ return;
+ }
+
+ AppendSTDOUT(out.data(), out.size());
+ } else {
+ AppendSTDOUT(buffer.data(), buffer.size());
+ }
+}
+
+llvm::Error
+ProcessWindows::ReadDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word,
+ llvm::SmallVectorImpl<char> &output) {
+ if (is_unicode && length_lower_word % 2 != 0) {
+ return llvm::createStringError(
+ "Utf16 string can't have uneven size in bytes");
+ }
+
+ const auto is_zero_terminated = [&] {
+ // The zero terminator is always at the end of the buffer.
+ if (is_unicode) {
+ return output.size() >= 2 && output.back() == 0 &&
+ output[output.size() - 2] == 0;
+ }
+ return !output.empty() && output.back() == 0;
+ };
+
+ // Read at most 1 MiB ((1 << 16) * 16 - 1 Bytes) since we don't know the exact
+ // size of the string. We know that `strlen(string) & 0xffff ==
+ // length_lower_word`, so we read in chunks until we reach the terminator:
+ // - 0: `length_lower_word` Bytes
+ // - 1..16: 64 KiB (= 2^16 Bytes)
+ size_t start = length_lower_word == 0 ? 1 : 0;
+ for (size_t i = start; i < 16; ++i) {
+ output.resize_for_overwrite(length_lower_word + i * (1 << 16));
+ size_t chunk_size = i == 0 ? length_lower_word : (1 << 16);
+ lldb::addr_t addr = debug_string_addr + output.size_in_bytes() - chunk_size;
+
+ Status error;
+ size_t bytes_read =
+ DoReadMemory(addr, output.end() - chunk_size, chunk_size, error);
+ if (error.Fail())
+ return error.takeError();
+
+ if (bytes_read != chunk_size) {
+ return llvm::createStringErrorV(
+ "Expected to read {0} bytes, but read {1}", chunk_size, bytes_read);
+ }
+
+ if (is_zero_terminated())
+ break;
+ }
+
+ if (!is_zero_terminated())
+ return llvm::createStringError("String is 1 MiB or larger");
+
+ // Remove null terminator.
+ output.pop_back_n(is_unicode ? 2 : 1);
+ return llvm::Error::success();
+}
void ProcessWindows::OnDebuggerError(const Status &error, uint32_t type) {
llvm::sys::ScopedLock lock(m_mutex);
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
index 228619d0e3d5e..58dc510a17850 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
@@ -91,7 +91,8 @@ class ProcessWindows : public Process, public ProcessDebugger {
void OnLoadDll(const ModuleSpec &module_spec,
lldb::addr_t module_addr) override;
void OnUnloadDll(lldb::addr_t module_addr) override;
- void OnDebugString(const std::string &string) override;
+ void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word) override;
void OnDebuggerError(const Status &error, uint32_t type) override;
std::optional<uint32_t> GetWatchpointSlotCount() override;
@@ -114,6 +115,10 @@ class ProcessWindows : public Process, public ProcessDebugger {
MemoryRegionInfo &info) override;
private:
+ llvm::Error ReadDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+ uint16_t length_lower_word,
+ llvm::SmallVectorImpl<char> &output);
+
struct WatchpointInfo {
uint32_t slot_id;
lldb::addr_t address;
diff --git a/lldb/test/Shell/Process/Windows/output_debug_string.cpp b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
new file mode 100644
index 0000000000000..231d529e30a84
--- /dev/null
+++ b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
@@ -0,0 +1,57 @@
+// REQUIRES: target-windows
+// RUN: %build --compiler=clang-cl -o %t.exe -- %s
+// RUN: %lldb -f %t.exe -o "log enable windows process" -o r -o q | FileCheck %s
+
+#include <Windows.h>
+#include <string>
+
+int main() {
+ OutputDebugStringA("My string\nnext line\nBut this doesn't have trailing newline|");
+ OutputDebugStringW(L"OutputDebugStringW with some emojis 🦎🐊🐢🐍\n");
+ OutputDebugStringW(L"Another W1\n");
+ OutputDebugStringW(L"Another W2\n");
+ OutputDebugStringA("Some A1\n");
+ OutputDebugStringA("Some A2\n");
+ OutputDebugStringA("Some A3\n");
+
+ std::wstring maxW((1 << 19) - 4, L'A');
+ maxW.push_back(L'B');
+ maxW.push_back(L'\n');
+ OutputDebugStringW(maxW.c_str());
+
+ Sleep(100);
+
+ std::string maxA((1 << 20) - 4, 'C');
+ maxA.push_back(L'D');
+ maxA.push_back(L'\n');
+ OutputDebugStringA(maxA.c_str());
+
+ Sleep(100);
+
+ std::wstring tooBigW((1 << 19) - 3, L'E');
+ tooBigW.push_back(L'F');
+ tooBigW.push_back(L'\n');
+ OutputDebugStringW(tooBigW.c_str());
+
+ Sleep(100);
+
+ std::string tooBigA((1 << 20) - 3, 'G');
+ tooBigA.push_back(L'H');
+ tooBigA.push_back(L'\n');
+ OutputDebugStringA(tooBigA.c_str());
+
+ return 0;
+}
+
+// CHECK: My string
+// CHECK-NEXT: next line
+// CHECK-NEXT: But this doesn't have trailing newline|OutputDebugStringW with some emojis 🦎🐊🐢🐍
+// CHECK-NEXT: Another W1
+// CHECK-NEXT: Another W2
+// CHECK-NEXT: Some A1
+// CHECK-NEXT: Some A2
+// CHECK-NEXT: Some A3
+// CHECK-NEXT: {{A+B}}
+// CHECK-NEXT: {{C+D}}
+// CHECK-NEXT: Failed to read debug string at 0x{{.*}} (size & 0xffff=0, unicode=true): String is 1 MiB or larger
+// CHECK-NEXT: Failed to read debug string at 0x{{.*}} (size & 0xffff=0, unicode=false): String is 1 MiB or larger
>From 96212e81abbb4637330454d4cabf7af47d72bf5e Mon Sep 17 00:00:00 2001
From: Nerixyz <nerixdev at outlook.de>
Date: Fri, 8 May 2026 15:07:44 +0200
Subject: [PATCH 2/4] fix: remove unnecessary sleep and add comment about the
others
---
lldb/test/Shell/Process/Windows/output_debug_string.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lldb/test/Shell/Process/Windows/output_debug_string.cpp b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
index 231d529e30a84..d6e6580484e0f 100644
--- a/lldb/test/Shell/Process/Windows/output_debug_string.cpp
+++ b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
@@ -19,13 +19,14 @@ int main() {
maxW.push_back(L'\n');
OutputDebugStringW(maxW.c_str());
- Sleep(100);
-
std::string maxA((1 << 20) - 4, 'C');
maxA.push_back(L'D');
maxA.push_back(L'\n');
OutputDebugStringA(maxA.c_str());
+ // Give LLDB time to print out the previous debug strings.
+ // The following ones should generate a log.
+ // This makes sure we see the log after the previous line, not before.
Sleep(100);
std::wstring tooBigW((1 << 19) - 3, L'E');
>From fc4fb2d33635a29274cead3e6376e6582550c85d Mon Sep 17 00:00:00 2001
From: Nerixyz <nerixdev at outlook.de>
Date: Mon, 11 May 2026 13:39:32 +0200
Subject: [PATCH 3/4] docs: mention support
---
llvm/docs/ReleaseNotes.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index 7363c2fd8c52b..e8b7fb9a092d4 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -325,6 +325,9 @@ Makes programs 10x faster by doing Special New Thing.
#### Windows
* Python 3.11 or later is now recommended for building LLDB 23 on Windows. From LLDB 24, Python 3.11 or later will be required.
+* Messages from `OutputDebugString[A|W]` are now shown inline when using LLDB
+ from the command-line and in the output window when using lldb-dap.
+
### Changes to BOLT
>From 85a27acea4bbb581a77aebfb50b3b48898410e93 Mon Sep 17 00:00:00 2001
From: Nerixyz <nerixdev at outlook.de>
Date: Mon, 11 May 2026 13:42:37 +0200
Subject: [PATCH 4/4] fix: suggestion
---
lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 152e91b970e0f..8701961e57588 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -605,7 +605,8 @@ DWORD
DebuggerThread::HandleODSEvent(const OUTPUT_DEBUG_STRING_INFO &info,
DWORD thread_id) {
m_debug_delegate->OnDebugString(
- llvm::bit_cast<lldb::addr_t>(info.lpDebugStringData),
+ static_cast<lldb::addr_t>(
+ reinterpret_cast<uintptr_t>(info.lpDebugStringData)),
info.fUnicode == TRUE, info.nDebugStringLength);
return DBG_CONTINUE;
}
More information about the lldb-commits
mailing list