[Lldb-commits] [lldb] [lldb][Windows] Key lldb-server's loaded-module list by base address (PR #223445)

Charles Zablit via lldb-commits lldb-commits at lists.llvm.org
Tue Sep 15 07:07:17 PDT 2026


https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/223445

>From e84abff575da6801dabd8725887a4d8fa1f4ff28 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Mon, 14 Sep 2026 16:43:02 +0100
Subject: [PATCH] [lldb][Windows] Key lldb-server's loaded-module list by base
 address

---
 .../Process/Windows/Common/LoadedModuleList.h |  81 ++++++++++++++
 .../Windows/Common/NativeProcessWindows.cpp   |  43 +++-----
 .../Windows/Common/NativeProcessWindows.h     |   4 +-
 lldb/unittests/Process/CMakeLists.txt         |   3 +
 lldb/unittests/Process/Windows/CMakeLists.txt |   6 +
 .../Process/Windows/LoadedModuleListTest.cpp  | 104 ++++++++++++++++++
 6 files changed, 213 insertions(+), 28 deletions(-)
 create mode 100644 lldb/source/Plugins/Process/Windows/Common/LoadedModuleList.h
 create mode 100644 lldb/unittests/Process/Windows/CMakeLists.txt
 create mode 100644 lldb/unittests/Process/Windows/LoadedModuleListTest.cpp

diff --git a/lldb/source/Plugins/Process/Windows/Common/LoadedModuleList.h b/lldb/source/Plugins/Process/Windows/Common/LoadedModuleList.h
new file mode 100644
index 0000000000000..6d3e0d858aad9
--- /dev/null
+++ b/lldb/source/Plugins/Process/Windows/Common/LoadedModuleList.h
@@ -0,0 +1,81 @@
+//===-- LoadedModuleList.h --------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_PROCESS_WINDOWS_COMMON_LOADEDMODULELIST_H
+#define LLDB_SOURCE_PLUGINS_PROCESS_WINDOWS_COMMON_LOADEDMODULELIST_H
+
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/lldb-types.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+
+#include <map>
+#include <optional>
+
+namespace lldb_private {
+
+/// The images mapped into the inferior. The same file can be mapped more than
+/// once, and an UNLOAD_DLL_DEBUG_EVENT only carries a base address, so every
+/// mapping has to be resolvable back to its file.
+class LoadedModuleList {
+  using Container = std::map<FileSpec, llvm::SmallVector<lldb::addr_t, 1>>;
+
+public:
+  void Add(const FileSpec &file_spec, lldb::addr_t base_addr) {
+    m_modules[file_spec].push_back(base_addr);
+  }
+
+  /// Drop the mapping at \p base_addr. \return the file if that was its last
+  /// mapping, an empty FileSpec otherwise.
+  FileSpec Remove(lldb::addr_t base_addr) {
+    for (auto it = m_modules.begin(); it != m_modules.end(); ++it) {
+      auto &base_addrs = it->second;
+      auto addr_it = llvm::find(base_addrs, base_addr);
+      if (addr_it == base_addrs.end())
+        continue;
+
+      base_addrs.erase(addr_it);
+      if (!base_addrs.empty())
+        return {};
+
+      FileSpec file_spec = it->first;
+      m_modules.erase(it);
+      return file_spec;
+    }
+    return {};
+  }
+
+  /// The address \p file_spec was first mapped at. That is the mapping the
+  /// loader bound the image's imports against, so it is the one to report.
+  std::optional<lldb::addr_t> GetBaseAddress(const FileSpec &file_spec) const {
+    auto it = m_modules.find(file_spec);
+    if (it == m_modules.end())
+      return std::nullopt;
+    return it->second.front();
+  }
+
+  /// \return the stored spelling of \p file_spec, or nullptr if it is not
+  /// mapped.
+  const FileSpec *FindFile(const FileSpec &file_spec) const {
+    auto it = m_modules.find(file_spec);
+    return it == m_modules.end() ? nullptr : &it->first;
+  }
+
+  bool IsEmpty() const { return m_modules.empty(); }
+  size_t GetSize() const { return m_modules.size(); }
+
+  Container::const_iterator begin() const { return m_modules.begin(); }
+  Container::const_iterator end() const { return m_modules.end(); }
+
+private:
+  Container m_modules;
+};
+
+} // namespace lldb_private
+
+#endif
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index bb1feca5eb097..0fa3eae2c7ed6 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -26,6 +26,7 @@
 #include "lldb/Target/MemoryRegionInfo.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Utility/State.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/ConvertUTF.h"
 #include "llvm/Support/Errc.h"
@@ -360,7 +361,7 @@ Status NativeProcessWindows::RemoveBreakpoint(lldb::addr_t addr,
 
 Status NativeProcessWindows::CacheLoadedModules() {
   Status error;
-  if (!m_loaded_modules.empty())
+  if (!m_loaded_modules.IsEmpty())
     return Status();
 
   // Retrieve loaded modules by a Target/Module-free implementation.
@@ -376,11 +377,11 @@ Status NativeProcessWindows::CacheLoadedModules() {
 
         FileSpec file_spec(path);
         FileSystem::Instance().Resolve(file_spec);
-        m_loaded_modules[file_spec] = (addr_t)me.modBaseAddr;
+        m_loaded_modules.Add(file_spec, (addr_t)me.modBaseAddr);
       } while (Module32Next(snapshot.get(), &me));
     }
 
-    if (!m_loaded_modules.empty())
+    if (!m_loaded_modules.IsEmpty())
       return Status();
   }
 
@@ -396,11 +397,9 @@ Status NativeProcessWindows::GetLoadedModuleFileSpec(const char *module_path,
 
   FileSpec module_file_spec(module_path);
   FileSystem::Instance().Resolve(module_file_spec);
-  for (auto &it : m_loaded_modules) {
-    if (it.first == module_file_spec) {
-      file_spec = it.first;
-      return Status();
-    }
+  if (const FileSpec *found = m_loaded_modules.FindFile(module_file_spec)) {
+    file_spec = *found;
+    return Status();
   }
   return Status::FromErrorStringWithFormat(
       "Module (%s) not found in process %" PRIu64 "!",
@@ -417,11 +416,9 @@ NativeProcessWindows::GetFileLoadAddress(const llvm::StringRef &file_name,
   load_addr = LLDB_INVALID_ADDRESS;
   FileSpec file_spec(file_name);
   FileSystem::Instance().Resolve(file_spec);
-  for (auto &it : m_loaded_modules) {
-    if (it.first == file_spec) {
-      load_addr = it.second;
-      return Status();
-    }
+  if (std::optional<addr_t> base = m_loaded_modules.GetBaseAddress(file_spec)) {
+    load_addr = *base;
+    return Status();
   }
   return Status::FromErrorStringWithFormat(
       "Can't get loaded address of file (%s) in process %" PRIu64 "!",
@@ -434,11 +431,11 @@ NativeProcessWindows::GetLoadedLibraries() {
     return error.ToError();
 
   std::vector<LoadedLibraryInfo> libs;
-  libs.reserve(m_loaded_modules.size());
-  for (const auto &[file_spec, base] : m_loaded_modules) {
+  libs.reserve(m_loaded_modules.GetSize());
+  for (const auto &[file_spec, base_addrs] : m_loaded_modules) {
     LoadedLibraryInfo info;
     info.name = file_spec.GetPath();
-    info.base_addr = base;
+    info.base_addr = base_addrs.front();
     libs.push_back(std::move(info));
   }
   return libs;
@@ -492,7 +489,7 @@ void NativeProcessWindows::OnDebuggerConnected(lldb::addr_t image_base) {
     FileSpec exe = info.GetExecutableFile();
     if (exe) {
       FileSystem::Instance().Resolve(exe);
-      m_loaded_modules[exe] = image_base;
+      m_loaded_modules.Add(exe, image_base);
     }
   }
 
@@ -742,7 +739,7 @@ DllEventAction NativeProcessWindows::OnLoadDll(const ModuleSpec &module_spec,
   FileSpec resolved = module_spec.GetFileSpec();
   if (resolved) {
     FileSystem::Instance().Resolve(resolved);
-    m_loaded_modules[resolved] = module_addr;
+    m_loaded_modules.Add(resolved, module_addr);
   }
   m_pending_library_events = true;
 
@@ -779,15 +776,7 @@ DllEventAction NativeProcessWindows::OnUnloadDll(lldb::addr_t module_addr,
   Log *log = GetLog(WindowsLog::Process);
   llvm::sys::ScopedLock lock(m_mutex);
 
-  FileSpec unloaded_spec;
-  for (auto it = m_loaded_modules.begin(); it != m_loaded_modules.end();) {
-    if (it->second == module_addr) {
-      unloaded_spec = it->first;
-      it = m_loaded_modules.erase(it);
-    } else {
-      ++it;
-    }
-  }
+  FileSpec unloaded_spec = m_loaded_modules.Remove(module_addr);
   m_pending_library_events = true;
 
   if (!m_initial_stop_seen || !m_client_supports_libraries_read)
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index 3074eea1bc932..114b4a200e18c 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -14,6 +14,7 @@
 #include "lldb/lldb-forward.h"
 
 #include "IDebugDelegate.h"
+#include "LoadedModuleList.h"
 #include "ProcessDebugger.h"
 
 namespace lldb_private {
@@ -172,7 +173,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
                                          const ExceptionRecord &record);
 
   Status CacheLoadedModules();
-  std::map<lldb_private::FileSpec, lldb::addr_t> m_loaded_modules;
+
+  LoadedModuleList m_loaded_modules;
 
   /// Set whenever an OS DLL load/unload event has been seen since the last stop
   /// reply.
diff --git a/lldb/unittests/Process/CMakeLists.txt b/lldb/unittests/Process/CMakeLists.txt
index a240d773c3f30..939fb9f741c68 100644
--- a/lldb/unittests/Process/CMakeLists.txt
+++ b/lldb/unittests/Process/CMakeLists.txt
@@ -4,6 +4,9 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux|Android")
   add_subdirectory(Linux)
   add_subdirectory(POSIX)
 endif()
+if (CMAKE_SYSTEM_NAME MATCHES "Windows")
+  add_subdirectory(Windows)
+endif()
 add_subdirectory(Utility)
 add_subdirectory(minidump)
 
diff --git a/lldb/unittests/Process/Windows/CMakeLists.txt b/lldb/unittests/Process/Windows/CMakeLists.txt
new file mode 100644
index 0000000000000..672c0983e88f1
--- /dev/null
+++ b/lldb/unittests/Process/Windows/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_lldb_unittest(ProcessWindowsTests
+  LoadedModuleListTest.cpp
+
+  LINK_LIBS
+    lldbUtility
+  )
diff --git a/lldb/unittests/Process/Windows/LoadedModuleListTest.cpp b/lldb/unittests/Process/Windows/LoadedModuleListTest.cpp
new file mode 100644
index 0000000000000..08c082d4f3b15
--- /dev/null
+++ b/lldb/unittests/Process/Windows/LoadedModuleListTest.cpp
@@ -0,0 +1,104 @@
+//===-- LoadedModuleListTest.cpp ------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/Process/Windows/Common/LoadedModuleList.h"
+#include "gtest/gtest.h"
+
+using namespace lldb_private;
+
+namespace {
+const FileSpec kDll("C:\\a\\one.dll", FileSpec::Style::windows);
+const FileSpec kOther("C:\\a\\two.dll", FileSpec::Style::windows);
+} // namespace
+
+TEST(LoadedModuleListTest, ReportsTheMappingItWasGiven) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+
+  EXPECT_EQ(modules.GetSize(), 1u);
+  EXPECT_EQ(modules.GetBaseAddress(kDll), 0x1000u);
+  EXPECT_EQ(*modules.FindFile(kDll), kDll);
+}
+
+TEST(LoadedModuleListTest, IsEmptyUntilSomethingIsMapped) {
+  LoadedModuleList modules;
+  EXPECT_TRUE(modules.IsEmpty());
+  modules.Add(kDll, 0x1000);
+  EXPECT_FALSE(modules.IsEmpty());
+}
+
+TEST(LoadedModuleListTest, DoesNotFindAnUnmappedFile) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+
+  EXPECT_EQ(modules.GetBaseAddress(kOther), std::nullopt);
+  EXPECT_EQ(modules.FindFile(kOther), nullptr);
+}
+
+TEST(LoadedModuleListTest, ReportsTheFirstOfSeveralMappings) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+  modules.Add(kDll, 0x9000);
+
+  // One entry per file, at the address the loader mapped it first.
+  EXPECT_EQ(modules.GetSize(), 1u);
+  EXPECT_EQ(modules.GetBaseAddress(kDll), 0x1000u);
+}
+
+TEST(LoadedModuleListTest, KeepsTheFileWhileAnyMappingRemains) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+  modules.Add(kDll, 0x9000);
+
+  // Unmapping the second mapping must not retire the file: the program is
+  // still running from the first one.
+  EXPECT_FALSE(modules.Remove(0x9000));
+  EXPECT_EQ(modules.GetBaseAddress(kDll), 0x1000u);
+
+  EXPECT_EQ(modules.Remove(0x1000), kDll);
+  EXPECT_TRUE(modules.IsEmpty());
+}
+
+TEST(LoadedModuleListTest, FallsBackToASurvivingMapping) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+  modules.Add(kDll, 0x9000);
+
+  // Dropping the reported mapping leaves the other one to report.
+  EXPECT_FALSE(modules.Remove(0x1000));
+  EXPECT_EQ(modules.GetBaseAddress(kDll), 0x9000u);
+}
+
+TEST(LoadedModuleListTest, RemovesOnlyTheGivenMapping) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+  modules.Add(kOther, 0x2000);
+
+  EXPECT_EQ(modules.Remove(0x1000), kDll);
+  EXPECT_EQ(modules.GetSize(), 1u);
+  EXPECT_EQ(modules.GetBaseAddress(kOther), 0x2000u);
+}
+
+TEST(LoadedModuleListTest, IgnoresAnUnknownAddress) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+
+  EXPECT_FALSE(modules.Remove(0xdead));
+  EXPECT_EQ(modules.GetSize(), 1u);
+}
+
+TEST(LoadedModuleListTest, SameAddressTwiceIsNotOneMapping) {
+  LoadedModuleList modules;
+  modules.Add(kDll, 0x1000);
+  modules.Add(kDll, 0x1000);
+
+  // Two reported loads owe two reported unloads, even at the same base.
+  EXPECT_FALSE(modules.Remove(0x1000));
+  EXPECT_EQ(modules.Remove(0x1000), kDll);
+  EXPECT_TRUE(modules.IsEmpty());
+}



More information about the lldb-commits mailing list