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

Adds a command that replaces a module in the target with a different file on 
disk:
```
target modules replace [--old-path <path>] [--force] <path>
```

The usual case is a core file whose binary could not be found, leaving a 
placeholder module with no real contents. Once the file is located this swaps 
it in at the address the placeholder had, so backtraces symbolicate and 
breakpoints resolve into it. 

The module to replace is worked out from the new file's UUID, or from its name 
when it has no UUID, and `--old-path` names it explicitly when neither is 
enough. 

A UUID mismatch is an error unless we add `--force`.

It removes the old module through the regular unload path, which unloads its 
sections and deletes the breakpoint locations, then it adds the new module at 
the same load address, and notifies the dynamic loader.

The old module is destroyed rather than having its object file swapped out 
underneath it. Replacing the object file of a live Module leaves stack frames, 
symbol blocks and breakpoint locations pointing into freed memory.

>From 743db310fb679864883ca524a550fd6c2eb7cd0e Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <[email protected]>
Date: Thu, 6 Aug 2026 13:44:31 -0700
Subject: [PATCH] [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 62307a9f9c2a2..2d082c4b236e9 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 b83d67bbf045e..0a40583808f3e 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(

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

Reply via email to