[Lldb-commits] [lldb] [lldb] Add target modules replace command (PR #214576)
Bar Soloveychik via lldb-commits
lldb-commits at lists.llvm.org
Mon Aug 10 17:48:24 PDT 2026
https://github.com/barsolo2000 updated https://github.com/llvm/llvm-project/pull/214576
>From 743db310fb679864883ca524a550fd6c2eb7cd0e Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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 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(
>From 65109f50618dc680b102bc72931f69f818219122 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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 0a40583808f3e..7b647bab2a37e 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 c2cdd987f241a6009346e687befa93d032a84619 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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 2d082c4b236e9..7622fb0bb57ab 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 380e45b002fccb60a63eb8dd088a4ab90d51e270 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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 *
+ at 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 21a167f5d3364e81c81c059d2b09edfde0d75252 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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 879f4cd5216068015315c492a2fcb259d0cb49f0 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
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();
More information about the lldb-commits
mailing list