[Lldb-commits] [lldb] [lldb][Windows] Report module paths the way the loader spelled them (PR #226492)
via lldb-commits
lldb-commits at lists.llvm.org
Fri Sep 25 07:52:55 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Charles Zablit (charles-zablit)
<details>
<summary>Changes</summary>
On Windows, when creating a target, the path gets canonicalized:
Consider a subst mapping from `C:\S` to `S:\`. A target created with `S:\a.exe` becomes `C:\S\a.exe`.
The process reports `C:\S\a.exe`, lldb adds a second module for a file it already has, and breakpoints get a stale unresolved location next to the real one. This is why swift-ci's Windows tests, which run from a `T:` subst drive, fail with `2 != 1`.
This patch matches what lldb-server does on Linux: report the paths the loader used instead.
- DLL load events use `lpImageName`, or the loader's module list on attach.
- The executable's path comes from `PEB::ProcessParameters->ImagePathName`.
- `ObjectFilePECOFF::ParseDependentModules` no longer resolves dependent DLL paths with `real_path`.
- `DynamicLoaderWindowsDYLD::DidAttach` loads the process's modules even when it does not know the target's executable path.
When the paths differ only in case, the on-disk spelling is kept.
Tested on Windows x86_64 from a subst drive. The patch fixes 37 API tests failures.
The patch adds a regression test `functionalities/module_aliased_path` that fails without the fix.
---
Full diff: https://github.com/llvm/llvm-project/pull/226492.diff
9 Files Affected:
- (modified) lldb/source/Plugins/DynamicLoader/Windows-DYLD/DynamicLoaderWindowsDYLD.cpp (+8-1)
- (modified) lldb/source/Plugins/ObjectFile/PECOFF/ObjectFilePECOFF.cpp (+2-3)
- (modified) lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp (+86-11)
- (modified) lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h (+6)
- (modified) lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp (+18-3)
- (added) lldb/test/API/functionalities/module_aliased_path/Makefile (+5)
- (added) lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py (+90)
- (added) lldb/test/API/functionalities/module_aliased_path/foo.c (+3)
- (added) lldb/test/API/functionalities/module_aliased_path/main.c (+6)
``````````diff
diff --git a/lldb/source/Plugins/DynamicLoader/Windows-DYLD/DynamicLoaderWindowsDYLD.cpp b/lldb/source/Plugins/DynamicLoader/Windows-DYLD/DynamicLoaderWindowsDYLD.cpp
index 2d6e97b067c30..fb0619364dcb9 100644
--- a/lldb/source/Plugins/DynamicLoader/Windows-DYLD/DynamicLoaderWindowsDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/Windows-DYLD/DynamicLoaderWindowsDYLD.cpp
@@ -148,8 +148,15 @@ void DynamicLoaderWindowsDYLD::DidAttach() {
// Try to fetch the load address of the file from the process, since there
// could be randomization of the load address.
lldb::addr_t load_addr = GetLoadAddress(executable);
- if (load_addr == LLDB_INVALID_ADDRESS)
+ if (load_addr == LLDB_INVALID_ADDRESS) {
+ // The process does not know the executable under the target's path: it
+ // was started through another path to the same file, or runs a different
+ // one. Take the modules it reports as loaded, which replaces the target's
+ // executable with the process's.
+ auto error = m_process->LoadModules();
+ LLDB_LOG_ERROR(log, std::move(error), "failed to load modules: {0}");
return;
+ }
// Request the process base address.
lldb::addr_t image_base = m_process->GetImageInfoAddress();
diff --git a/lldb/source/Plugins/ObjectFile/PECOFF/ObjectFilePECOFF.cpp b/lldb/source/Plugins/ObjectFile/PECOFF/ObjectFilePECOFF.cpp
index 985dd6b516bae..bc769e0394eda 100644
--- a/lldb/source/Plugins/ObjectFile/PECOFF/ObjectFilePECOFF.cpp
+++ b/lldb/source/Plugins/ObjectFile/PECOFF/ObjectFilePECOFF.cpp
@@ -1161,12 +1161,11 @@ uint32_t ObjectFilePECOFF::ParseDependentModules() {
// At this moment we only have the base name of the DLL. The full path can
// only be seen after the dynamic loading. Our best guess is Try to get it
// with the help of the object file's directory.
- llvm::SmallString<128> dll_fullpath;
FileSpec dll_specs(dll_name);
dll_specs.SetDirectory(m_file.GetDirectory());
- if (!llvm::sys::fs::real_path(dll_specs.GetPath(), dll_fullpath))
- m_deps_filespec->EmplaceBack(dll_fullpath);
+ if (FileSystem::Instance().Exists(dll_specs))
+ m_deps_filespec->Append(dll_specs);
else {
// Known DLLs or DLL not found in the object file directory.
m_deps_filespec->EmplaceBack(dll_name);
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 260ac0fe0faf6..67db11b8f4641 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -29,12 +29,14 @@
#include "lldb/Utility/LLDBLog.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/ConvertUTF.h"
+#include "llvm/Support/Path.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>
#include <pathcch.h>
#include <psapi.h>
+#include <winternl.h>
#ifndef STATUS_WX86_BREAKPOINT
#define STATUS_WX86_BREAKPOINT 0x4000001FL // For WOW64
@@ -453,6 +455,53 @@ DebuggerThread::HandleCreateThreadEvent(const CREATE_THREAD_DEBUG_INFO &info,
return DBG_CONTINUE;
}
+/// Read the path the process was created with from its process parameters
+/// (PEB::ProcessParameters->ImagePathName).
+///
+/// This keeps the subst drive, junction or symbolic link the process was
+/// launched through.
+static std::optional<std::string> GetImagePathFromPEB(HANDLE process) {
+ // winternl.h declares NtQueryInformationProcess, but there is no import
+ // library for it.
+ static LazyImport<decltype(&::NtQueryInformationProcess)>
+ s_query_information_process{L"ntdll.dll", "NtQueryInformationProcess"};
+ if (!s_query_information_process)
+ return std::nullopt;
+
+ PROCESS_BASIC_INFORMATION pbi = {};
+ if ((*s_query_information_process)(process, ProcessBasicInformation, &pbi,
+ sizeof(pbi), nullptr) < 0 ||
+ !pbi.PebBaseAddress)
+ return std::nullopt;
+
+ PEB peb = {};
+ if (!::ReadProcessMemory(process, pbi.PebBaseAddress, &peb, sizeof(peb),
+ nullptr) ||
+ !peb.ProcessParameters)
+ return std::nullopt;
+
+ RTL_USER_PROCESS_PARAMETERS params = {};
+ if (!::ReadProcessMemory(process, peb.ProcessParameters, ¶ms,
+ sizeof(params), nullptr) ||
+ !params.ImagePathName.Buffer || params.ImagePathName.Length == 0)
+ return std::nullopt;
+
+ std::wstring wpath(params.ImagePathName.Length / sizeof(wchar_t), L'\0');
+ if (!::ReadProcessMemory(process, params.ImagePathName.Buffer, wpath.data(),
+ params.ImagePathName.Length, nullptr))
+ return std::nullopt;
+
+ std::string path;
+ if (!llvm::convertWideToUTF8(wpath, path))
+ return std::nullopt;
+ // A process launched through an extended-length path has the "\\?\" prefix.
+ llvm::StringRef path_ref = path;
+ if (path_ref.consume_front("\\\\?\\UNC\\"))
+ return "\\\\" + path_ref.str();
+ path_ref.consume_front("\\\\?\\");
+ return path_ref.str();
+}
+
DWORD
DebuggerThread::HandleCreateProcessEvent(const CREATE_PROCESS_DEBUG_INFO &info,
DWORD thread_id) {
@@ -476,6 +525,7 @@ DebuggerThread::HandleCreateProcessEvent(const CREATE_PROCESS_DEBUG_INFO &info,
m_image_file = info.hFile;
lldb::addr_t load_addr = reinterpret_cast<lldb::addr_t>(info.lpBaseOfImage);
+ m_image_path = GetImagePathFromPEB(info.hProcess).value_or("");
m_debug_delegate->OnDebuggerConnected(load_addr);
return DBG_CONTINUE;
@@ -617,15 +667,17 @@ static std::optional<std::string> GetFileNameFromHandleFallback(HANDLE hFile) {
return GetMappedFileDosPath(::GetCurrentProcess(), pMem.get());
}
-static std::optional<std::string> GetFileNameByLoadAddress(HANDLE process,
- LPVOID base_addr) {
+/// The path the loader recorded for the module at \p base_addr, or nullopt if
+/// the loader's module list has no entry for it yet.
+static std::optional<std::string> GetLoaderModuleName(HANDLE process,
+ LPVOID base_addr) {
std::vector<wchar_t> module_filename(MAX_PATH + 1);
while (module_filename.size() <= PATHCCH_MAX_CCH) {
DWORD len =
::GetModuleFileNameExW(process, reinterpret_cast<HMODULE>(base_addr),
module_filename.data(), module_filename.size());
if (len == 0)
- break; // Not loaded as a module; fall back to the mapped-file query.
+ return std::nullopt;
if (len < module_filename.size()) {
std::string path_utf8;
llvm::convertWideToUTF8(std::wstring_view(module_filename.data(), len),
@@ -634,9 +686,7 @@ static std::optional<std::string> GetFileNameByLoadAddress(HANDLE process,
}
module_filename.resize(module_filename.size() * 2);
}
-
- // Fallback: ask the kernel for the file backing the mapping at this address.
- return GetMappedFileDosPath(process, base_addr);
+ return std::nullopt;
}
// Determine how many bytes can be read at `addr` in `process` before crossing
@@ -747,7 +797,25 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
action = m_debug_delegate->OnLoadDll(module_spec, load_addr, thread_id);
};
- std::optional<std::string> resolved_path;
+ HANDLE process = m_process.GetNativeProcess().GetSystemHandle();
+
+ // Prefer the path the loader used for the DLL, which keeps the subst drive,
+ // junction or symbolic link it was reached through.
+ std::optional<std::string> loader_path =
+ GetFileNameFromImageNameField(process, info);
+ if (loader_path && !llvm::sys::path::is_absolute(
+ *loader_path, llvm::sys::path::Style::windows))
+ loader_path.reset();
+ if (!loader_path)
+ loader_path = GetLoaderModuleName(process, info.lpBaseOfDll);
+ if (loader_path) {
+ llvm::StringRef path_ref = *loader_path;
+ path_ref.consume_front("\\\\?\\");
+ loader_path = path_ref.str();
+ }
+
+ // The file handle gives the resolved path, with the on-disk case.
+ std::optional<std::string> file_path;
if (info.hFile != nullptr) {
std::vector<wchar_t> buffer(1);
DWORD required_size =
@@ -760,17 +828,24 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
llvm::convertWideToUTF8(buffer.data(), path_str_utf8);
llvm::StringRef path_str = path_str_utf8;
path_str.consume_front("\\\\?\\");
- resolved_path = path_str.str();
+ file_path = path_str.str();
} else {
- resolved_path = GetFileNameFromHandleFallback(info.hFile);
+ file_path = GetFileNameFromHandleFallback(info.hFile);
}
}
- HANDLE process = m_process.GetNativeProcess().GetSystemHandle();
+ // The loader's path often differs from the on-disk one only in case (e.g.
+ // C:\windows\System32\KERNEL32.DLL). If that's the case, keep the on disk
+ // spelling.
+ std::optional<std::string> resolved_path = loader_path;
+ if (!resolved_path ||
+ (file_path &&
+ llvm::StringRef(*file_path).equals_insensitive(*resolved_path)))
+ resolved_path = file_path;
if (!resolved_path)
resolved_path = GetFileNameFromImageNameField(process, info);
if (!resolved_path)
- resolved_path = GetFileNameByLoadAddress(process, info.lpBaseOfDll);
+ resolved_path = GetMappedFileDosPath(process, info.lpBaseOfDll);
if (resolved_path)
on_load_dll(*resolved_path);
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
index 03202264b01ee..a9fa3ec0c7599 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
@@ -36,6 +36,11 @@ class DebuggerThread : public std::enable_shared_from_this<DebuggerThread> {
HostProcess GetProcess() const { return m_process; }
HostThread GetMainThread() const { return m_main_thread; }
+ /// The path the process was created with, as recorded in its process
+ /// parameters, or an empty string if it could not be read. Set by the
+ /// create-process event.
+ const std::string &GetImagePath() const { return m_image_path; }
+
/// Returns the exception the debug loop is currently reporting, or null if
/// there is none. Safe to call from any thread.
ExceptionRecordSP GetActiveException();
@@ -75,6 +80,7 @@ class DebuggerThread : public std::enable_shared_from_this<DebuggerThread> {
// The image file of the process being debugged.
HANDLE m_image_file = nullptr;
+ std::string m_image_path;
// The current exception waiting to be handled.
ExceptionRecordSP m_active_exception;
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 849931eaec372..0045696bd416b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -360,8 +360,10 @@ Status NativeProcessWindows::RemoveBreakpoint(lldb::addr_t addr,
return RemoveSoftwareBreakpoint(addr);
}
-// Resolve the fully qualified, normalized on disk path of a module loaded in
-// the target process.
+// Get the path of a module loaded in the target process, as the loader recorded
+// it.
+//
+// Keep the loader's spelling.
static bool GetLoadedModulePath(HANDLE process, HMODULE module,
std::string &path) {
std::vector<wchar_t> name(MAX_PATH);
@@ -408,7 +410,15 @@ static bool GetLoadedModulePath(HANDLE process, HMODULE module,
canonical.replace(0, wcslen(kUNCPrefix), L"\\\\");
else if (canonical.rfind(kDOSPrefix, 0) == 0)
canonical.erase(0, wcslen(kDOSPrefix));
- wpath = std::move(canonical);
+ std::wstring loader_path = wpath;
+ if (loader_path.rfind(kUNCPrefix, 0) == 0)
+ loader_path.replace(0, wcslen(kUNCPrefix), L"\\\\");
+ else if (loader_path.rfind(kDOSPrefix, 0) == 0)
+ loader_path.erase(0, wcslen(kDOSPrefix));
+ if (::_wcsicmp(canonical.c_str(), loader_path.c_str()) == 0)
+ wpath = std::move(canonical);
+ else
+ wpath = std::move(loader_path);
break;
}
full.resize(needed);
@@ -551,6 +561,11 @@ void NativeProcessWindows::OnDebuggerConnected(lldb::addr_t image_base) {
if (got_info) {
FileSpec exe = info.GetExecutableFile();
+ if (const std::string &image_path =
+ m_session_data->m_debugger->GetImagePath();
+ !image_path.empty() &&
+ !llvm::StringRef(image_path).equals_insensitive(exe.GetPath()))
+ exe = FileSpec(image_path);
if (exe) {
FileSystem::Instance().Resolve(exe);
m_loaded_modules.Add(exe, image_base);
diff --git a/lldb/test/API/functionalities/module_aliased_path/Makefile b/lldb/test/API/functionalities/module_aliased_path/Makefile
new file mode 100644
index 0000000000000..e2607a0b08a7c
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/Makefile
@@ -0,0 +1,5 @@
+C_SOURCES := main.c
+DYLIB_C_SOURCES := foo.c
+DYLIB_NAME := foo
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py b/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py
new file mode 100644
index 0000000000000..cde581ae87bcc
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py
@@ -0,0 +1,90 @@
+"""
+Test that a process launched through an aliased directory reports its modules
+under that directory, so LLDB does not duplicate the modules it already has.
+
+The target is created from a junction to the build directory. lldb-server must
+report the executable and the DLL next to it with the path the Windows loader
+recorded, which keeps the junction, rather than with the resolved path, which
+does not. Otherwise LLDB adds a second module for each file and the breakpoints
+get a location in both.
+"""
+
+import os
+import subprocess
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+ at requireWindows
+ at skipIfWindowsAndNoLLDBServer
+ at skipIfRemote
+class TestModuleAliasedPath(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def make_junction(self, real_dir):
+ """Create a junction to real_dir, skipping the test if the host can't
+ create one."""
+ alias_dir = real_dir + ".alias"
+ try:
+ if os.path.lexists(alias_dir):
+ os.rmdir(alias_dir)
+ subprocess.run(
+ 'mklink /J "%s" "%s"' % (alias_dir, real_dir),
+ shell=True,
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ except (OSError, subprocess.CalledProcessError) as e:
+ detail = getattr(e, "stderr", None) or e
+ self.skipTest("could not create a junction: %s" % detail)
+ self.addTearDownHook(lambda: os.rmdir(alias_dir))
+ self.assertNotEqual(os.path.realpath(alias_dir), alias_dir)
+ return alias_dir
+
+ def test_launch_through_aliased_path(self):
+ self.build()
+
+ alias_dir = self.make_junction(self.getBuildDir())
+
+ target = self.dbg.CreateTarget(os.path.join(alias_dir, "a.out"))
+ self.assertTrue(target, VALID_TARGET)
+ main_bkpt = target.BreakpointCreateBySourceRegex(
+ "break main", lldb.SBFileSpec("main.c")
+ )
+ foo_bkpt = target.BreakpointCreateBySourceRegex(
+ "break here", lldb.SBFileSpec("foo.c")
+ )
+ self.assertEqual(main_bkpt.GetNumLocations(), 1)
+
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertState(process.GetState(), lldb.eStateStopped)
+ self.assertIsNotNone(
+ lldbutil.get_one_thread_stopped_at_breakpoint(process, main_bkpt)
+ )
+
+ lib_name = self.platformContext.getFullLibName("foo")
+ for name in ["a.out", lib_name]:
+ paths = [
+ m.GetFileSpec().fullpath
+ for m in target.module_iter()
+ if m.GetFileSpec().GetFilename() == name
+ ]
+ self.assertEqual(len(paths), 1, "one module for %s: %s" % (name, paths))
+ self.assertTrue(
+ paths[0].lower().startswith(alias_dir.lower()),
+ "%s is reported under the junction" % paths[0],
+ )
+
+ for bkpt in [main_bkpt, foo_bkpt]:
+ self.assertEqual(bkpt.GetNumLocations(), 1)
+ self.assertEqual(bkpt.GetNumResolvedLocations(), 1)
+
+ process.Continue()
+ self.assertIsNotNone(
+ lldbutil.get_one_thread_stopped_at_breakpoint(process, foo_bkpt)
+ )
diff --git a/lldb/test/API/functionalities/module_aliased_path/foo.c b/lldb/test/API/functionalities/module_aliased_path/foo.c
new file mode 100644
index 0000000000000..375a5207d2654
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/foo.c
@@ -0,0 +1,3 @@
+int LLDB_DYLIB_EXPORT foo(void) {
+ return 42; // break here
+}
diff --git a/lldb/test/API/functionalities/module_aliased_path/main.c b/lldb/test/API/functionalities/module_aliased_path/main.c
new file mode 100644
index 0000000000000..3bd932f5e291e
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/main.c
@@ -0,0 +1,6 @@
+extern int foo(void);
+
+int main(void) {
+ int result = foo(); // break main
+ return result == 42 ? 0 : 1;
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/226492
More information about the lldb-commits
mailing list