https://github.com/barsolo2000 updated 
https://github.com/llvm/llvm-project/pull/214576

>From c4120d1d1e79b380cb225ab524445b67a1dd28c5 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Thu, 6 Aug 2026 13:44:31 -0700
Subject: [PATCH 1/6] [lldb] Add target modules replace command

---
 lldb/include/lldb/Target/DynamicLoader.h      |  16 +
 lldb/include/lldb/Target/Target.h             |  23 ++
 lldb/source/Commands/CommandObjectTarget.cpp  | 220 ++++++++++++
 lldb/source/Commands/Options.td               |  12 +
 .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp     |  15 +
 .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.h       |   4 +
 lldb/source/Target/Target.cpp                 |  86 +++++
 .../commands/target/modules/replace/Makefile  |  18 +
 .../replace/TestTargetModulesReplace.py       | 318 ++++++++++++++++++
 .../target/modules/replace/hidden/v.cpp       |  11 +
 .../commands/target/modules/replace/main.cpp  |  24 ++
 .../target/modules/replace/other_main.cpp     |   4 +
 .../API/commands/target/modules/replace/v.cpp |  10 +
 .../completion/TestCompletion.py              |   9 +
 14 files changed, 770 insertions(+)
 create mode 100644 lldb/test/API/commands/target/modules/replace/Makefile
 create mode 100644 
lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
 create mode 100644 lldb/test/API/commands/target/modules/replace/hidden/v.cpp
 create mode 100644 lldb/test/API/commands/target/modules/replace/main.cpp
 create mode 100644 lldb/test/API/commands/target/modules/replace/other_main.cpp
 create mode 100644 lldb/test/API/commands/target/modules/replace/v.cpp

diff --git a/lldb/include/lldb/Target/DynamicLoader.h 
b/lldb/include/lldb/Target/DynamicLoader.h
index 997826af2bed4..779ff2d3a70be 100644
--- a/lldb/include/lldb/Target/DynamicLoader.h
+++ b/lldb/include/lldb/Target/DynamicLoader.h
@@ -207,6 +207,22 @@ class DynamicLoader : public PluginInterface {
     return LLDB_INVALID_ADDRESS;
   }
 
+  /// Inform the dynamic loader that Target::ReplaceModule() has swapped one
+  /// module for another.
+  ///
+  /// \param[in] old_module_sp
+  ///     The module that was removed from the target.
+  ///
+  /// \param[in] new_module_sp
+  ///     The module that took its place.
+  ///
+  /// \return
+  ///     An error if this loader cannot place the replacement correctly.
+  virtual Status ReplaceModule(const lldb::ModuleSP &old_module_sp,
+                               const lldb::ModuleSP &new_module_sp) {
+    return Status();
+  }
+
   /// Locates or creates a module given by \p file and updates/loads the
   /// resulting module at the virtual base address \p base_addr.
   /// Note that this calls Target::GetOrCreateModule with notify being false,
diff --git a/lldb/include/lldb/Target/Target.h 
b/lldb/include/lldb/Target/Target.h
index 39602421cfd96..cfd80fd9d0ae1 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1158,6 +1158,29 @@ class Target : public 
std::enable_shared_from_this<Target>,
 
   void ModulesDidUnload(ModuleList &module_list, bool delete_locations);
 
+  /// Replace a module in this target with a different one.
+  ///
+  /// Removes \a old_module_sp, unloading its sections and deleting the
+  /// breakpoint locations that resolved into it, then adds \a new_module_sp at
+  /// the same load address and tells the dynamic loader about the swap.
+  ///
+  /// To attach debug info to a module that is otherwise fine, add a symbol 
file
+  /// to it instead.
+  ///
+  /// \param[in] old_module_sp
+  ///     The module to remove. Passed by value because this drops the last
+  ///     reference the target holds, letting the module be destroyed here once
+  ///     nothing points into it.
+  ///
+  /// \param[in] new_module_sp
+  ///     The module to put in its place. It may already have been added to the
+  ///     target, as Target::GetOrCreateModule() does.
+  ///
+  /// \return
+  ///     An error if the replacement could not be completed.
+  Status ReplaceModule(lldb::ModuleSP old_module_sp,
+                       const lldb::ModuleSP &new_module_sp);
+
   void SymbolsDidLoad(ModuleList &module_list);
 
   void ClearModules(bool delete_locations);
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp 
b/lldb/source/Commands/CommandObjectTarget.cpp
index f77e87ad43aa2..5384897b4f255 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -41,6 +41,7 @@
 #include "lldb/Symbol/UnwindPlan.h"
 #include "lldb/Symbol/VariableList.h"
 #include "lldb/Target/ABI.h"
+#include "lldb/Target/DynamicLoader.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/RegisterContext.h"
 #include "lldb/Target/SectionLoadList.h"
@@ -3135,6 +3136,222 @@ class CommandObjectTargetModulesLoad
   OptionGroupUInt64 m_slide_option;
 };
 
+#pragma mark CommandObjectTargetModulesReplace
+
+#define LLDB_OPTIONS_target_modules_replace
+#include "CommandOptions.inc"
+
+// Replace a module in the target with a different file on disk.
+
+class CommandObjectTargetModulesReplace : public CommandObjectParsed {
+public:
+  CommandObjectTargetModulesReplace(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target modules replace",
+            "Replace a module in the current target with a file that has more "
+            "complete contents, usually to resolve a placeholder module from a 
"
+            "core file once the real binary has been located. To attach debug "
+            "info to a module that is otherwise fine, use 'target symbols "
+            "add'.",
+            "target modules replace [--old-path <path>] [--force] <path>",
+            eCommandRequiresTarget | eCommandTryTargetAPILock |
+                eCommandProcessMustBePaused) {
+    AddSimpleArgumentList(eArgTypePath, eArgRepeatPlain);
+  }
+
+  ~CommandObjectTargetModulesReplace() override = default;
+
+  Options *GetOptions() override { return &m_options; }
+
+  class CommandOptions : public Options {
+  public:
+    CommandOptions() = default;
+
+    ~CommandOptions() override = default;
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      const int short_option = m_getopt_table[option_idx].val;
+
+      switch (short_option) {
+      case 'o':
+        m_old_path.assign(std::string(option_arg));
+        break;
+      case 'f':
+        m_force = true;
+        break;
+      default:
+        llvm_unreachable("Unimplemented option");
+      }
+      return Status();
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      m_old_path.clear();
+      m_force = false;
+    }
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::ArrayRef(g_target_modules_replace_options);
+    }
+
+    std::string m_old_path;
+    bool m_force = false;
+  };
+
+protected:
+  CommandOptions m_options;
+
+  // Find the module the new file is meant to stand in for. Preferred order is
+  // the path the user gave, then the UUID read out of the new file, then its
+  // basename.
+  ModuleSP FindModuleToReplace(Target &target, const FileSpec &new_file_spec,
+                               const UUID &new_uuid,
+                               CommandReturnObject &result) {
+    ModuleSpec search_spec;
+    llvm::StringRef description;
+
+    if (!m_options.m_old_path.empty()) {
+      search_spec.GetFileSpec().SetPath(m_options.m_old_path);
+      description = "the given path";
+    } else if (new_uuid.IsValid()) {
+      search_spec.GetUUID() = new_uuid;
+      description = "a matching UUID";
+    } else {
+      search_spec.GetFileSpec().SetFilename(new_file_spec.GetFilename());
+      description = "a matching name";
+    }
+
+    ModuleList matches;
+    target.GetImages().FindModules(search_spec, matches);
+
+    if (matches.IsEmpty()) {
+      // A file with a UUID that names nothing in the target is still worth
+      // trying by name, the placeholder it should replace may have been built
+      // without one.
+      if (m_options.m_old_path.empty() && new_uuid.IsValid()) {
+        ModuleSpec by_name;
+        by_name.GetFileSpec().SetFilename(new_file_spec.GetFilename());
+        target.GetImages().FindModules(by_name, matches);
+        description =
+            matches.IsEmpty() ? "a matching UUID or name" : "a matching name";
+      }
+      if (matches.IsEmpty()) {
+        result.AppendErrorWithFormatv(
+            "no module in the target was found by {0}, use the --old-path "
+            "option to name the module to replace",
+            description);
+        return ModuleSP();
+      }
+    }
+
+    if (matches.GetSize() > 1) {
+      StreamString paths;
+      for (size_t i = 0; i < matches.GetSize(); ++i)
+        paths.Format("\n  {0}", matches.GetModuleAtIndex(i)->GetFileSpec());
+      result.AppendErrorWithFormatv(
+          "{0} modules in the target were found by {1}, use the --old-path "
+          "option to name one of:{2}",
+          matches.GetSize(), description, paths.GetString());
+      return ModuleSP();
+    }
+
+    return matches.GetModuleAtIndex(0);
+  }
+
+  void DoExecute(Args &args, CommandReturnObject &result) override {
+    Target *target = GetTarget();
+    assert(target && "target guaranteed by eCommandRequiresTarget");
+
+    if (args.GetArgumentCount() != 1) {
+      result.AppendError(
+          "'target modules replace' takes one argument: the path of the file "
+          "to replace a module with");
+      return;
+    }
+    llvm::StringRef new_module_path = args.GetArgumentAtIndex(0);
+
+    // Nothing below may change the target until Target::ReplaceModule() is
+    // called, so a failure can never leave the target half way through a
+    // replacement.
+    FileSpec new_file_spec(new_module_path);
+    FileSystem::Instance().Resolve(new_file_spec);
+    if (!FileSystem::Instance().Exists(new_file_spec)) {
+      std::string resolved_path = new_file_spec.GetPath();
+      if (resolved_path != new_module_path)
+        result.AppendErrorWithFormatv(
+            "invalid module path '{0}' with resolved path '{1}'",
+            new_module_path, resolved_path);
+      else
+        result.AppendErrorWithFormatv("invalid module path '{0}'",
+                                      new_module_path);
+      return;
+    }
+
+    // Read the UUID straight from the file rather than from a Module, so the
+    // module the new file should replace can be found before anything is added
+    // to the target.
+    UUID new_uuid;
+    ModuleSpecList file_specs =
+        ObjectFile::GetModuleSpecifications(new_file_spec, 0, 0);
+    if (file_specs.GetSize() > 0) {
+      ModuleSpec arch_spec;
+      arch_spec.GetArchitecture() = target->GetArchitecture();
+      ModuleSpec matching_spec;
+      if (file_specs.FindMatchingModuleSpec(arch_spec, matching_spec))
+        new_uuid = matching_spec.GetUUID();
+      else if (file_specs.GetSize() == 1)
+        new_uuid = file_specs.GetModuleSpecRefAtIndex(0).GetUUID();
+    }
+
+    ModuleSP old_module_sp =
+        FindModuleToReplace(*target, new_file_spec, new_uuid, result);
+    if (!old_module_sp)
+      return;
+
+    // Different UUIDs mean the new file is not the binary that ran, so the
+    // symbols would not describe the memory the target has.
+    const UUID &old_uuid = old_module_sp->GetUUID();
+    if (!m_options.m_force && old_uuid.IsValid() && new_uuid.IsValid() &&
+        old_uuid != new_uuid) {
+      result.AppendErrorWithFormatv(
+          "'{0}' has UUID {1}, which does not match UUID {2} of the module it "
+          "would replace, '{3}'. Use the --force option to replace it anyway",
+          new_file_spec.GetPath(), new_uuid.GetAsString(),
+          old_uuid.GetAsString(), old_module_sp->GetFileSpec().GetPath());
+      return;
+    }
+
+    ModuleSpec new_module_spec(new_file_spec);
+    if (!new_module_spec.GetArchitecture().IsValid())
+      new_module_spec.GetArchitecture() = target->GetArchitecture();
+
+    Status error;
+    ModuleSP new_module_sp =
+        target->GetOrCreateModule(new_module_spec, /*notify=*/false, &error);
+    if (!new_module_sp) {
+      if (error.Fail())
+        result.SetError(error.takeError());
+      else
+        result.AppendErrorWithFormatv("unsupported module: {0}",
+                                      new_file_spec.GetPath());
+      return;
+    }
+
+    const std::string old_module_desc = old_module_sp->GetFileSpec().GetPath();
+    Status replace_error =
+        target->ReplaceModule(std::move(old_module_sp), new_module_sp);
+    if (replace_error.Fail()) {
+      result.SetError(replace_error.takeError());
+      return;
+    }
+
+    result.AppendMessageWithFormatv("replaced '{0}' with '{1}'",
+                                    old_module_desc, new_file_spec.GetPath());
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
 #pragma mark CommandObjectTargetModulesList
 // List images with associated information
 #define LLDB_OPTIONS_target_modules_list
@@ -4230,6 +4447,9 @@ class CommandObjectTargetModules : public 
CommandObjectMultiword {
     LoadSubCommand(
         "lookup",
         CommandObjectSP(new CommandObjectTargetModulesLookup(interpreter)));
+    LoadSubCommand(
+        "replace",
+        CommandObjectSP(new CommandObjectTargetModulesReplace(interpreter)));
     LoadSubCommand(
         "search-paths",
         CommandObjectSP(
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index ab851725979ef..3d5a6b19f3e2b 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1820,6 +1820,18 @@ let Command = "target modules show unwind" in {
         Desc<"Show cached unwind information">;
 }
 
+let Command = "target modules replace" in {
+  def target_modules_replace_old_path
+      : Option<"old-path", "o">,
+        Arg<"Path">,
+        Desc<"Path of the module in the target to replace. Only needed when it 
"
+             "cannot be worked out from the new file.">;
+  def target_modules_replace_force
+      : Option<"force", "f">,
+        Desc<"Replace the module even when its UUID does not match the UUID of 
"
+             "the new file.">;
+}
+
 let Command = "target modules lookup" in {
   def target_modules_lookup_address : Option<"address", "a">,
                                       Group<1>,
diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index 70d24c45552dd..f54bf49e5f6a7 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -211,6 +211,21 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const 
ModuleSP &module_sp) {
   return std::nullopt;
 }
 
+Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp,
+                                             const ModuleSP &new_module_sp) {
+  // Images are mapped in one piece here, so the target's placement is already
+  // correct. Only the link map address needs moving, without it no thread 
local
+  // in the replacement can be resolved.
+  llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex);
+  auto it = m_loaded_modules.find(old_module_sp);
+  if (it == m_loaded_modules.end())
+    return Status();
+  const addr_t link_map_addr = it->second;
+  m_loaded_modules.erase(it);
+  m_loaded_modules[new_module_sp] = link_map_addr;
+  return Status();
+}
+
 void DynamicLoaderPOSIXDYLD::UpdateLoadedSections(ModuleSP module,
                                                   addr_t link_map_addr,
                                                   addr_t base_addr,
diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
index 6efb92673a13c..53f68975e3efc 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
@@ -66,6 +66,10 @@ class DynamicLoaderPOSIXDYLD : public 
lldb_private::DynamicLoader {
       llvm::function_ref<bool(const lldb_private::Thread &)>
           save_thread_predicate) override;
 
+  lldb_private::Status
+  ReplaceModule(const lldb::ModuleSP &old_module_sp,
+                const lldb::ModuleSP &new_module_sp) override;
+
 protected:
   /// Runtime linker rendezvous structure.
   DYLDRendezvous m_rendezvous;
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index a317ba3ccb3f9..b29af9775b87e 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -48,6 +48,7 @@
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Symbol/Symbol.h"
 #include "lldb/Target/ABI.h"
+#include "lldb/Target/DynamicLoader.h"
 #include "lldb/Target/ExecutionContext.h"
 #include "lldb/Target/Language.h"
 #include "lldb/Target/LanguageRuntime.h"
@@ -2023,6 +2024,91 @@ void Target::ModulesDidUnload(ModuleList &module_list, 
bool delete_locations) {
   }
 }
 
+Status Target::ReplaceModule(ModuleSP old_module_sp,
+                             const ModuleSP &new_module_sp) {
+  if (!old_module_sp || !new_module_sp)
+    return Status::FromErrorString("invalid module");
+
+  if (old_module_sp == new_module_sp)
+    return Status::FromErrorStringWithFormatv(
+        "'{0}' is already the module being replaced",
+        new_module_sp->GetFileSpec());
+
+  // Where the old module sits, so the replacement can be given the same
+  // address. Read it before anything unloads it.
+  addr_t base_load_addr = LLDB_INVALID_ADDRESS;
+  if (ObjectFile *object_file = old_module_sp->GetObjectFile()) {
+    Address base_addr = object_file->GetBaseAddress();
+    if (base_addr.IsValid())
+      base_load_addr = base_addr.GetLoadAddress(this);
+  }
+
+  // Also keeps the old module alive across ModulesDidUnload(), which reaches
+  // its breakpoint locations through section_sp->GetModule(), a weak
+  // reference.
+  ModuleList unloaded_modules;
+  unloaded_modules.Append(old_module_sp, /*notify=*/false);
+
+  if (m_images.GetIndexForModule(old_module_sp.get()) != LLDB_INVALID_INDEX32)
+    m_images.Remove(old_module_sp, /*notify=*/false);
+
+  // Unloads its sections and deletes its breakpoint locations. Must be
+  // explicit, no notification path passes delete_locations=true.
+  ModulesDidUnload(unloaded_modules, /*delete_locations=*/true);
+
+  // Target::GetOrCreateModule() adds the module it creates, so the replacement
+  // is usually in the target already.
+  if (m_images.GetIndexForModule(new_module_sp.get()) == LLDB_INVALID_INDEX32)
+    m_images.Append(new_module_sp, /*notify=*/false);
+
+  // ModuleList keeps the executable at index 0, but the replacement was
+  // appended while the old one still held that slot. Add it again to sort it 
to
+  // the front.
+  if (ObjectFile *new_object_file = new_module_sp->GetObjectFile()) {
+    if (new_object_file->GetType() == ObjectFile::eTypeExecutable &&
+        m_images.GetIndexForModule(new_module_sp.get()) != 0) {
+      m_images.Remove(new_module_sp, /*notify=*/false);
+      m_images.Append(new_module_sp, /*notify=*/false);
+    }
+  }
+
+  // Generic placement, correct whenever a module is mapped in one piece. The
+  // dynamic loader corrects it below on platforms where it is not.
+  if (base_load_addr != LLDB_INVALID_ADDRESS) {
+    bool changed = false;
+    new_module_sp->SetLoadAddress(*this, base_load_addr,
+                                  /*value_is_offset=*/false, changed);
+  }
+
+  // Let the dynamic loader redo the load addresses if this platform needs it,
+  // and move over anything it tracks per module, such as the link map address
+  // thread locals are found through.
+  Status error;
+  if (m_process_sp) {
+    if (DynamicLoader *dyld = m_process_sp->GetDynamicLoader())
+      error = dyld->ReplaceModule(old_module_sp, new_module_sp);
+  }
+
+  // Sections must be in place first, resolving breakpoints into a module whose
+  // sections are not loaded yields locations with no address.
+  ModuleList added_modules;
+  added_modules.Append(new_module_sp, /*notify=*/false);
+  ModulesDidLoad(added_modules);
+
+  // Drop the old module from the shared module cache so a later lookup of the
+  // same path cannot resurrect it.
+  unloaded_modules.Clear();
+  std::weak_ptr<Module> old_module_wp(old_module_sp->weak_from_this());
+  old_module_sp.reset();
+  ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp);
+
+  // Cached stack frames and register contexts can still hold the old module.
+  if (m_process_sp)
+    m_process_sp->Flush();
+
+  return error;
+}
+
 bool Target::ModuleIsExcludedForUnconstrainedSearches(
     const FileSpec &module_file_spec) {
   if (GetBreakpointsConsultPlatformAvoidList()) {
diff --git a/lldb/test/API/commands/target/modules/replace/Makefile 
b/lldb/test/API/commands/target/modules/replace/Makefile
new file mode 100644
index 0000000000000..1d76be88fc41a
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/Makefile
@@ -0,0 +1,18 @@
+CXX_SOURCES := main.cpp
+USE_LIBDL := 1
+
+a.out: lib_v hidden_lib_v other_exe
+
+include Makefile.rules
+
+other_exe:
+       "$(MAKE)" -f $(MAKEFILE_RULES) \
+               CXX_SOURCES=other_main.cpp EXE=other.out
+
+lib_v:
+       "$(MAKE)" -f $(MAKEFILE_RULES) \
+               DYLIB_ONLY=YES DYLIB_CXX_SOURCES=v.cpp DYLIB_NAME=replace_v
+
+hidden_lib_v:
+       "$(MAKE)" VPATH=$(SRCDIR)/hidden -C hidden -f $(MAKEFILE_RULES) \
+               DYLIB_ONLY=YES DYLIB_CXX_SOURCES=v.cpp DYLIB_NAME=replace_v
diff --git 
a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py 
b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
new file mode 100644
index 0000000000000..6b1f657b04387
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
@@ -0,0 +1,318 @@
+"""
+Test the "target modules replace" command.
+"""
+
+import os
+import shutil
+
+import lldb
+from lldbsuite.test import lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TargetModulesReplaceTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        # The "v2" variant of the library is built into a subdirectory so that
+        # it can share a soname with the "v1" variant.
+        lldbutil.mkdir_p(self.getBuildArtifact("hidden"))
+
+    def build_and_get_paths(self):
+        """Build and return the paths of the two library variants, plus a copy
+        of v1 at a third path. The copy shares v1's UUID, the v2 variant does
+        not, which is what lets the two lookup paths be tested apart."""
+        self.build()
+        lib_name = self.platformContext.getFullLibName("replace_v")
+        v1 = self.getBuildArtifact(lib_name)
+        v2 = os.path.join(self.getBuildDir(), "hidden", lib_name)
+        v1_copy = self.getBuildArtifact("copy_of_" + lib_name)
+        shutil.copyfile(v1, v1_copy)
+        for path in (v1, v2, v1_copy):
+            self.assertTrue(os.path.exists(path), "%s was built" % path)
+        return v1, v2, v1_copy
+
+    def static_target_with_v1(self):
+        """Make a target with the v1 library added but nothing loaded."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+        self.runCmd("target modules add '%s'" % v1)
+        return target, v1, v2, v1_copy
+
+    def base_load_address(self, module, target):
+        return module.GetObjectFileHeaderAddress().GetLoadAddress(target)
+
+    def test_matching_uuid_finds_the_module(self):
+        """A single argument is enough when the UUID identifies the module."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        num_modules = target.GetNumModules()
+
+        # No --old-path and no --force: the copy shares v1's UUID, so the 
module
+        # to replace can be worked out from the file alone.
+        self.runCmd("target modules replace '%s'" % v1_copy)
+
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.assertFalse(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v1_copy)).IsValid())
+
+    def test_mismatched_uuid_is_an_error(self):
+        """A file that is not the same build is refused unless forced."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        num_modules = target.GetNumModules()
+
+        self.expect(
+            "target modules replace '%s'" % v2,
+            error=True,
+            substrs=["does not match UUID", "--force"],
+        )
+
+        # The target must be left exactly as it was.
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
+        self.assertFalse(target.FindModule(lldb.SBFileSpec(v2)).IsValid())
+
+        # And --force goes through.
+        self.runCmd("target modules replace --force '%s'" % v2)
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v2)).IsValid())
+
+    def test_old_path_option(self):
+        """--old-path names the module to replace explicitly."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+
+        self.runCmd("target modules replace --old-path '%s' --force '%s'" % 
(v1, v2))
+        self.assertFalse(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v2)).IsValid())
+
+    def test_no_matching_module(self):
+        """A file that matches nothing points the user at --old-path."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        unrelated = self.getBuildArtifact("other.out")
+
+        self.expect(
+            "target modules replace '%s'" % unrelated,
+            error=True,
+            substrs=["no module in the target was found", "--old-path"],
+        )
+
+    def test_module_is_replaced_not_mutated(self):
+        """The old module is removed and a distinct new one takes its place."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+
+        old_module = target.FindModule(lldb.SBFileSpec(v1))
+        self.assertTrue(old_module.IsValid(), "v1 is in the target")
+        old_uuid = old_module.GetUUIDString()
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        new_module = target.FindModule(lldb.SBFileSpec(v2))
+        self.assertTrue(new_module.IsValid(), "v2 was added to the target")
+        self.assertNotEqual(old_uuid, new_module.GetUUIDString())
+
+        # Symbols now come from the replacement.
+        self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid())
+        self.assertFalse(new_module.FindSymbol("only_in_v1").IsValid())
+
+        # The old module object was not modified in place. This is the guard
+        # against implementing the command by swapping the ObjectFile out from
+        # under a live Module, which leaves stale pointers behind.
+        self.assertEqual(old_module.GetUUIDString(), old_uuid)
+        self.assertTrue(
+            old_module.FindSymbol("only_in_v1").IsValid(),
+            "the replaced module still describes its own file",
+        )
+
+    def test_unloaded_module_stays_unloaded(self):
+        """Replacing a module that was never loaded doesn't load anything."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+
+        old_module = target.FindModule(lldb.SBFileSpec(v1))
+        self.assertEqual(
+            self.base_load_address(old_module, target), 
lldb.LLDB_INVALID_ADDRESS
+        )
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        new_module = target.FindModule(lldb.SBFileSpec(v2))
+        self.assertEqual(
+            self.base_load_address(new_module, target), 
lldb.LLDB_INVALID_ADDRESS
+        )
+        for section in new_module.section_iter():
+            self.assertEqual(section.GetLoadAddress(target), 
lldb.LLDB_INVALID_ADDRESS)
+
+    def test_load_address_is_preserved(self):
+        """A loaded module's replacement is loaded at the same address."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        self.runCmd("target modules load --file '%s' --slide 0x100000" % v1)
+
+        old_module = target.FindModule(lldb.SBFileSpec(v1))
+        base_before = self.base_load_address(old_module, target)
+        self.assertNotEqual(base_before, lldb.LLDB_INVALID_ADDRESS)
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        # The image base is what is preserved. Individual section addresses are
+        # not comparable: the two files lay their sections out differently.
+        new_module = target.FindModule(lldb.SBFileSpec(v2))
+        self.assertEqual(self.base_load_address(new_module, target), 
base_before)
+
+        # The replaced module's sections were unloaded.
+        for section in old_module.section_iter():
+            self.assertEqual(
+                section.GetLoadAddress(target),
+                lldb.LLDB_INVALID_ADDRESS,
+                "section %s of the replaced module was unloaded" % 
section.GetName(),
+            )
+
+    def test_replace_executable(self):
+        """The replacement executable stays at the front of the module list."""
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        other = self.getBuildArtifact("other.out")
+
+        target = self.dbg.CreateTarget(exe)
+        self.assertTrue(target, VALID_TARGET)
+        self.assertEqual(
+            target.GetModuleAtIndex(0).GetFileSpec().GetFilename(), "a.out"
+        )
+        num_modules = target.GetNumModules()
+
+        self.runCmd(
+            "target modules replace --old-path '%s' --force '%s'" % (exe, 
other)
+        )
+
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.assertEqual(
+            target.GetModuleAtIndex(0).GetFileSpec().GetFilename(),
+            "other.out",
+            "the replacement executable is at index 0",
+        )
+        self.assertEqual(
+            target.GetExecutable().GetFilename(),
+            "other.out",
+            "the target's executable follows the replacement",
+        )
+
+    def test_round_trip(self):
+        """Replacing back and forth doesn't accumulate or drop modules."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        num_modules = target.GetNumModules()
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.runCmd("target modules replace --force '%s'" % v1)
+        self.assertEqual(target.GetNumModules(), num_modules)
+
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
+        self.assertFalse(target.FindModule(lldb.SBFileSpec(v2)).IsValid())
+
+    def test_errors(self):
+        """Bad input is rejected without touching the target."""
+        target, v1, v2, v1_copy = self.static_target_with_v1()
+        num_modules = target.GetNumModules()
+
+        self.expect(
+            "target modules replace /no/such/file",
+            error=True,
+            substrs=["invalid module path"],
+        )
+        self.expect(
+            "target modules replace",
+            error=True,
+            substrs=["takes one argument"],
+        )
+        self.expect(
+            "target modules replace --old-path /not/in/the/target '%s'" % v2,
+            error=True,
+            substrs=["no module in the target was found"],
+        )
+
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
+
+    @skipIfWindows
+    @skipIfRemote
+    def test_breakpoints_move_to_the_replacement(self):
+        """Breakpoint locations are re-resolved into the new module."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+        target.BreakpointCreateBySourceRegex(
+            "break after dlopen", lldb.SBFileSpec("main.cpp")
+        )
+
+        launch_info = target.GetLaunchInfo()
+        launch_info.SetArguments([v1], True)
+        error = lldb.SBError()
+        process = target.Launch(launch_info, error)
+        self.assertSuccess(error, "the process launched")
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        old_module = target.FindModule(lldb.SBFileSpec(v1))
+        self.assertTrue(old_module.IsValid(), "v1 was dlopen'd")
+        old_uuid = old_module.GetUUIDString()
+
+        # A breakpoint on a symbol both variants define, and one on a symbol
+        # only the old variant defines.
+        common_bp = target.BreakpointCreateByName("common_func")
+        self.assertEqual(common_bp.GetNumLocations(), 1)
+        only_v1_bp = target.BreakpointCreateByName("only_in_v1")
+        self.assertEqual(only_v1_bp.GetNumLocations(), 1)
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        # The shared symbol re-resolves, and nothing still points into the
+        # module that was removed.
+        self.assertGreaterEqual(common_bp.GetNumLocations(), 1)
+        for i in range(common_bp.GetNumLocations()):
+            module = common_bp.GetLocationAtIndex(i).GetAddress().GetModule()
+            self.assertNotEqual(
+                module.GetUUIDString(),
+                old_uuid,
+                "no location still resolves into the replaced module",
+            )
+
+        # The symbol that only existed in the old variant goes pending.
+        self.assertEqual(only_v1_bp.GetNumLocations(), 0)
+
+    @skipIfWindows
+    @skipIfRemote
+    @skipUnlessPlatform(["linux"])
+    def test_thread_local_storage_still_resolves(self):
+        """The dynamic loader's per module state follows the replacement.
+
+        The loader keys the link map address it needs for TLS lookups off the
+        module itself, so without help the replacement has no link map and 
every
+        thread local in it reads back as "no TLS data currently exists"."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+        target.BreakpointCreateBySourceRegex(
+            "break after dlopen", lldb.SBFileSpec("main.cpp")
+        )
+
+        launch_info = target.GetLaunchInfo()
+        launch_info.SetArguments([v1], True)
+        error = lldb.SBError()
+        process = target.Launch(launch_info, error)
+        self.assertSuccess(error, "the process launched")
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        # Sanity check that TLS resolves at all before the replace, so that a
+        # failure below is attributable to the replace and not to the platform.
+        before = target.EvaluateExpression("tls_var")
+        self.assertSuccess(before.GetError(), "TLS resolves before the 
replace")
+        self.assertEqual(before.GetValueAsSigned(), 701)
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        after = target.EvaluateExpression("tls_var")
+        self.assertSuccess(after.GetError(), "TLS still resolves after the 
replace")
+        # The variable is read out of the live process, whose mapped pages are
+        # still the old library's, so the value is v1's. What matters is that 
the
+        # lookup resolves at all instead of failing to find a link map.
+        self.assertNotEqual(after.GetValueAsSigned(), 0)
diff --git a/lldb/test/API/commands/target/modules/replace/hidden/v.cpp 
b/lldb/test/API/commands/target/modules/replace/hidden/v.cpp
new file mode 100644
index 0000000000000..506da883ad35e
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/hidden/v.cpp
@@ -0,0 +1,11 @@
+// The "v2" variant of the library. Built with the same soname as ../v.cpp so
+// that it can stand in for it, but with different content so that the two are
+// distinguishable by UUID and by the symbols they define.
+
+extern "C" int only_in_v2() { return 202; }
+
+__thread int tls_var = 702;
+
+extern "C" int get_tls_var() { return tls_var; }
+
+extern "C" int common_func() { return 2; }
diff --git a/lldb/test/API/commands/target/modules/replace/main.cpp 
b/lldb/test/API/commands/target/modules/replace/main.cpp
new file mode 100644
index 0000000000000..be04550b2f60c
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/main.cpp
@@ -0,0 +1,24 @@
+#include <cstdio>
+#include <dlfcn.h>
+
+int main(int argc, char **argv) {
+  if (argc < 2)
+    return 1;
+
+  void *handle = dlopen(argv[1], RTLD_NOW);
+  if (!handle)
+    return 2;
+
+  int (*common_func)() = (int (*)())dlsym(handle, "common_func");
+  int (*get_tls_var)() = (int (*)())dlsym(handle, "get_tls_var");
+  if (!common_func || !get_tls_var)
+    return 3;
+
+  // Call through to the library's thread-local before stopping, so that its
+  // TLS block has actually been allocated for this thread by the time the
+  // test looks at it.
+  int tls = get_tls_var();
+
+  printf("%d %d\n", common_func(), tls); // break after dlopen
+  return 0;
+}
diff --git a/lldb/test/API/commands/target/modules/replace/other_main.cpp 
b/lldb/test/API/commands/target/modules/replace/other_main.cpp
new file mode 100644
index 0000000000000..ff7eaf19686b1
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/other_main.cpp
@@ -0,0 +1,4 @@
+// A second executable, used to check that replacing the executable module
+// keeps it at the front of the target's module list.
+
+int main() { return 0; }
diff --git a/lldb/test/API/commands/target/modules/replace/v.cpp 
b/lldb/test/API/commands/target/modules/replace/v.cpp
new file mode 100644
index 0000000000000..b009c7edc167a
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/v.cpp
@@ -0,0 +1,10 @@
+// The "v1" variant of the library. See hidden/v.cpp for the "v2" variant that
+// it gets replaced with: same soname, different content and different UUID.
+
+extern "C" int only_in_v1() { return 101; }
+
+__thread int tls_var = 701;
+
+extern "C" int get_tls_var() { return tls_var; }
+
+extern "C" int common_func() { return 1; }
diff --git a/lldb/test/API/functionalities/completion/TestCompletion.py 
b/lldb/test/API/functionalities/completion/TestCompletion.py
index f4bdc3a894215..8cab49bdcd973 100644
--- a/lldb/test/API/functionalities/completion/TestCompletion.py
+++ b/lldb/test/API/functionalities/completion/TestCompletion.py
@@ -533,6 +533,15 @@ def test_target_modules_load_aout(self):
         self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
         self.complete_from_to("target modules load a.ou", ["a.out"])
 
+    def test_target_modules_replace(self):
+        """Tests that the argument completes against paths on disk."""
+        self.build()
+        self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.complete_from_to(
+            "target modules replace " + self.getBuildArtifact("a.ou"),
+            [self.getBuildArtifact("a.out")],
+        )
+
     def test_target_modules_search_paths_insert(self):
         # Completion won't work without a valid target.
         self.complete_from_to(

>From 7e5c5c62c91a7f7335d723f8dc75d66bc5ecccd4 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Thu, 6 Aug 2026 17:05:08 -0700
Subject: [PATCH 2/6] Fixed Greg's comments

---
 lldb/source/Commands/CommandObjectTarget.cpp  | 35 +++++---
 lldb/source/Commands/Options.td               |  5 +-
 .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp     | 38 ++++++---
 lldb/source/Target/Target.cpp                 | 72 +++++++++++-----
 .../replace/TestTargetModulesReplace.py       | 83 +++++++++++++++++++
 .../modules/replace/placeholder-no-uuid.yaml  | 17 ++++
 .../target/modules/replace/unplaceable.yaml   | 13 +++
 7 files changed, 219 insertions(+), 44 deletions(-)
 create mode 100644 
lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml
 create mode 100644 
lldb/test/API/commands/target/modules/replace/unplaceable.yaml

diff --git a/lldb/source/Commands/CommandObjectTarget.cpp 
b/lldb/source/Commands/CommandObjectTarget.cpp
index 5384897b4f255..14e56998fd407 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -3152,7 +3152,11 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
             "complete contents, usually to resolve a placeholder module from a 
"
             "core file once the real binary has been located. To attach debug "
             "info to a module that is otherwise fine, use 'target symbols "
-            "add'.",
+            "add'.\n"
+            "The module to replace is found by the new file's UUID, or by its "
+            "basename if it has no UUID. Use --old-path when neither picks a "
+            "single module, and --force to replace a module whose UUID does "
+            "not match the new file's.",
             "target modules replace [--old-path <path>] [--force] <path>",
             eCommandRequiresTarget | eCommandTryTargetAPILock |
                 eCommandProcessMustBePaused) {
@@ -3288,21 +3292,25 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
       return;
     }
 
-    // Read the UUID straight from the file rather than from a Module, so the
-    // module the new file should replace can be found before anything is added
-    // to the target.
-    UUID new_uuid;
-    ModuleSpecList file_specs =
+    // Read the file rather than build a Module from it, so the module it
+    // should replace can be found before anything is added to the target.
+    ModuleSpec new_module_spec(new_file_spec);
+    ModuleSpecList new_module_specs =
         ObjectFile::GetModuleSpecifications(new_file_spec, 0, 0);
-    if (file_specs.GetSize() > 0) {
+    if (new_module_specs.GetSize() > 0) {
       ModuleSpec arch_spec;
       arch_spec.GetArchitecture() = target->GetArchitecture();
       ModuleSpec matching_spec;
-      if (file_specs.FindMatchingModuleSpec(arch_spec, matching_spec))
-        new_uuid = matching_spec.GetUUID();
-      else if (file_specs.GetSize() == 1)
-        new_uuid = file_specs.GetModuleSpecRefAtIndex(0).GetUUID();
+      if (!new_module_specs.FindMatchingModuleSpec(arch_spec, matching_spec)) {
+        result.AppendErrorWithFormatv(
+            "'{0}' does not contain the target architecture {1}",
+            new_file_spec.GetPath(),
+            target->GetArchitecture().GetTriple().str());
+        return;
+      }
+      new_module_spec = matching_spec;
     }
+    const UUID &new_uuid = new_module_spec.GetUUID();
 
     ModuleSP old_module_sp =
         FindModuleToReplace(*target, new_file_spec, new_uuid, result);
@@ -3322,10 +3330,13 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
       return;
     }
 
-    ModuleSpec new_module_spec(new_file_spec);
     if (!new_module_spec.GetArchitecture().IsValid())
       new_module_spec.GetArchitecture() = target->GetArchitecture();
 
+    // Look the file up by path alone. Asking for the UUID as well would find
+    // the module already in the target, which is the one being replaced.
+    new_module_spec.GetUUID().Clear();
+
     Status error;
     ModuleSP new_module_sp =
         target->GetOrCreateModule(new_module_spec, /*notify=*/false, &error);
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 3d5a6b19f3e2b..055adf1222e8a 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1824,8 +1824,9 @@ let Command = "target modules replace" in {
   def target_modules_replace_old_path
       : Option<"old-path", "o">,
         Arg<"Path">,
-        Desc<"Path of the module in the target to replace. Only needed when it 
"
-             "cannot be worked out from the new file.">;
+        Desc<"Path of the module in the target to replace. This is only needed 
"
+             "if the new module doesn't have a valid UUID or if the basename "
+             "doesn't uniquely match any existing target modules.">;
   def target_modules_replace_force
       : Option<"force", "f">,
         Desc<"Replace the module even when its UUID does not match the UUID of 
"
diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index f54bf49e5f6a7..6d8c6ba36b8b9 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -213,16 +213,34 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const 
ModuleSP &module_sp) {
 
 Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp,
                                              const ModuleSP &new_module_sp) {
-  // Images are mapped in one piece here, so the target's placement is already
-  // correct. Only the link map address needs moving, without it no thread 
local
-  // in the replacement can be resolved.
-  llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex);
-  auto it = m_loaded_modules.find(old_module_sp);
-  if (it == m_loaded_modules.end())
-    return Status();
-  const addr_t link_map_addr = it->second;
-  m_loaded_modules.erase(it);
-  m_loaded_modules[new_module_sp] = link_map_addr;
+  // Where the old module was mapped, read before its sections go away.
+  addr_t base_addr = LLDB_INVALID_ADDRESS;
+  if (ObjectFile *object_file = old_module_sp->GetObjectFile()) {
+    Address base = object_file->GetBaseAddress();
+    if (base.IsValid())
+      base_addr = base.GetLoadAddress(&m_process->GetTarget());
+  }
+  if (base_addr == LLDB_INVALID_ADDRESS)
+    return Status::FromErrorStringWithFormatv(
+        "'{0}' is not loaded at a known address", 
old_module_sp->GetFileSpec());
+
+  addr_t link_map_addr = LLDB_INVALID_ADDRESS;
+  {
+    // The link map address is what thread local lookups are found through, and
+    // it is keyed by module, so it has to be moved onto the replacement.
+    llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex);
+    auto it = m_loaded_modules.find(old_module_sp);
+    if (it != m_loaded_modules.end()) {
+      link_map_addr = it->second;
+      m_loaded_modules.erase(it);
+    }
+  }
+
+  UnloadSections(old_module_sp);
+  // Images are mapped in one piece here, so the recorded address is where the
+  // replacement goes, not an offset to slide it by.
+  UpdateLoadedSections(new_module_sp, link_map_addr, base_addr,
+                       /*base_addr_is_offset=*/false);
   return Status();
 }
 
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index b29af9775b87e..557d6934c54af 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -2052,10 +2052,6 @@ Status Target::ReplaceModule(ModuleSP old_module_sp,
   if (m_images.GetIndexForModule(old_module_sp.get()) != LLDB_INVALID_INDEX32)
     m_images.Remove(old_module_sp, /*notify=*/false);
 
-  // Unloads its sections and deletes its breakpoint locations. Must be
-  // explicit, no notification path passes delete_locations=true.
-  ModulesDidUnload(unloaded_modules, /*delete_locations=*/true);
-
   // Target::GetOrCreateModule() adds the module it creates, so the replacement
   // is usually in the target already.
   if (m_images.GetIndexForModule(new_module_sp.get()) == LLDB_INVALID_INDEX32)
@@ -2072,35 +2068,71 @@ Status Target::ReplaceModule(ModuleSP old_module_sp,
     }
   }
 
-  // Generic placement, correct whenever a module is mapped in one piece. The
-  // dynamic loader corrects it below on platforms where it is not.
-  if (base_load_addr != LLDB_INVALID_ADDRESS) {
-    bool changed = false;
-    new_module_sp->SetLoadAddress(*this, base_load_addr,
-                                  /*value_is_offset=*/false, changed);
+  // The dynamic loader is the only thing that knows how this platform maps an
+  // image, so it moves the sections over and carries across anything it tracks
+  // per module, such as the link map address thread locals are found through.
+  // It runs before ModulesDidUnload() below, which would otherwise take the 
old
+  // module's load addresses away before the loader could read them.
+  Status error;
+  DynamicLoader *dyld =
+      m_process_sp ? m_process_sp->GetDynamicLoader() : nullptr;
+  if (dyld) {
+    error = dyld->ReplaceModule(old_module_sp, new_module_sp);
+  } else {
+    // Targets with no dynamic loader, a static target or most minidumps, still
+    // need the old sections taken away and the replacement put where they 
were.
+    UnloadModuleSections(old_module_sp);
+    if (base_load_addr != LLDB_INVALID_ADDRESS) {
+      // Module::SetLoadAddress() only reports whether there was an object file
+      // to ask, so \a loaded is what says any section was placed.
+      bool loaded = false;
+      new_module_sp->SetLoadAddress(*this, base_load_addr,
+                                    /*value_is_offset=*/false, loaded);
+      if (!loaded)
+        error = Status::FromErrorStringWithFormatv(
+            "'{0}' could not be loaded at {1:x}, where '{2}' was",
+            new_module_sp->GetFileSpec(), base_load_addr,
+            old_module_sp->GetFileSpec());
+    }
   }
 
-  // Let the dynamic loader redo the load addresses if this platform needs it,
-  // and move over anything it tracks per module, such as the link map address
-  // thread locals are found through.
-  Status error;
-  if (m_process_sp) {
-    if (DynamicLoader *dyld = m_process_sp->GetDynamicLoader())
-      error = dyld->ReplaceModule(old_module_sp, new_module_sp);
+  if (error.Fail()) {
+    // Placement got part way through at most, so leave neither module loaded.
+    // The old module goes back into the target, unloaded, so that the caller 
is
+    // not left short of a module it never asked to lose.
+    UnloadModuleSections(new_module_sp);
+    UnloadModuleSections(old_module_sp);
+    m_images.Remove(new_module_sp, /*notify=*/false);
+    if (m_images.GetIndexForModule(old_module_sp.get()) == 
LLDB_INVALID_INDEX32)
+      m_images.Append(old_module_sp, /*notify=*/false);
+    ModulesDidUnload(unloaded_modules, /*delete_locations=*/true);
+    return error;
   }
 
+  // Deletes the breakpoint locations that resolved into the old module. Must 
be
+  // explicit, no notification path passes delete_locations=true. Its section
+  // unload is a no-op by now.
+  ModulesDidUnload(unloaded_modules, /*delete_locations=*/true);
+
   // Sections must be in place first, resolving breakpoints into a module whose
   // sections are not loaded yields locations with no address.
   ModuleList added_modules;
   added_modules.Append(new_module_sp, /*notify=*/false);
   ModulesDidLoad(added_modules);
 
-  // Drop the old module from the shared module cache so a later lookup of the
-  // same path cannot resurrect it.
+  // A placeholder or a module read out of memory stands for a file we could 
not
+  // use, so drop it from the shared module cache to stop a later lookup of the
+  // same path resurrecting it. Real files are left cached.
+  ObjectFile *old_object_file = old_module_sp->GetObjectFile();
+  const bool evict_from_cache =
+      old_object_file == nullptr ||
+      old_object_file->GetPluginName() == "placeholder" ||
+      old_object_file->GetPluginName() == "memory";
   unloaded_modules.Clear();
   std::weak_ptr<Module> old_module_wp(old_module_sp->weak_from_this());
   old_module_sp.reset();
-  ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp);
+  if (evict_from_cache)
+    ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp);
 
   // Cached stack frames and register contexts can still hold the old module.
   if (m_process_sp)
diff --git 
a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py 
b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
index 6b1f657b04387..e0f9da8596766 100644
--- a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
+++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
@@ -232,6 +232,89 @@ def test_errors(self):
         self.assertEqual(target.GetNumModules(), num_modules)
         self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
 
+    @skipUnlessPlatform(["linux"])
+    def test_placeholder_without_uuid(self):
+        """A placeholder with no UUID needs no --force, nothing can be 
compared."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+        core = self.getBuildArtifact("no-uuid.dmp")
+        self.yaml2obj("placeholder-no-uuid.yaml", core)
+
+        target = self.dbg.CreateTarget("")
+        self.assertTrue(target.LoadCore(core).IsValid())
+        placeholder = target.FindModule(lldb.SBFileSpec("/no/such/module.so"))
+        self.assertTrue(placeholder.IsValid())
+        self.assertFalse(placeholder.GetUUIDString(), "the placeholder has no 
UUID")
+        base = self.base_load_address(placeholder, target)
+        self.assertNotEqual(base, lldb.LLDB_INVALID_ADDRESS)
+
+        self.runCmd("target modules replace --old-path /no/such/module.so 
'%s'" % v1)
+
+        new_module = target.FindModule(lldb.SBFileSpec(v1))
+        self.assertTrue(new_module.IsValid())
+        self.assertEqual(self.base_load_address(new_module, target), base)
+        self.assertTrue(new_module.FindSymbol("only_in_v1").IsValid())
+
+    @skipUnlessPlatform(["linux"])
+    def test_failure_leaves_the_target_alone(self):
+        """A replacement that cannot be placed is refused, and nothing is 
lost."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+        core = self.getBuildArtifact("no-uuid.dmp")
+        self.yaml2obj("placeholder-no-uuid.yaml", core)
+
+        target = self.dbg.CreateTarget("")
+        self.assertTrue(target.LoadCore(core).IsValid())
+        num_modules = target.GetNumModules()
+
+        # An object file with no loadable segments cannot go where the
+        # placeholder was.
+        unplaceable = self.getBuildArtifact("unplaceable.o")
+        self.yaml2obj("unplaceable.yaml", unplaceable)
+        self.expect(
+            "target modules replace --old-path /no/such/module.so --force '%s'"
+            % unplaceable,
+            error=True,
+            substrs=["could not be loaded at"],
+        )
+
+        self.assertEqual(target.GetNumModules(), num_modules)
+        self.assertTrue(
+            target.FindModule(lldb.SBFileSpec("/no/such/module.so")).IsValid(),
+            "the module that could not be replaced is still in the target",
+        )
+
+    @skipIfWindows
+    @skipIfRemote
+    @skipUnlessPlatform(["linux"])
+    def test_core_file(self):
+        """Replacing a module in a core file keeps it at the same address."""
+        v1, v2, v1_copy = self.build_and_get_paths()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        target.BreakpointCreateBySourceRegex(
+            "break after dlopen", lldb.SBFileSpec("main.cpp")
+        )
+        launch_info = target.GetLaunchInfo()
+        launch_info.SetArguments([v1], True)
+        error = lldb.SBError()
+        process = target.Launch(launch_info, error)
+        self.assertSuccess(error)
+
+        core = self.getBuildArtifact("saved.core")
+        self.runCmd("process save-core --style=full '%s'" % core)
+        process.Kill()
+
+        target = self.dbg.CreateTarget("")
+        self.assertTrue(target.LoadCore(core).IsValid())
+        old_module = target.FindModule(lldb.SBFileSpec(v1))
+        self.assertTrue(old_module.IsValid())
+        base = self.base_load_address(old_module, target)
+
+        self.runCmd("target modules replace --force '%s'" % v2)
+
+        new_module = target.FindModule(lldb.SBFileSpec(v2))
+        self.assertTrue(new_module.IsValid())
+        self.assertEqual(self.base_load_address(new_module, target), base)
+        self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid())
+
     @skipIfWindows
     @skipIfRemote
     def test_breakpoints_move_to_the_replacement(self):
diff --git 
a/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml 
b/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml
new file mode 100644
index 0000000000000..7eab2c37c0274
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml
@@ -0,0 +1,17 @@
+--- !minidump
+Streams:
+  - Type:            SystemInfo
+    Processor Arch:  AMD64
+    Platform ID:     Linux
+    CSD Version:     'Linux'
+    CPU:
+      Vendor ID:       GenuineIntel
+      Version Info:    0x00000000
+      Feature Info:    0x00000000
+  - Type:            ModuleList
+    Modules:
+      - Base of Image:   0x0000000000400000
+        Size of Image:   0x00002000
+        Module Name:     '/no/such/module.so'
+        CodeView Record: 4C45704200000000000000000000000000000000
+...
diff --git a/lldb/test/API/commands/target/modules/replace/unplaceable.yaml 
b/lldb/test/API/commands/target/modules/replace/unplaceable.yaml
new file mode 100644
index 0000000000000..e730082e5f593
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/unplaceable.yaml
@@ -0,0 +1,13 @@
+--- !ELF
+FileHeader:
+  Class:           ELFCLASS64
+  Data:            ELFDATA2LSB
+  Type:            ET_DYN
+  Machine:         EM_X86_64
+Sections:
+  - Name:            .text
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_EXECINSTR ]
+    Address:         0x0000000000000200
+    AddressAlign:    0x0000000000000004
+    Content:         'C3'

>From 2a4c39e18c9ab71bb81b74bcfe946c5114558c67 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Thu, 6 Aug 2026 17:18:14 -0700
Subject: [PATCH 3/6] small fixes

---
 lldb/include/lldb/Target/DynamicLoader.h     | 11 +++++++-
 lldb/source/Commands/CommandObjectTarget.cpp | 29 ++++++++++----------
 2 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/lldb/include/lldb/Target/DynamicLoader.h 
b/lldb/include/lldb/Target/DynamicLoader.h
index 779ff2d3a70be..214a54fd03fc3 100644
--- a/lldb/include/lldb/Target/DynamicLoader.h
+++ b/lldb/include/lldb/Target/DynamicLoader.h
@@ -210,6 +210,12 @@ class DynamicLoader : public PluginInterface {
   /// Inform the dynamic loader that Target::ReplaceModule() has swapped one
   /// module for another.
   ///
+  /// If this function returns a Status that is a success, the dynamic loader
+  /// will have removed all old section mappings for \a old_module_sp, and
+  /// loaded all sections for \a new_module_sp. If an error is returned from
+  /// this function, the target will unload all sections from \a old_module_sp
+  /// and not do anything to load the \a new_module_sp's sections.
+  ///
   /// \param[in] old_module_sp
   ///     The module that was removed from the target.
   ///
@@ -220,7 +226,10 @@ class DynamicLoader : public PluginInterface {
   ///     An error if this loader cannot place the replacement correctly.
   virtual Status ReplaceModule(const lldb::ModuleSP &old_module_sp,
                                const lldb::ModuleSP &new_module_sp) {
-    return Status();
+    return Status::FromErrorStringWithFormatv(
+        "The {0} dynamic loader plug-in doesn't support replacing a module. "
+        "You can try starting your debug session again to use the new module.",
+        GetPluginName());
   }
 
   /// Locates or creates a module given by \p file and updates/loads the
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp 
b/lldb/source/Commands/CommandObjectTarget.cpp
index 14e56998fd407..e6580acf762f1 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -3209,21 +3209,22 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
   // Find the module the new file is meant to stand in for. Preferred order is
   // the path the user gave, then the UUID read out of the new file, then its
   // basename.
-  ModuleSP FindModuleToReplace(Target &target, const FileSpec &new_file_spec,
-                               const UUID &new_uuid,
+  ModuleSP FindModuleToReplace(Target &target,
+                               const ModuleSpec &new_module_spec,
                                CommandReturnObject &result) {
     ModuleSpec search_spec;
     llvm::StringRef description;
 
     if (!m_options.m_old_path.empty()) {
       search_spec.GetFileSpec().SetPath(m_options.m_old_path);
-      description = "the given path";
-    } else if (new_uuid.IsValid()) {
-      search_spec.GetUUID() = new_uuid;
+      description = "the specified path";
+    } else if (new_module_spec.GetUUID().IsValid()) {
+      search_spec.GetUUID() = new_module_spec.GetUUID();
       description = "a matching UUID";
     } else {
-      search_spec.GetFileSpec().SetFilename(new_file_spec.GetFilename());
-      description = "a matching name";
+      search_spec.GetFileSpec().SetFilename(
+          new_module_spec.GetFileSpec().GetFilename());
+      description = "a matching file basename";
     }
 
     ModuleList matches;
@@ -3233,12 +3234,13 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
       // A file with a UUID that names nothing in the target is still worth
       // trying by name, the placeholder it should replace may have been built
       // without one.
-      if (m_options.m_old_path.empty() && new_uuid.IsValid()) {
+      if (m_options.m_old_path.empty() && new_module_spec.GetUUID().IsValid()) 
{
         ModuleSpec by_name;
-        by_name.GetFileSpec().SetFilename(new_file_spec.GetFilename());
+        by_name.GetFileSpec().SetFilename(
+            new_module_spec.GetFileSpec().GetFilename());
         target.GetImages().FindModules(by_name, matches);
-        description =
-            matches.IsEmpty() ? "a matching UUID or name" : "a matching name";
+        description = matches.IsEmpty() ? "a matching UUID or file basename"
+                                        : "a matching file basename";
       }
       if (matches.IsEmpty()) {
         result.AppendErrorWithFormatv(
@@ -3310,16 +3312,15 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
       }
       new_module_spec = matching_spec;
     }
-    const UUID &new_uuid = new_module_spec.GetUUID();
-
     ModuleSP old_module_sp =
-        FindModuleToReplace(*target, new_file_spec, new_uuid, result);
+        FindModuleToReplace(*target, new_module_spec, result);
     if (!old_module_sp)
       return;
 
     // Different UUIDs mean the new file is not the binary that ran, so the
     // symbols would not describe the memory the target has.
     const UUID &old_uuid = old_module_sp->GetUUID();
+    const UUID &new_uuid = new_module_spec.GetUUID();
     if (!m_options.m_force && old_uuid.IsValid() && new_uuid.IsValid() &&
         old_uuid != new_uuid) {
       result.AppendErrorWithFormatv(

>From 31091f7b700494b818f7c9d82e2b527127ea115e Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Fri, 7 Aug 2026 10:29:00 -0700
Subject: [PATCH 4/6] fix tests

---
 .../replace/TestTargetModulesReplace.py       | 23 +++++++++++--------
 .../target/modules/replace/replacement.yaml   | 20 ++++++++++++++++
 2 files changed, 33 insertions(+), 10 deletions(-)
 create mode 100644 
lldb/test/API/commands/target/modules/replace/replacement.yaml

diff --git 
a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py 
b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
index e0f9da8596766..e3c2aa93e9bdf 100644
--- a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
+++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py
@@ -11,6 +11,7 @@
 from lldbsuite.test.lldbtest import *
 
 
+@skipIfWindows
 class TargetModulesReplaceTestCase(TestBase):
     NO_DEBUG_INFO_TESTCASE = True
 
@@ -232,12 +233,14 @@ def test_errors(self):
         self.assertEqual(target.GetNumModules(), num_modules)
         self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid())
 
-    @skipUnlessPlatform(["linux"])
     def test_placeholder_without_uuid(self):
         """A placeholder with no UUID needs no --force, nothing can be 
compared."""
-        v1, v2, v1_copy = self.build_and_get_paths()
         core = self.getBuildArtifact("no-uuid.dmp")
         self.yaml2obj("placeholder-no-uuid.yaml", core)
+        # The dump is x86_64, so the replacement comes from a yaml too rather
+        # than from this test's libraries, which follow the host architecture.
+        replacement = self.getBuildArtifact("replacement.so")
+        self.yaml2obj("replacement.yaml", replacement)
 
         target = self.dbg.CreateTarget("")
         self.assertTrue(target.LoadCore(core).IsValid())
@@ -247,17 +250,19 @@ def test_placeholder_without_uuid(self):
         base = self.base_load_address(placeholder, target)
         self.assertNotEqual(base, lldb.LLDB_INVALID_ADDRESS)
 
-        self.runCmd("target modules replace --old-path /no/such/module.so 
'%s'" % v1)
+        self.runCmd(
+            "target modules replace --old-path /no/such/module.so '%s'" % 
replacement
+        )
 
-        new_module = target.FindModule(lldb.SBFileSpec(v1))
+        new_module = target.FindModule(lldb.SBFileSpec(replacement))
         self.assertTrue(new_module.IsValid())
         self.assertEqual(self.base_load_address(new_module, target), base)
-        self.assertTrue(new_module.FindSymbol("only_in_v1").IsValid())
+        self.assertFalse(
+            target.FindModule(lldb.SBFileSpec("/no/such/module.so")).IsValid()
+        )
 
-    @skipUnlessPlatform(["linux"])
     def test_failure_leaves_the_target_alone(self):
         """A replacement that cannot be placed is refused, and nothing is 
lost."""
-        v1, v2, v1_copy = self.build_and_get_paths()
         core = self.getBuildArtifact("no-uuid.dmp")
         self.yaml2obj("placeholder-no-uuid.yaml", core)
 
@@ -282,7 +287,6 @@ def test_failure_leaves_the_target_alone(self):
             "the module that could not be replaced is still in the target",
         )
 
-    @skipIfWindows
     @skipIfRemote
     @skipUnlessPlatform(["linux"])
     def test_core_file(self):
@@ -315,7 +319,6 @@ def test_core_file(self):
         self.assertEqual(self.base_load_address(new_module, target), base)
         self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid())
 
-    @skipIfWindows
     @skipIfRemote
     def test_breakpoints_move_to_the_replacement(self):
         """Breakpoint locations are re-resolved into the new module."""
@@ -361,9 +364,9 @@ def test_breakpoints_move_to_the_replacement(self):
         # The symbol that only existed in the old variant goes pending.
         self.assertEqual(only_v1_bp.GetNumLocations(), 0)
 
-    @skipIfWindows
     @skipIfRemote
     @skipUnlessPlatform(["linux"])
+    @skipIf(archs=no_match(["x86_64"]))
     def test_thread_local_storage_still_resolves(self):
         """The dynamic loader's per module state follows the replacement.
 
diff --git a/lldb/test/API/commands/target/modules/replace/replacement.yaml 
b/lldb/test/API/commands/target/modules/replace/replacement.yaml
new file mode 100644
index 0000000000000..a2273bef7e4c7
--- /dev/null
+++ b/lldb/test/API/commands/target/modules/replace/replacement.yaml
@@ -0,0 +1,20 @@
+--- !ELF
+FileHeader:
+  Class:           ELFCLASS64
+  Data:            ELFDATA2LSB
+  Type:            ET_DYN
+  Machine:         EM_X86_64
+Sections:
+  - Name:            .text
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_EXECINSTR ]
+    Address:         0x0000000000000200
+    AddressAlign:    0x0000000000000004
+    Content:         'C3'
+ProgramHeaders:
+  - Type:            PT_LOAD
+    Flags:           [ PF_X, PF_R ]
+    VAddr:           0x0000000000000000
+    Align:           0x1000
+    FirstSec:        .text
+    LastSec:         .text

>From 5ee16703bd2f265aac6c8a0a2fa6278b86a7d3a6 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Fri, 7 Aug 2026 17:07:47 -0700
Subject: [PATCH 5/6] Addressed comments

---
 lldb/source/Commands/CommandObjectTarget.cpp  |  1 +
 .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp     | 60 +++++++++----------
 .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.h       | 18 ++++--
 3 files changed, 45 insertions(+), 34 deletions(-)

diff --git a/lldb/source/Commands/CommandObjectTarget.cpp 
b/lldb/source/Commands/CommandObjectTarget.cpp
index e6580acf762f1..bd463bc3f4cf9 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -3314,6 +3314,7 @@ class CommandObjectTargetModulesReplace : public 
CommandObjectParsed {
     }
     ModuleSP old_module_sp =
         FindModuleToReplace(*target, new_module_spec, result);
+    // FindModuleToReplace() has already put the error into result.
     if (!old_module_sp)
       return;
 
diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index 6d8c6ba36b8b9..58597638d3507 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -192,9 +192,11 @@ void DynamicLoaderPOSIXDYLD::DidLaunch() {
 Status DynamicLoaderPOSIXDYLD::CanLoadImage() { return Status(); }
 
 void DynamicLoaderPOSIXDYLD::SetLoadedModule(const ModuleSP &module_sp,
-                                             addr_t link_map_addr) {
+                                             addr_t link_map_addr,
+                                             addr_t base_addr,
+                                             bool base_addr_is_offset) {
   llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex);
-  m_loaded_modules[module_sp] = link_map_addr;
+  m_loaded_modules[module_sp] = {link_map_addr, base_addr, 
base_addr_is_offset};
 }
 
 void DynamicLoaderPOSIXDYLD::UnloadModule(const ModuleSP &module_sp) {
@@ -202,8 +204,8 @@ void DynamicLoaderPOSIXDYLD::UnloadModule(const ModuleSP 
&module_sp) {
   m_loaded_modules.erase(module_sp);
 }
 
-std::optional<lldb::addr_t>
-DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const ModuleSP &module_sp) {
+std::optional<DynamicLoaderPOSIXDYLD::LoadedModuleInfo>
+DynamicLoaderPOSIXDYLD::GetLoadedModuleInfo(const ModuleSP &module_sp) {
   llvm::sys::ScopedReader lock(m_loaded_modules_rw_mutex);
   auto it = m_loaded_modules.find(module_sp);
   if (it != m_loaded_modules.end())
@@ -213,34 +215,32 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const 
ModuleSP &module_sp) {
 
 Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp,
                                              const ModuleSP &new_module_sp) {
-  // Where the old module was mapped, read before its sections go away.
-  addr_t base_addr = LLDB_INVALID_ADDRESS;
-  if (ObjectFile *object_file = old_module_sp->GetObjectFile()) {
+  // Load the replacement the same way the old module was loaded, which also
+  // carries the link map address across so its thread locals resolve.
+  LoadedModuleInfo info;
+  if (std::optional<LoadedModuleInfo> loaded =
+          GetLoadedModuleInfo(old_module_sp);
+      loaded && loaded->base_addr != LLDB_INVALID_ADDRESS) {
+    info = *loaded;
+  } else if (ObjectFile *object_file = old_module_sp->GetObjectFile()) {
+    // Post mortem processes build their module list from the core file rather
+    // than by walking the rendezvous, so there is nothing recorded here for
+    // them. Fall back to where the module says it is.
     Address base = object_file->GetBaseAddress();
     if (base.IsValid())
-      base_addr = base.GetLoadAddress(&m_process->GetTarget());
+      info.base_addr = base.GetLoadAddress(&m_process->GetTarget());
+    if (loaded)
+      info.link_map_addr = loaded->link_map_addr;
   }
-  if (base_addr == LLDB_INVALID_ADDRESS)
+
+  if (info.base_addr == LLDB_INVALID_ADDRESS)
     return Status::FromErrorStringWithFormatv(
         "'{0}' is not loaded at a known address", 
old_module_sp->GetFileSpec());
 
-  addr_t link_map_addr = LLDB_INVALID_ADDRESS;
-  {
-    // The link map address is what thread local lookups are found through, and
-    // it is keyed by module, so it has to be moved onto the replacement.
-    llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex);
-    auto it = m_loaded_modules.find(old_module_sp);
-    if (it != m_loaded_modules.end()) {
-      link_map_addr = it->second;
-      m_loaded_modules.erase(it);
-    }
-  }
-
-  UnloadSections(old_module_sp);
-  // Images are mapped in one piece here, so the recorded address is where the
-  // replacement goes, not an offset to slide it by.
-  UpdateLoadedSections(new_module_sp, link_map_addr, base_addr,
-                       /*base_addr_is_offset=*/false);
+  UnloadModule(old_module_sp);
+  UnloadSectionsCommon(old_module_sp);
+  UpdateLoadedSections(new_module_sp, info.link_map_addr, info.base_addr,
+                       info.base_addr_is_offset);
   return Status();
 }
 
@@ -248,7 +248,7 @@ void DynamicLoaderPOSIXDYLD::UpdateLoadedSections(ModuleSP 
module,
                                                   addr_t link_map_addr,
                                                   addr_t base_addr,
                                                   bool base_addr_is_offset) {
-  SetLoadedModule(module, link_map_addr);
+  SetLoadedModule(module, link_map_addr, base_addr, base_addr_is_offset);
 
   UpdateLoadedSectionsCommon(module, base_addr, base_addr_is_offset);
 }
@@ -874,8 +874,8 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const 
lldb::ModuleSP module_sp,
                                            const lldb::ThreadSP thread,
                                            lldb::addr_t tls_file_addr) {
   Log *log = GetLog(LLDBLog::DynamicLoader);
-  std::optional<addr_t> link_map_addr_opt = GetLoadedModuleLinkAddr(module_sp);
-  if (!link_map_addr_opt.has_value()) {
+  std::optional<LoadedModuleInfo> info = GetLoadedModuleInfo(module_sp);
+  if (!info.has_value()) {
     LLDB_LOG(
         log,
         "GetThreadLocalData error: module({0}) not found in loaded modules",
@@ -883,7 +883,7 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const 
lldb::ModuleSP module_sp,
     return LLDB_INVALID_ADDRESS;
   }
 
-  addr_t link_map = link_map_addr_opt.value();
+  addr_t link_map = info->link_map_addr;
   if (link_map == LLDB_INVALID_ADDRESS || link_map == 0) {
     LLDB_LOGF(log,
               "GetThreadLocalData error: invalid link map address=0x%" PRIx64,
diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
index 53f68975e3efc..559a551cd5f73 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h
@@ -181,18 +181,28 @@ class DynamicLoaderPOSIXDYLD : public 
lldb_private::DynamicLoader {
   const DynamicLoaderPOSIXDYLD &
   operator=(const DynamicLoaderPOSIXDYLD &) = delete;
 
+  /// What each module was loaded with, enough to load another module the same
+  /// way.
+  struct LoadedModuleInfo {
+    lldb::addr_t link_map_addr = LLDB_INVALID_ADDRESS;
+    lldb::addr_t base_addr = LLDB_INVALID_ADDRESS;
+    bool base_addr_is_offset = false;
+  };
+
   /// Loaded module list. (link map for each module)
   /// This may be accessed in a multi-threaded context. Use the accessor 
methods
   /// to access `m_loaded_modules` safely.
-  std::map<lldb::ModuleWP, lldb::addr_t, std::owner_less<lldb::ModuleWP>>
+  std::map<lldb::ModuleWP, LoadedModuleInfo, std::owner_less<lldb::ModuleWP>>
       m_loaded_modules;
   llvm::sys::RWMutex m_loaded_modules_rw_mutex;
 
   void SetLoadedModule(const lldb::ModuleSP &module_sp,
-                       lldb::addr_t link_map_addr);
+                       lldb::addr_t link_map_addr,
+                       lldb::addr_t base_addr = LLDB_INVALID_ADDRESS,
+                       bool base_addr_is_offset = false);
   void UnloadModule(const lldb::ModuleSP &module_sp);
-  std::optional<lldb::addr_t>
-  GetLoadedModuleLinkAddr(const lldb::ModuleSP &module_sp);
+  std::optional<LoadedModuleInfo>
+  GetLoadedModuleInfo(const lldb::ModuleSP &module_sp);
 };
 
 #endif // LLDB_SOURCE_PLUGINS_DYNAMICLOADER_POSIX_DYLD_DYNAMICLOADERPOSIXDYLD_H

>From cf301c1774a66e182f606d2e69ba24ebbde981c2 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Mon, 10 Aug 2026 16:36:31 -0700
Subject: [PATCH 6/6] combined the functions

---
 .../DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp        | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git 
a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp 
b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index 58597638d3507..9bc40aed2bd17 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -237,8 +237,7 @@ Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP 
&old_module_sp,
     return Status::FromErrorStringWithFormatv(
         "'{0}' is not loaded at a known address", 
old_module_sp->GetFileSpec());
 
-  UnloadModule(old_module_sp);
-  UnloadSectionsCommon(old_module_sp);
+  UnloadSections(old_module_sp);
   UpdateLoadedSections(new_module_sp, info.link_map_addr, info.base_addr,
                        info.base_addr_is_offset);
   return Status();

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to