https://github.com/barsolo2000 updated https://github.com/llvm/llvm-project/pull/214576
>From c4120d1d1e79b380cb225ab524445b67a1dd28c5 Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Thu, 6 Aug 2026 13:44:31 -0700 Subject: [PATCH 1/6] [lldb] Add target modules replace command --- lldb/include/lldb/Target/DynamicLoader.h | 16 + lldb/include/lldb/Target/Target.h | 23 ++ lldb/source/Commands/CommandObjectTarget.cpp | 220 ++++++++++++ lldb/source/Commands/Options.td | 12 + .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp | 15 + .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.h | 4 + lldb/source/Target/Target.cpp | 86 +++++ .../commands/target/modules/replace/Makefile | 18 + .../replace/TestTargetModulesReplace.py | 318 ++++++++++++++++++ .../target/modules/replace/hidden/v.cpp | 11 + .../commands/target/modules/replace/main.cpp | 24 ++ .../target/modules/replace/other_main.cpp | 4 + .../API/commands/target/modules/replace/v.cpp | 10 + .../completion/TestCompletion.py | 9 + 14 files changed, 770 insertions(+) create mode 100644 lldb/test/API/commands/target/modules/replace/Makefile create mode 100644 lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py create mode 100644 lldb/test/API/commands/target/modules/replace/hidden/v.cpp create mode 100644 lldb/test/API/commands/target/modules/replace/main.cpp create mode 100644 lldb/test/API/commands/target/modules/replace/other_main.cpp create mode 100644 lldb/test/API/commands/target/modules/replace/v.cpp diff --git a/lldb/include/lldb/Target/DynamicLoader.h b/lldb/include/lldb/Target/DynamicLoader.h index 997826af2bed4..779ff2d3a70be 100644 --- a/lldb/include/lldb/Target/DynamicLoader.h +++ b/lldb/include/lldb/Target/DynamicLoader.h @@ -207,6 +207,22 @@ class DynamicLoader : public PluginInterface { return LLDB_INVALID_ADDRESS; } + /// Inform the dynamic loader that Target::ReplaceModule() has swapped one + /// module for another. + /// + /// \param[in] old_module_sp + /// The module that was removed from the target. + /// + /// \param[in] new_module_sp + /// The module that took its place. + /// + /// \return + /// An error if this loader cannot place the replacement correctly. + virtual Status ReplaceModule(const lldb::ModuleSP &old_module_sp, + const lldb::ModuleSP &new_module_sp) { + return Status(); + } + /// Locates or creates a module given by \p file and updates/loads the /// resulting module at the virtual base address \p base_addr. /// Note that this calls Target::GetOrCreateModule with notify being false, diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h index 39602421cfd96..cfd80fd9d0ae1 100644 --- a/lldb/include/lldb/Target/Target.h +++ b/lldb/include/lldb/Target/Target.h @@ -1158,6 +1158,29 @@ class Target : public std::enable_shared_from_this<Target>, void ModulesDidUnload(ModuleList &module_list, bool delete_locations); + /// Replace a module in this target with a different one. + /// + /// Removes \a old_module_sp, unloading its sections and deleting the + /// breakpoint locations that resolved into it, then adds \a new_module_sp at + /// the same load address and tells the dynamic loader about the swap. + /// + /// To attach debug info to a module that is otherwise fine, add a symbol file + /// to it instead. + /// + /// \param[in] old_module_sp + /// The module to remove. Passed by value because this drops the last + /// reference the target holds, letting the module be destroyed here once + /// nothing points into it. + /// + /// \param[in] new_module_sp + /// The module to put in its place. It may already have been added to the + /// target, as Target::GetOrCreateModule() does. + /// + /// \return + /// An error if the replacement could not be completed. + Status ReplaceModule(lldb::ModuleSP old_module_sp, + const lldb::ModuleSP &new_module_sp); + void SymbolsDidLoad(ModuleList &module_list); void ClearModules(bool delete_locations); diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp index f77e87ad43aa2..5384897b4f255 100644 --- a/lldb/source/Commands/CommandObjectTarget.cpp +++ b/lldb/source/Commands/CommandObjectTarget.cpp @@ -41,6 +41,7 @@ #include "lldb/Symbol/UnwindPlan.h" #include "lldb/Symbol/VariableList.h" #include "lldb/Target/ABI.h" +#include "lldb/Target/DynamicLoader.h" #include "lldb/Target/Process.h" #include "lldb/Target/RegisterContext.h" #include "lldb/Target/SectionLoadList.h" @@ -3135,6 +3136,222 @@ class CommandObjectTargetModulesLoad OptionGroupUInt64 m_slide_option; }; +#pragma mark CommandObjectTargetModulesReplace + +#define LLDB_OPTIONS_target_modules_replace +#include "CommandOptions.inc" + +// Replace a module in the target with a different file on disk. + +class CommandObjectTargetModulesReplace : public CommandObjectParsed { +public: + CommandObjectTargetModulesReplace(CommandInterpreter &interpreter) + : CommandObjectParsed( + interpreter, "target modules replace", + "Replace a module in the current target with a file that has more " + "complete contents, usually to resolve a placeholder module from a " + "core file once the real binary has been located. To attach debug " + "info to a module that is otherwise fine, use 'target symbols " + "add'.", + "target modules replace [--old-path <path>] [--force] <path>", + eCommandRequiresTarget | eCommandTryTargetAPILock | + eCommandProcessMustBePaused) { + AddSimpleArgumentList(eArgTypePath, eArgRepeatPlain); + } + + ~CommandObjectTargetModulesReplace() override = default; + + Options *GetOptions() override { return &m_options; } + + class CommandOptions : public Options { + public: + CommandOptions() = default; + + ~CommandOptions() override = default; + + Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, + ExecutionContext *execution_context) override { + const int short_option = m_getopt_table[option_idx].val; + + switch (short_option) { + case 'o': + m_old_path.assign(std::string(option_arg)); + break; + case 'f': + m_force = true; + break; + default: + llvm_unreachable("Unimplemented option"); + } + return Status(); + } + + void OptionParsingStarting(ExecutionContext *execution_context) override { + m_old_path.clear(); + m_force = false; + } + + llvm::ArrayRef<OptionDefinition> GetDefinitions() override { + return llvm::ArrayRef(g_target_modules_replace_options); + } + + std::string m_old_path; + bool m_force = false; + }; + +protected: + CommandOptions m_options; + + // Find the module the new file is meant to stand in for. Preferred order is + // the path the user gave, then the UUID read out of the new file, then its + // basename. + ModuleSP FindModuleToReplace(Target &target, const FileSpec &new_file_spec, + const UUID &new_uuid, + CommandReturnObject &result) { + ModuleSpec search_spec; + llvm::StringRef description; + + if (!m_options.m_old_path.empty()) { + search_spec.GetFileSpec().SetPath(m_options.m_old_path); + description = "the given path"; + } else if (new_uuid.IsValid()) { + search_spec.GetUUID() = new_uuid; + description = "a matching UUID"; + } else { + search_spec.GetFileSpec().SetFilename(new_file_spec.GetFilename()); + description = "a matching name"; + } + + ModuleList matches; + target.GetImages().FindModules(search_spec, matches); + + if (matches.IsEmpty()) { + // A file with a UUID that names nothing in the target is still worth + // trying by name, the placeholder it should replace may have been built + // without one. + if (m_options.m_old_path.empty() && new_uuid.IsValid()) { + ModuleSpec by_name; + by_name.GetFileSpec().SetFilename(new_file_spec.GetFilename()); + target.GetImages().FindModules(by_name, matches); + description = + matches.IsEmpty() ? "a matching UUID or name" : "a matching name"; + } + if (matches.IsEmpty()) { + result.AppendErrorWithFormatv( + "no module in the target was found by {0}, use the --old-path " + "option to name the module to replace", + description); + return ModuleSP(); + } + } + + if (matches.GetSize() > 1) { + StreamString paths; + for (size_t i = 0; i < matches.GetSize(); ++i) + paths.Format("\n {0}", matches.GetModuleAtIndex(i)->GetFileSpec()); + result.AppendErrorWithFormatv( + "{0} modules in the target were found by {1}, use the --old-path " + "option to name one of:{2}", + matches.GetSize(), description, paths.GetString()); + return ModuleSP(); + } + + return matches.GetModuleAtIndex(0); + } + + void DoExecute(Args &args, CommandReturnObject &result) override { + Target *target = GetTarget(); + assert(target && "target guaranteed by eCommandRequiresTarget"); + + if (args.GetArgumentCount() != 1) { + result.AppendError( + "'target modules replace' takes one argument: the path of the file " + "to replace a module with"); + return; + } + llvm::StringRef new_module_path = args.GetArgumentAtIndex(0); + + // Nothing below may change the target until Target::ReplaceModule() is + // called, so a failure can never leave the target half way through a + // replacement. + FileSpec new_file_spec(new_module_path); + FileSystem::Instance().Resolve(new_file_spec); + if (!FileSystem::Instance().Exists(new_file_spec)) { + std::string resolved_path = new_file_spec.GetPath(); + if (resolved_path != new_module_path) + result.AppendErrorWithFormatv( + "invalid module path '{0}' with resolved path '{1}'", + new_module_path, resolved_path); + else + result.AppendErrorWithFormatv("invalid module path '{0}'", + new_module_path); + return; + } + + // Read the UUID straight from the file rather than from a Module, so the + // module the new file should replace can be found before anything is added + // to the target. + UUID new_uuid; + ModuleSpecList file_specs = + ObjectFile::GetModuleSpecifications(new_file_spec, 0, 0); + if (file_specs.GetSize() > 0) { + ModuleSpec arch_spec; + arch_spec.GetArchitecture() = target->GetArchitecture(); + ModuleSpec matching_spec; + if (file_specs.FindMatchingModuleSpec(arch_spec, matching_spec)) + new_uuid = matching_spec.GetUUID(); + else if (file_specs.GetSize() == 1) + new_uuid = file_specs.GetModuleSpecRefAtIndex(0).GetUUID(); + } + + ModuleSP old_module_sp = + FindModuleToReplace(*target, new_file_spec, new_uuid, result); + if (!old_module_sp) + return; + + // Different UUIDs mean the new file is not the binary that ran, so the + // symbols would not describe the memory the target has. + const UUID &old_uuid = old_module_sp->GetUUID(); + if (!m_options.m_force && old_uuid.IsValid() && new_uuid.IsValid() && + old_uuid != new_uuid) { + result.AppendErrorWithFormatv( + "'{0}' has UUID {1}, which does not match UUID {2} of the module it " + "would replace, '{3}'. Use the --force option to replace it anyway", + new_file_spec.GetPath(), new_uuid.GetAsString(), + old_uuid.GetAsString(), old_module_sp->GetFileSpec().GetPath()); + return; + } + + ModuleSpec new_module_spec(new_file_spec); + if (!new_module_spec.GetArchitecture().IsValid()) + new_module_spec.GetArchitecture() = target->GetArchitecture(); + + Status error; + ModuleSP new_module_sp = + target->GetOrCreateModule(new_module_spec, /*notify=*/false, &error); + if (!new_module_sp) { + if (error.Fail()) + result.SetError(error.takeError()); + else + result.AppendErrorWithFormatv("unsupported module: {0}", + new_file_spec.GetPath()); + return; + } + + const std::string old_module_desc = old_module_sp->GetFileSpec().GetPath(); + Status replace_error = + target->ReplaceModule(std::move(old_module_sp), new_module_sp); + if (replace_error.Fail()) { + result.SetError(replace_error.takeError()); + return; + } + + result.AppendMessageWithFormatv("replaced '{0}' with '{1}'", + old_module_desc, new_file_spec.GetPath()); + result.SetStatus(eReturnStatusSuccessFinishResult); + } +}; + #pragma mark CommandObjectTargetModulesList // List images with associated information #define LLDB_OPTIONS_target_modules_list @@ -4230,6 +4447,9 @@ class CommandObjectTargetModules : public CommandObjectMultiword { LoadSubCommand( "lookup", CommandObjectSP(new CommandObjectTargetModulesLookup(interpreter))); + LoadSubCommand( + "replace", + CommandObjectSP(new CommandObjectTargetModulesReplace(interpreter))); LoadSubCommand( "search-paths", CommandObjectSP( diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td index ab851725979ef..3d5a6b19f3e2b 100644 --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -1820,6 +1820,18 @@ let Command = "target modules show unwind" in { Desc<"Show cached unwind information">; } +let Command = "target modules replace" in { + def target_modules_replace_old_path + : Option<"old-path", "o">, + Arg<"Path">, + Desc<"Path of the module in the target to replace. Only needed when it " + "cannot be worked out from the new file.">; + def target_modules_replace_force + : Option<"force", "f">, + Desc<"Replace the module even when its UUID does not match the UUID of " + "the new file.">; +} + let Command = "target modules lookup" in { def target_modules_lookup_address : Option<"address", "a">, Group<1>, diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp index 70d24c45552dd..f54bf49e5f6a7 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp @@ -211,6 +211,21 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const ModuleSP &module_sp) { return std::nullopt; } +Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp, + const ModuleSP &new_module_sp) { + // Images are mapped in one piece here, so the target's placement is already + // correct. Only the link map address needs moving, without it no thread local + // in the replacement can be resolved. + llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex); + auto it = m_loaded_modules.find(old_module_sp); + if (it == m_loaded_modules.end()) + return Status(); + const addr_t link_map_addr = it->second; + m_loaded_modules.erase(it); + m_loaded_modules[new_module_sp] = link_map_addr; + return Status(); +} + void DynamicLoaderPOSIXDYLD::UpdateLoadedSections(ModuleSP module, addr_t link_map_addr, addr_t base_addr, diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h index 6efb92673a13c..53f68975e3efc 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h @@ -66,6 +66,10 @@ class DynamicLoaderPOSIXDYLD : public lldb_private::DynamicLoader { llvm::function_ref<bool(const lldb_private::Thread &)> save_thread_predicate) override; + lldb_private::Status + ReplaceModule(const lldb::ModuleSP &old_module_sp, + const lldb::ModuleSP &new_module_sp) override; + protected: /// Runtime linker rendezvous structure. DYLDRendezvous m_rendezvous; diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index a317ba3ccb3f9..b29af9775b87e 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -48,6 +48,7 @@ #include "lldb/Symbol/ObjectFile.h" #include "lldb/Symbol/Symbol.h" #include "lldb/Target/ABI.h" +#include "lldb/Target/DynamicLoader.h" #include "lldb/Target/ExecutionContext.h" #include "lldb/Target/Language.h" #include "lldb/Target/LanguageRuntime.h" @@ -2023,6 +2024,91 @@ void Target::ModulesDidUnload(ModuleList &module_list, bool delete_locations) { } } +Status Target::ReplaceModule(ModuleSP old_module_sp, + const ModuleSP &new_module_sp) { + if (!old_module_sp || !new_module_sp) + return Status::FromErrorString("invalid module"); + + if (old_module_sp == new_module_sp) + return Status::FromErrorStringWithFormatv( + "'{0}' is already the module being replaced", + new_module_sp->GetFileSpec()); + + // Where the old module sits, so the replacement can be given the same + // address. Read it before anything unloads it. + addr_t base_load_addr = LLDB_INVALID_ADDRESS; + if (ObjectFile *object_file = old_module_sp->GetObjectFile()) { + Address base_addr = object_file->GetBaseAddress(); + if (base_addr.IsValid()) + base_load_addr = base_addr.GetLoadAddress(this); + } + + // Also keeps the old module alive across ModulesDidUnload(), which reaches + // its breakpoint locations through section_sp->GetModule(), a weak + // reference. + ModuleList unloaded_modules; + unloaded_modules.Append(old_module_sp, /*notify=*/false); + + if (m_images.GetIndexForModule(old_module_sp.get()) != LLDB_INVALID_INDEX32) + m_images.Remove(old_module_sp, /*notify=*/false); + + // Unloads its sections and deletes its breakpoint locations. Must be + // explicit, no notification path passes delete_locations=true. + ModulesDidUnload(unloaded_modules, /*delete_locations=*/true); + + // Target::GetOrCreateModule() adds the module it creates, so the replacement + // is usually in the target already. + if (m_images.GetIndexForModule(new_module_sp.get()) == LLDB_INVALID_INDEX32) + m_images.Append(new_module_sp, /*notify=*/false); + + // ModuleList keeps the executable at index 0, but the replacement was + // appended while the old one still held that slot. Add it again to sort it to + // the front. + if (ObjectFile *new_object_file = new_module_sp->GetObjectFile()) { + if (new_object_file->GetType() == ObjectFile::eTypeExecutable && + m_images.GetIndexForModule(new_module_sp.get()) != 0) { + m_images.Remove(new_module_sp, /*notify=*/false); + m_images.Append(new_module_sp, /*notify=*/false); + } + } + + // Generic placement, correct whenever a module is mapped in one piece. The + // dynamic loader corrects it below on platforms where it is not. + if (base_load_addr != LLDB_INVALID_ADDRESS) { + bool changed = false; + new_module_sp->SetLoadAddress(*this, base_load_addr, + /*value_is_offset=*/false, changed); + } + + // Let the dynamic loader redo the load addresses if this platform needs it, + // and move over anything it tracks per module, such as the link map address + // thread locals are found through. + Status error; + if (m_process_sp) { + if (DynamicLoader *dyld = m_process_sp->GetDynamicLoader()) + error = dyld->ReplaceModule(old_module_sp, new_module_sp); + } + + // Sections must be in place first, resolving breakpoints into a module whose + // sections are not loaded yields locations with no address. + ModuleList added_modules; + added_modules.Append(new_module_sp, /*notify=*/false); + ModulesDidLoad(added_modules); + + // Drop the old module from the shared module cache so a later lookup of the + // same path cannot resurrect it. + unloaded_modules.Clear(); + std::weak_ptr<Module> old_module_wp(old_module_sp->weak_from_this()); + old_module_sp.reset(); + ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp); + + // Cached stack frames and register contexts can still hold the old module. + if (m_process_sp) + m_process_sp->Flush(); + + return error; +} + bool Target::ModuleIsExcludedForUnconstrainedSearches( const FileSpec &module_file_spec) { if (GetBreakpointsConsultPlatformAvoidList()) { diff --git a/lldb/test/API/commands/target/modules/replace/Makefile b/lldb/test/API/commands/target/modules/replace/Makefile new file mode 100644 index 0000000000000..1d76be88fc41a --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/Makefile @@ -0,0 +1,18 @@ +CXX_SOURCES := main.cpp +USE_LIBDL := 1 + +a.out: lib_v hidden_lib_v other_exe + +include Makefile.rules + +other_exe: + "$(MAKE)" -f $(MAKEFILE_RULES) \ + CXX_SOURCES=other_main.cpp EXE=other.out + +lib_v: + "$(MAKE)" -f $(MAKEFILE_RULES) \ + DYLIB_ONLY=YES DYLIB_CXX_SOURCES=v.cpp DYLIB_NAME=replace_v + +hidden_lib_v: + "$(MAKE)" VPATH=$(SRCDIR)/hidden -C hidden -f $(MAKEFILE_RULES) \ + DYLIB_ONLY=YES DYLIB_CXX_SOURCES=v.cpp DYLIB_NAME=replace_v diff --git a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py new file mode 100644 index 0000000000000..6b1f657b04387 --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py @@ -0,0 +1,318 @@ +""" +Test the "target modules replace" command. +""" + +import os +import shutil + +import lldb +from lldbsuite.test import lldbutil +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class TargetModulesReplaceTestCase(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def setUp(self): + TestBase.setUp(self) + # The "v2" variant of the library is built into a subdirectory so that + # it can share a soname with the "v1" variant. + lldbutil.mkdir_p(self.getBuildArtifact("hidden")) + + def build_and_get_paths(self): + """Build and return the paths of the two library variants, plus a copy + of v1 at a third path. The copy shares v1's UUID, the v2 variant does + not, which is what lets the two lookup paths be tested apart.""" + self.build() + lib_name = self.platformContext.getFullLibName("replace_v") + v1 = self.getBuildArtifact(lib_name) + v2 = os.path.join(self.getBuildDir(), "hidden", lib_name) + v1_copy = self.getBuildArtifact("copy_of_" + lib_name) + shutil.copyfile(v1, v1_copy) + for path in (v1, v2, v1_copy): + self.assertTrue(os.path.exists(path), "%s was built" % path) + return v1, v2, v1_copy + + def static_target_with_v1(self): + """Make a target with the v1 library added but nothing loaded.""" + v1, v2, v1_copy = self.build_and_get_paths() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + self.runCmd("target modules add '%s'" % v1) + return target, v1, v2, v1_copy + + def base_load_address(self, module, target): + return module.GetObjectFileHeaderAddress().GetLoadAddress(target) + + def test_matching_uuid_finds_the_module(self): + """A single argument is enough when the UUID identifies the module.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + num_modules = target.GetNumModules() + + # No --old-path and no --force: the copy shares v1's UUID, so the module + # to replace can be worked out from the file alone. + self.runCmd("target modules replace '%s'" % v1_copy) + + self.assertEqual(target.GetNumModules(), num_modules) + self.assertFalse(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + self.assertTrue(target.FindModule(lldb.SBFileSpec(v1_copy)).IsValid()) + + def test_mismatched_uuid_is_an_error(self): + """A file that is not the same build is refused unless forced.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + num_modules = target.GetNumModules() + + self.expect( + "target modules replace '%s'" % v2, + error=True, + substrs=["does not match UUID", "--force"], + ) + + # The target must be left exactly as it was. + self.assertEqual(target.GetNumModules(), num_modules) + self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + self.assertFalse(target.FindModule(lldb.SBFileSpec(v2)).IsValid()) + + # And --force goes through. + self.runCmd("target modules replace --force '%s'" % v2) + self.assertTrue(target.FindModule(lldb.SBFileSpec(v2)).IsValid()) + + def test_old_path_option(self): + """--old-path names the module to replace explicitly.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + + self.runCmd("target modules replace --old-path '%s' --force '%s'" % (v1, v2)) + self.assertFalse(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + self.assertTrue(target.FindModule(lldb.SBFileSpec(v2)).IsValid()) + + def test_no_matching_module(self): + """A file that matches nothing points the user at --old-path.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + unrelated = self.getBuildArtifact("other.out") + + self.expect( + "target modules replace '%s'" % unrelated, + error=True, + substrs=["no module in the target was found", "--old-path"], + ) + + def test_module_is_replaced_not_mutated(self): + """The old module is removed and a distinct new one takes its place.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + + old_module = target.FindModule(lldb.SBFileSpec(v1)) + self.assertTrue(old_module.IsValid(), "v1 is in the target") + old_uuid = old_module.GetUUIDString() + + self.runCmd("target modules replace --force '%s'" % v2) + + new_module = target.FindModule(lldb.SBFileSpec(v2)) + self.assertTrue(new_module.IsValid(), "v2 was added to the target") + self.assertNotEqual(old_uuid, new_module.GetUUIDString()) + + # Symbols now come from the replacement. + self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid()) + self.assertFalse(new_module.FindSymbol("only_in_v1").IsValid()) + + # The old module object was not modified in place. This is the guard + # against implementing the command by swapping the ObjectFile out from + # under a live Module, which leaves stale pointers behind. + self.assertEqual(old_module.GetUUIDString(), old_uuid) + self.assertTrue( + old_module.FindSymbol("only_in_v1").IsValid(), + "the replaced module still describes its own file", + ) + + def test_unloaded_module_stays_unloaded(self): + """Replacing a module that was never loaded doesn't load anything.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + + old_module = target.FindModule(lldb.SBFileSpec(v1)) + self.assertEqual( + self.base_load_address(old_module, target), lldb.LLDB_INVALID_ADDRESS + ) + + self.runCmd("target modules replace --force '%s'" % v2) + + new_module = target.FindModule(lldb.SBFileSpec(v2)) + self.assertEqual( + self.base_load_address(new_module, target), lldb.LLDB_INVALID_ADDRESS + ) + for section in new_module.section_iter(): + self.assertEqual(section.GetLoadAddress(target), lldb.LLDB_INVALID_ADDRESS) + + def test_load_address_is_preserved(self): + """A loaded module's replacement is loaded at the same address.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + self.runCmd("target modules load --file '%s' --slide 0x100000" % v1) + + old_module = target.FindModule(lldb.SBFileSpec(v1)) + base_before = self.base_load_address(old_module, target) + self.assertNotEqual(base_before, lldb.LLDB_INVALID_ADDRESS) + + self.runCmd("target modules replace --force '%s'" % v2) + + # The image base is what is preserved. Individual section addresses are + # not comparable: the two files lay their sections out differently. + new_module = target.FindModule(lldb.SBFileSpec(v2)) + self.assertEqual(self.base_load_address(new_module, target), base_before) + + # The replaced module's sections were unloaded. + for section in old_module.section_iter(): + self.assertEqual( + section.GetLoadAddress(target), + lldb.LLDB_INVALID_ADDRESS, + "section %s of the replaced module was unloaded" % section.GetName(), + ) + + def test_replace_executable(self): + """The replacement executable stays at the front of the module list.""" + self.build() + exe = self.getBuildArtifact("a.out") + other = self.getBuildArtifact("other.out") + + target = self.dbg.CreateTarget(exe) + self.assertTrue(target, VALID_TARGET) + self.assertEqual( + target.GetModuleAtIndex(0).GetFileSpec().GetFilename(), "a.out" + ) + num_modules = target.GetNumModules() + + self.runCmd( + "target modules replace --old-path '%s' --force '%s'" % (exe, other) + ) + + self.assertEqual(target.GetNumModules(), num_modules) + self.assertEqual( + target.GetModuleAtIndex(0).GetFileSpec().GetFilename(), + "other.out", + "the replacement executable is at index 0", + ) + self.assertEqual( + target.GetExecutable().GetFilename(), + "other.out", + "the target's executable follows the replacement", + ) + + def test_round_trip(self): + """Replacing back and forth doesn't accumulate or drop modules.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + num_modules = target.GetNumModules() + + self.runCmd("target modules replace --force '%s'" % v2) + self.assertEqual(target.GetNumModules(), num_modules) + self.runCmd("target modules replace --force '%s'" % v1) + self.assertEqual(target.GetNumModules(), num_modules) + + self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + self.assertFalse(target.FindModule(lldb.SBFileSpec(v2)).IsValid()) + + def test_errors(self): + """Bad input is rejected without touching the target.""" + target, v1, v2, v1_copy = self.static_target_with_v1() + num_modules = target.GetNumModules() + + self.expect( + "target modules replace /no/such/file", + error=True, + substrs=["invalid module path"], + ) + self.expect( + "target modules replace", + error=True, + substrs=["takes one argument"], + ) + self.expect( + "target modules replace --old-path /not/in/the/target '%s'" % v2, + error=True, + substrs=["no module in the target was found"], + ) + + self.assertEqual(target.GetNumModules(), num_modules) + self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + + @skipIfWindows + @skipIfRemote + def test_breakpoints_move_to_the_replacement(self): + """Breakpoint locations are re-resolved into the new module.""" + v1, v2, v1_copy = self.build_and_get_paths() + + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + target.BreakpointCreateBySourceRegex( + "break after dlopen", lldb.SBFileSpec("main.cpp") + ) + + launch_info = target.GetLaunchInfo() + launch_info.SetArguments([v1], True) + error = lldb.SBError() + process = target.Launch(launch_info, error) + self.assertSuccess(error, "the process launched") + self.assertState(process.GetState(), lldb.eStateStopped) + + old_module = target.FindModule(lldb.SBFileSpec(v1)) + self.assertTrue(old_module.IsValid(), "v1 was dlopen'd") + old_uuid = old_module.GetUUIDString() + + # A breakpoint on a symbol both variants define, and one on a symbol + # only the old variant defines. + common_bp = target.BreakpointCreateByName("common_func") + self.assertEqual(common_bp.GetNumLocations(), 1) + only_v1_bp = target.BreakpointCreateByName("only_in_v1") + self.assertEqual(only_v1_bp.GetNumLocations(), 1) + + self.runCmd("target modules replace --force '%s'" % v2) + + # The shared symbol re-resolves, and nothing still points into the + # module that was removed. + self.assertGreaterEqual(common_bp.GetNumLocations(), 1) + for i in range(common_bp.GetNumLocations()): + module = common_bp.GetLocationAtIndex(i).GetAddress().GetModule() + self.assertNotEqual( + module.GetUUIDString(), + old_uuid, + "no location still resolves into the replaced module", + ) + + # The symbol that only existed in the old variant goes pending. + self.assertEqual(only_v1_bp.GetNumLocations(), 0) + + @skipIfWindows + @skipIfRemote + @skipUnlessPlatform(["linux"]) + def test_thread_local_storage_still_resolves(self): + """The dynamic loader's per module state follows the replacement. + + The loader keys the link map address it needs for TLS lookups off the + module itself, so without help the replacement has no link map and every + thread local in it reads back as "no TLS data currently exists".""" + v1, v2, v1_copy = self.build_and_get_paths() + + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + target.BreakpointCreateBySourceRegex( + "break after dlopen", lldb.SBFileSpec("main.cpp") + ) + + launch_info = target.GetLaunchInfo() + launch_info.SetArguments([v1], True) + error = lldb.SBError() + process = target.Launch(launch_info, error) + self.assertSuccess(error, "the process launched") + self.assertState(process.GetState(), lldb.eStateStopped) + + # Sanity check that TLS resolves at all before the replace, so that a + # failure below is attributable to the replace and not to the platform. + before = target.EvaluateExpression("tls_var") + self.assertSuccess(before.GetError(), "TLS resolves before the replace") + self.assertEqual(before.GetValueAsSigned(), 701) + + self.runCmd("target modules replace --force '%s'" % v2) + + after = target.EvaluateExpression("tls_var") + self.assertSuccess(after.GetError(), "TLS still resolves after the replace") + # The variable is read out of the live process, whose mapped pages are + # still the old library's, so the value is v1's. What matters is that the + # lookup resolves at all instead of failing to find a link map. + self.assertNotEqual(after.GetValueAsSigned(), 0) diff --git a/lldb/test/API/commands/target/modules/replace/hidden/v.cpp b/lldb/test/API/commands/target/modules/replace/hidden/v.cpp new file mode 100644 index 0000000000000..506da883ad35e --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/hidden/v.cpp @@ -0,0 +1,11 @@ +// The "v2" variant of the library. Built with the same soname as ../v.cpp so +// that it can stand in for it, but with different content so that the two are +// distinguishable by UUID and by the symbols they define. + +extern "C" int only_in_v2() { return 202; } + +__thread int tls_var = 702; + +extern "C" int get_tls_var() { return tls_var; } + +extern "C" int common_func() { return 2; } diff --git a/lldb/test/API/commands/target/modules/replace/main.cpp b/lldb/test/API/commands/target/modules/replace/main.cpp new file mode 100644 index 0000000000000..be04550b2f60c --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/main.cpp @@ -0,0 +1,24 @@ +#include <cstdio> +#include <dlfcn.h> + +int main(int argc, char **argv) { + if (argc < 2) + return 1; + + void *handle = dlopen(argv[1], RTLD_NOW); + if (!handle) + return 2; + + int (*common_func)() = (int (*)())dlsym(handle, "common_func"); + int (*get_tls_var)() = (int (*)())dlsym(handle, "get_tls_var"); + if (!common_func || !get_tls_var) + return 3; + + // Call through to the library's thread-local before stopping, so that its + // TLS block has actually been allocated for this thread by the time the + // test looks at it. + int tls = get_tls_var(); + + printf("%d %d\n", common_func(), tls); // break after dlopen + return 0; +} diff --git a/lldb/test/API/commands/target/modules/replace/other_main.cpp b/lldb/test/API/commands/target/modules/replace/other_main.cpp new file mode 100644 index 0000000000000..ff7eaf19686b1 --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/other_main.cpp @@ -0,0 +1,4 @@ +// A second executable, used to check that replacing the executable module +// keeps it at the front of the target's module list. + +int main() { return 0; } diff --git a/lldb/test/API/commands/target/modules/replace/v.cpp b/lldb/test/API/commands/target/modules/replace/v.cpp new file mode 100644 index 0000000000000..b009c7edc167a --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/v.cpp @@ -0,0 +1,10 @@ +// The "v1" variant of the library. See hidden/v.cpp for the "v2" variant that +// it gets replaced with: same soname, different content and different UUID. + +extern "C" int only_in_v1() { return 101; } + +__thread int tls_var = 701; + +extern "C" int get_tls_var() { return tls_var; } + +extern "C" int common_func() { return 1; } diff --git a/lldb/test/API/functionalities/completion/TestCompletion.py b/lldb/test/API/functionalities/completion/TestCompletion.py index f4bdc3a894215..8cab49bdcd973 100644 --- a/lldb/test/API/functionalities/completion/TestCompletion.py +++ b/lldb/test/API/functionalities/completion/TestCompletion.py @@ -533,6 +533,15 @@ def test_target_modules_load_aout(self): self.dbg.CreateTarget(self.getBuildArtifact("a.out")) self.complete_from_to("target modules load a.ou", ["a.out"]) + def test_target_modules_replace(self): + """Tests that the argument completes against paths on disk.""" + self.build() + self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.complete_from_to( + "target modules replace " + self.getBuildArtifact("a.ou"), + [self.getBuildArtifact("a.out")], + ) + def test_target_modules_search_paths_insert(self): # Completion won't work without a valid target. self.complete_from_to( >From 7e5c5c62c91a7f7335d723f8dc75d66bc5ecccd4 Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Thu, 6 Aug 2026 17:05:08 -0700 Subject: [PATCH 2/6] Fixed Greg's comments --- lldb/source/Commands/CommandObjectTarget.cpp | 35 +++++--- lldb/source/Commands/Options.td | 5 +- .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp | 38 ++++++--- lldb/source/Target/Target.cpp | 72 +++++++++++----- .../replace/TestTargetModulesReplace.py | 83 +++++++++++++++++++ .../modules/replace/placeholder-no-uuid.yaml | 17 ++++ .../target/modules/replace/unplaceable.yaml | 13 +++ 7 files changed, 219 insertions(+), 44 deletions(-) create mode 100644 lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml create mode 100644 lldb/test/API/commands/target/modules/replace/unplaceable.yaml diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp index 5384897b4f255..14e56998fd407 100644 --- a/lldb/source/Commands/CommandObjectTarget.cpp +++ b/lldb/source/Commands/CommandObjectTarget.cpp @@ -3152,7 +3152,11 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { "complete contents, usually to resolve a placeholder module from a " "core file once the real binary has been located. To attach debug " "info to a module that is otherwise fine, use 'target symbols " - "add'.", + "add'.\n" + "The module to replace is found by the new file's UUID, or by its " + "basename if it has no UUID. Use --old-path when neither picks a " + "single module, and --force to replace a module whose UUID does " + "not match the new file's.", "target modules replace [--old-path <path>] [--force] <path>", eCommandRequiresTarget | eCommandTryTargetAPILock | eCommandProcessMustBePaused) { @@ -3288,21 +3292,25 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { return; } - // Read the UUID straight from the file rather than from a Module, so the - // module the new file should replace can be found before anything is added - // to the target. - UUID new_uuid; - ModuleSpecList file_specs = + // Read the file rather than build a Module from it, so the module it + // should replace can be found before anything is added to the target. + ModuleSpec new_module_spec(new_file_spec); + ModuleSpecList new_module_specs = ObjectFile::GetModuleSpecifications(new_file_spec, 0, 0); - if (file_specs.GetSize() > 0) { + if (new_module_specs.GetSize() > 0) { ModuleSpec arch_spec; arch_spec.GetArchitecture() = target->GetArchitecture(); ModuleSpec matching_spec; - if (file_specs.FindMatchingModuleSpec(arch_spec, matching_spec)) - new_uuid = matching_spec.GetUUID(); - else if (file_specs.GetSize() == 1) - new_uuid = file_specs.GetModuleSpecRefAtIndex(0).GetUUID(); + if (!new_module_specs.FindMatchingModuleSpec(arch_spec, matching_spec)) { + result.AppendErrorWithFormatv( + "'{0}' does not contain the target architecture {1}", + new_file_spec.GetPath(), + target->GetArchitecture().GetTriple().str()); + return; + } + new_module_spec = matching_spec; } + const UUID &new_uuid = new_module_spec.GetUUID(); ModuleSP old_module_sp = FindModuleToReplace(*target, new_file_spec, new_uuid, result); @@ -3322,10 +3330,13 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { return; } - ModuleSpec new_module_spec(new_file_spec); if (!new_module_spec.GetArchitecture().IsValid()) new_module_spec.GetArchitecture() = target->GetArchitecture(); + // Look the file up by path alone. Asking for the UUID as well would find + // the module already in the target, which is the one being replaced. + new_module_spec.GetUUID().Clear(); + Status error; ModuleSP new_module_sp = target->GetOrCreateModule(new_module_spec, /*notify=*/false, &error); diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td index 3d5a6b19f3e2b..055adf1222e8a 100644 --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -1824,8 +1824,9 @@ let Command = "target modules replace" in { def target_modules_replace_old_path : Option<"old-path", "o">, Arg<"Path">, - Desc<"Path of the module in the target to replace. Only needed when it " - "cannot be worked out from the new file.">; + Desc<"Path of the module in the target to replace. This is only needed " + "if the new module doesn't have a valid UUID or if the basename " + "doesn't uniquely match any existing target modules.">; def target_modules_replace_force : Option<"force", "f">, Desc<"Replace the module even when its UUID does not match the UUID of " diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp index f54bf49e5f6a7..6d8c6ba36b8b9 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp @@ -213,16 +213,34 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const ModuleSP &module_sp) { Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp, const ModuleSP &new_module_sp) { - // Images are mapped in one piece here, so the target's placement is already - // correct. Only the link map address needs moving, without it no thread local - // in the replacement can be resolved. - llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex); - auto it = m_loaded_modules.find(old_module_sp); - if (it == m_loaded_modules.end()) - return Status(); - const addr_t link_map_addr = it->second; - m_loaded_modules.erase(it); - m_loaded_modules[new_module_sp] = link_map_addr; + // Where the old module was mapped, read before its sections go away. + addr_t base_addr = LLDB_INVALID_ADDRESS; + if (ObjectFile *object_file = old_module_sp->GetObjectFile()) { + Address base = object_file->GetBaseAddress(); + if (base.IsValid()) + base_addr = base.GetLoadAddress(&m_process->GetTarget()); + } + if (base_addr == LLDB_INVALID_ADDRESS) + return Status::FromErrorStringWithFormatv( + "'{0}' is not loaded at a known address", old_module_sp->GetFileSpec()); + + addr_t link_map_addr = LLDB_INVALID_ADDRESS; + { + // The link map address is what thread local lookups are found through, and + // it is keyed by module, so it has to be moved onto the replacement. + llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex); + auto it = m_loaded_modules.find(old_module_sp); + if (it != m_loaded_modules.end()) { + link_map_addr = it->second; + m_loaded_modules.erase(it); + } + } + + UnloadSections(old_module_sp); + // Images are mapped in one piece here, so the recorded address is where the + // replacement goes, not an offset to slide it by. + UpdateLoadedSections(new_module_sp, link_map_addr, base_addr, + /*base_addr_is_offset=*/false); return Status(); } diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index b29af9775b87e..557d6934c54af 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -2052,10 +2052,6 @@ Status Target::ReplaceModule(ModuleSP old_module_sp, if (m_images.GetIndexForModule(old_module_sp.get()) != LLDB_INVALID_INDEX32) m_images.Remove(old_module_sp, /*notify=*/false); - // Unloads its sections and deletes its breakpoint locations. Must be - // explicit, no notification path passes delete_locations=true. - ModulesDidUnload(unloaded_modules, /*delete_locations=*/true); - // Target::GetOrCreateModule() adds the module it creates, so the replacement // is usually in the target already. if (m_images.GetIndexForModule(new_module_sp.get()) == LLDB_INVALID_INDEX32) @@ -2072,35 +2068,71 @@ Status Target::ReplaceModule(ModuleSP old_module_sp, } } - // Generic placement, correct whenever a module is mapped in one piece. The - // dynamic loader corrects it below on platforms where it is not. - if (base_load_addr != LLDB_INVALID_ADDRESS) { - bool changed = false; - new_module_sp->SetLoadAddress(*this, base_load_addr, - /*value_is_offset=*/false, changed); + // The dynamic loader is the only thing that knows how this platform maps an + // image, so it moves the sections over and carries across anything it tracks + // per module, such as the link map address thread locals are found through. + // It runs before ModulesDidUnload() below, which would otherwise take the old + // module's load addresses away before the loader could read them. + Status error; + DynamicLoader *dyld = + m_process_sp ? m_process_sp->GetDynamicLoader() : nullptr; + if (dyld) { + error = dyld->ReplaceModule(old_module_sp, new_module_sp); + } else { + // Targets with no dynamic loader, a static target or most minidumps, still + // need the old sections taken away and the replacement put where they were. + UnloadModuleSections(old_module_sp); + if (base_load_addr != LLDB_INVALID_ADDRESS) { + // Module::SetLoadAddress() only reports whether there was an object file + // to ask, so \a loaded is what says any section was placed. + bool loaded = false; + new_module_sp->SetLoadAddress(*this, base_load_addr, + /*value_is_offset=*/false, loaded); + if (!loaded) + error = Status::FromErrorStringWithFormatv( + "'{0}' could not be loaded at {1:x}, where '{2}' was", + new_module_sp->GetFileSpec(), base_load_addr, + old_module_sp->GetFileSpec()); + } } - // Let the dynamic loader redo the load addresses if this platform needs it, - // and move over anything it tracks per module, such as the link map address - // thread locals are found through. - Status error; - if (m_process_sp) { - if (DynamicLoader *dyld = m_process_sp->GetDynamicLoader()) - error = dyld->ReplaceModule(old_module_sp, new_module_sp); + if (error.Fail()) { + // Placement got part way through at most, so leave neither module loaded. + // The old module goes back into the target, unloaded, so that the caller is + // not left short of a module it never asked to lose. + UnloadModuleSections(new_module_sp); + UnloadModuleSections(old_module_sp); + m_images.Remove(new_module_sp, /*notify=*/false); + if (m_images.GetIndexForModule(old_module_sp.get()) == LLDB_INVALID_INDEX32) + m_images.Append(old_module_sp, /*notify=*/false); + ModulesDidUnload(unloaded_modules, /*delete_locations=*/true); + return error; } + // Deletes the breakpoint locations that resolved into the old module. Must be + // explicit, no notification path passes delete_locations=true. Its section + // unload is a no-op by now. + ModulesDidUnload(unloaded_modules, /*delete_locations=*/true); + // Sections must be in place first, resolving breakpoints into a module whose // sections are not loaded yields locations with no address. ModuleList added_modules; added_modules.Append(new_module_sp, /*notify=*/false); ModulesDidLoad(added_modules); - // Drop the old module from the shared module cache so a later lookup of the - // same path cannot resurrect it. + // A placeholder or a module read out of memory stands for a file we could not + // use, so drop it from the shared module cache to stop a later lookup of the + // same path resurrecting it. Real files are left cached. + ObjectFile *old_object_file = old_module_sp->GetObjectFile(); + const bool evict_from_cache = + old_object_file == nullptr || + old_object_file->GetPluginName() == "placeholder" || + old_object_file->GetPluginName() == "memory"; unloaded_modules.Clear(); std::weak_ptr<Module> old_module_wp(old_module_sp->weak_from_this()); old_module_sp.reset(); - ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp); + if (evict_from_cache) + ModuleList::RemoveSharedModuleIfOrphaned(old_module_wp); // Cached stack frames and register contexts can still hold the old module. if (m_process_sp) diff --git a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py index 6b1f657b04387..e0f9da8596766 100644 --- a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py +++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py @@ -232,6 +232,89 @@ def test_errors(self): self.assertEqual(target.GetNumModules(), num_modules) self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) + @skipUnlessPlatform(["linux"]) + def test_placeholder_without_uuid(self): + """A placeholder with no UUID needs no --force, nothing can be compared.""" + v1, v2, v1_copy = self.build_and_get_paths() + core = self.getBuildArtifact("no-uuid.dmp") + self.yaml2obj("placeholder-no-uuid.yaml", core) + + target = self.dbg.CreateTarget("") + self.assertTrue(target.LoadCore(core).IsValid()) + placeholder = target.FindModule(lldb.SBFileSpec("/no/such/module.so")) + self.assertTrue(placeholder.IsValid()) + self.assertFalse(placeholder.GetUUIDString(), "the placeholder has no UUID") + base = self.base_load_address(placeholder, target) + self.assertNotEqual(base, lldb.LLDB_INVALID_ADDRESS) + + self.runCmd("target modules replace --old-path /no/such/module.so '%s'" % v1) + + new_module = target.FindModule(lldb.SBFileSpec(v1)) + self.assertTrue(new_module.IsValid()) + self.assertEqual(self.base_load_address(new_module, target), base) + self.assertTrue(new_module.FindSymbol("only_in_v1").IsValid()) + + @skipUnlessPlatform(["linux"]) + def test_failure_leaves_the_target_alone(self): + """A replacement that cannot be placed is refused, and nothing is lost.""" + v1, v2, v1_copy = self.build_and_get_paths() + core = self.getBuildArtifact("no-uuid.dmp") + self.yaml2obj("placeholder-no-uuid.yaml", core) + + target = self.dbg.CreateTarget("") + self.assertTrue(target.LoadCore(core).IsValid()) + num_modules = target.GetNumModules() + + # An object file with no loadable segments cannot go where the + # placeholder was. + unplaceable = self.getBuildArtifact("unplaceable.o") + self.yaml2obj("unplaceable.yaml", unplaceable) + self.expect( + "target modules replace --old-path /no/such/module.so --force '%s'" + % unplaceable, + error=True, + substrs=["could not be loaded at"], + ) + + self.assertEqual(target.GetNumModules(), num_modules) + self.assertTrue( + target.FindModule(lldb.SBFileSpec("/no/such/module.so")).IsValid(), + "the module that could not be replaced is still in the target", + ) + + @skipIfWindows + @skipIfRemote + @skipUnlessPlatform(["linux"]) + def test_core_file(self): + """Replacing a module in a core file keeps it at the same address.""" + v1, v2, v1_copy = self.build_and_get_paths() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + target.BreakpointCreateBySourceRegex( + "break after dlopen", lldb.SBFileSpec("main.cpp") + ) + launch_info = target.GetLaunchInfo() + launch_info.SetArguments([v1], True) + error = lldb.SBError() + process = target.Launch(launch_info, error) + self.assertSuccess(error) + + core = self.getBuildArtifact("saved.core") + self.runCmd("process save-core --style=full '%s'" % core) + process.Kill() + + target = self.dbg.CreateTarget("") + self.assertTrue(target.LoadCore(core).IsValid()) + old_module = target.FindModule(lldb.SBFileSpec(v1)) + self.assertTrue(old_module.IsValid()) + base = self.base_load_address(old_module, target) + + self.runCmd("target modules replace --force '%s'" % v2) + + new_module = target.FindModule(lldb.SBFileSpec(v2)) + self.assertTrue(new_module.IsValid()) + self.assertEqual(self.base_load_address(new_module, target), base) + self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid()) + @skipIfWindows @skipIfRemote def test_breakpoints_move_to_the_replacement(self): diff --git a/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml b/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml new file mode 100644 index 0000000000000..7eab2c37c0274 --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/placeholder-no-uuid.yaml @@ -0,0 +1,17 @@ +--- !minidump +Streams: + - Type: SystemInfo + Processor Arch: AMD64 + Platform ID: Linux + CSD Version: 'Linux' + CPU: + Vendor ID: GenuineIntel + Version Info: 0x00000000 + Feature Info: 0x00000000 + - Type: ModuleList + Modules: + - Base of Image: 0x0000000000400000 + Size of Image: 0x00002000 + Module Name: '/no/such/module.so' + CodeView Record: 4C45704200000000000000000000000000000000 +... diff --git a/lldb/test/API/commands/target/modules/replace/unplaceable.yaml b/lldb/test/API/commands/target/modules/replace/unplaceable.yaml new file mode 100644 index 0000000000000..e730082e5f593 --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/unplaceable.yaml @@ -0,0 +1,13 @@ +--- !ELF +FileHeader: + Class: ELFCLASS64 + Data: ELFDATA2LSB + Type: ET_DYN + Machine: EM_X86_64 +Sections: + - Name: .text + Type: SHT_PROGBITS + Flags: [ SHF_ALLOC, SHF_EXECINSTR ] + Address: 0x0000000000000200 + AddressAlign: 0x0000000000000004 + Content: 'C3' >From 2a4c39e18c9ab71bb81b74bcfe946c5114558c67 Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Thu, 6 Aug 2026 17:18:14 -0700 Subject: [PATCH 3/6] small fixes --- lldb/include/lldb/Target/DynamicLoader.h | 11 +++++++- lldb/source/Commands/CommandObjectTarget.cpp | 29 ++++++++++---------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lldb/include/lldb/Target/DynamicLoader.h b/lldb/include/lldb/Target/DynamicLoader.h index 779ff2d3a70be..214a54fd03fc3 100644 --- a/lldb/include/lldb/Target/DynamicLoader.h +++ b/lldb/include/lldb/Target/DynamicLoader.h @@ -210,6 +210,12 @@ class DynamicLoader : public PluginInterface { /// Inform the dynamic loader that Target::ReplaceModule() has swapped one /// module for another. /// + /// If this function returns a Status that is a success, the dynamic loader + /// will have removed all old section mappings for \a old_module_sp, and + /// loaded all sections for \a new_module_sp. If an error is returned from + /// this function, the target will unload all sections from \a old_module_sp + /// and not do anything to load the \a new_module_sp's sections. + /// /// \param[in] old_module_sp /// The module that was removed from the target. /// @@ -220,7 +226,10 @@ class DynamicLoader : public PluginInterface { /// An error if this loader cannot place the replacement correctly. virtual Status ReplaceModule(const lldb::ModuleSP &old_module_sp, const lldb::ModuleSP &new_module_sp) { - return Status(); + return Status::FromErrorStringWithFormatv( + "The {0} dynamic loader plug-in doesn't support replacing a module. " + "You can try starting your debug session again to use the new module.", + GetPluginName()); } /// Locates or creates a module given by \p file and updates/loads the diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp index 14e56998fd407..e6580acf762f1 100644 --- a/lldb/source/Commands/CommandObjectTarget.cpp +++ b/lldb/source/Commands/CommandObjectTarget.cpp @@ -3209,21 +3209,22 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { // Find the module the new file is meant to stand in for. Preferred order is // the path the user gave, then the UUID read out of the new file, then its // basename. - ModuleSP FindModuleToReplace(Target &target, const FileSpec &new_file_spec, - const UUID &new_uuid, + ModuleSP FindModuleToReplace(Target &target, + const ModuleSpec &new_module_spec, CommandReturnObject &result) { ModuleSpec search_spec; llvm::StringRef description; if (!m_options.m_old_path.empty()) { search_spec.GetFileSpec().SetPath(m_options.m_old_path); - description = "the given path"; - } else if (new_uuid.IsValid()) { - search_spec.GetUUID() = new_uuid; + description = "the specified path"; + } else if (new_module_spec.GetUUID().IsValid()) { + search_spec.GetUUID() = new_module_spec.GetUUID(); description = "a matching UUID"; } else { - search_spec.GetFileSpec().SetFilename(new_file_spec.GetFilename()); - description = "a matching name"; + search_spec.GetFileSpec().SetFilename( + new_module_spec.GetFileSpec().GetFilename()); + description = "a matching file basename"; } ModuleList matches; @@ -3233,12 +3234,13 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { // A file with a UUID that names nothing in the target is still worth // trying by name, the placeholder it should replace may have been built // without one. - if (m_options.m_old_path.empty() && new_uuid.IsValid()) { + if (m_options.m_old_path.empty() && new_module_spec.GetUUID().IsValid()) { ModuleSpec by_name; - by_name.GetFileSpec().SetFilename(new_file_spec.GetFilename()); + by_name.GetFileSpec().SetFilename( + new_module_spec.GetFileSpec().GetFilename()); target.GetImages().FindModules(by_name, matches); - description = - matches.IsEmpty() ? "a matching UUID or name" : "a matching name"; + description = matches.IsEmpty() ? "a matching UUID or file basename" + : "a matching file basename"; } if (matches.IsEmpty()) { result.AppendErrorWithFormatv( @@ -3310,16 +3312,15 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { } new_module_spec = matching_spec; } - const UUID &new_uuid = new_module_spec.GetUUID(); - ModuleSP old_module_sp = - FindModuleToReplace(*target, new_file_spec, new_uuid, result); + FindModuleToReplace(*target, new_module_spec, result); if (!old_module_sp) return; // Different UUIDs mean the new file is not the binary that ran, so the // symbols would not describe the memory the target has. const UUID &old_uuid = old_module_sp->GetUUID(); + const UUID &new_uuid = new_module_spec.GetUUID(); if (!m_options.m_force && old_uuid.IsValid() && new_uuid.IsValid() && old_uuid != new_uuid) { result.AppendErrorWithFormatv( >From 31091f7b700494b818f7c9d82e2b527127ea115e Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Fri, 7 Aug 2026 10:29:00 -0700 Subject: [PATCH 4/6] fix tests --- .../replace/TestTargetModulesReplace.py | 23 +++++++++++-------- .../target/modules/replace/replacement.yaml | 20 ++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 lldb/test/API/commands/target/modules/replace/replacement.yaml diff --git a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py index e0f9da8596766..e3c2aa93e9bdf 100644 --- a/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py +++ b/lldb/test/API/commands/target/modules/replace/TestTargetModulesReplace.py @@ -11,6 +11,7 @@ from lldbsuite.test.lldbtest import * +@skipIfWindows class TargetModulesReplaceTestCase(TestBase): NO_DEBUG_INFO_TESTCASE = True @@ -232,12 +233,14 @@ def test_errors(self): self.assertEqual(target.GetNumModules(), num_modules) self.assertTrue(target.FindModule(lldb.SBFileSpec(v1)).IsValid()) - @skipUnlessPlatform(["linux"]) def test_placeholder_without_uuid(self): """A placeholder with no UUID needs no --force, nothing can be compared.""" - v1, v2, v1_copy = self.build_and_get_paths() core = self.getBuildArtifact("no-uuid.dmp") self.yaml2obj("placeholder-no-uuid.yaml", core) + # The dump is x86_64, so the replacement comes from a yaml too rather + # than from this test's libraries, which follow the host architecture. + replacement = self.getBuildArtifact("replacement.so") + self.yaml2obj("replacement.yaml", replacement) target = self.dbg.CreateTarget("") self.assertTrue(target.LoadCore(core).IsValid()) @@ -247,17 +250,19 @@ def test_placeholder_without_uuid(self): base = self.base_load_address(placeholder, target) self.assertNotEqual(base, lldb.LLDB_INVALID_ADDRESS) - self.runCmd("target modules replace --old-path /no/such/module.so '%s'" % v1) + self.runCmd( + "target modules replace --old-path /no/such/module.so '%s'" % replacement + ) - new_module = target.FindModule(lldb.SBFileSpec(v1)) + new_module = target.FindModule(lldb.SBFileSpec(replacement)) self.assertTrue(new_module.IsValid()) self.assertEqual(self.base_load_address(new_module, target), base) - self.assertTrue(new_module.FindSymbol("only_in_v1").IsValid()) + self.assertFalse( + target.FindModule(lldb.SBFileSpec("/no/such/module.so")).IsValid() + ) - @skipUnlessPlatform(["linux"]) def test_failure_leaves_the_target_alone(self): """A replacement that cannot be placed is refused, and nothing is lost.""" - v1, v2, v1_copy = self.build_and_get_paths() core = self.getBuildArtifact("no-uuid.dmp") self.yaml2obj("placeholder-no-uuid.yaml", core) @@ -282,7 +287,6 @@ def test_failure_leaves_the_target_alone(self): "the module that could not be replaced is still in the target", ) - @skipIfWindows @skipIfRemote @skipUnlessPlatform(["linux"]) def test_core_file(self): @@ -315,7 +319,6 @@ def test_core_file(self): self.assertEqual(self.base_load_address(new_module, target), base) self.assertTrue(new_module.FindSymbol("only_in_v2").IsValid()) - @skipIfWindows @skipIfRemote def test_breakpoints_move_to_the_replacement(self): """Breakpoint locations are re-resolved into the new module.""" @@ -361,9 +364,9 @@ def test_breakpoints_move_to_the_replacement(self): # The symbol that only existed in the old variant goes pending. self.assertEqual(only_v1_bp.GetNumLocations(), 0) - @skipIfWindows @skipIfRemote @skipUnlessPlatform(["linux"]) + @skipIf(archs=no_match(["x86_64"])) def test_thread_local_storage_still_resolves(self): """The dynamic loader's per module state follows the replacement. diff --git a/lldb/test/API/commands/target/modules/replace/replacement.yaml b/lldb/test/API/commands/target/modules/replace/replacement.yaml new file mode 100644 index 0000000000000..a2273bef7e4c7 --- /dev/null +++ b/lldb/test/API/commands/target/modules/replace/replacement.yaml @@ -0,0 +1,20 @@ +--- !ELF +FileHeader: + Class: ELFCLASS64 + Data: ELFDATA2LSB + Type: ET_DYN + Machine: EM_X86_64 +Sections: + - Name: .text + Type: SHT_PROGBITS + Flags: [ SHF_ALLOC, SHF_EXECINSTR ] + Address: 0x0000000000000200 + AddressAlign: 0x0000000000000004 + Content: 'C3' +ProgramHeaders: + - Type: PT_LOAD + Flags: [ PF_X, PF_R ] + VAddr: 0x0000000000000000 + Align: 0x1000 + FirstSec: .text + LastSec: .text >From 5ee16703bd2f265aac6c8a0a2fa6278b86a7d3a6 Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Fri, 7 Aug 2026 17:07:47 -0700 Subject: [PATCH 5/6] Addressed comments --- lldb/source/Commands/CommandObjectTarget.cpp | 1 + .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp | 60 +++++++++---------- .../POSIX-DYLD/DynamicLoaderPOSIXDYLD.h | 18 ++++-- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp index e6580acf762f1..bd463bc3f4cf9 100644 --- a/lldb/source/Commands/CommandObjectTarget.cpp +++ b/lldb/source/Commands/CommandObjectTarget.cpp @@ -3314,6 +3314,7 @@ class CommandObjectTargetModulesReplace : public CommandObjectParsed { } ModuleSP old_module_sp = FindModuleToReplace(*target, new_module_spec, result); + // FindModuleToReplace() has already put the error into result. if (!old_module_sp) return; diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp index 6d8c6ba36b8b9..58597638d3507 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp @@ -192,9 +192,11 @@ void DynamicLoaderPOSIXDYLD::DidLaunch() { Status DynamicLoaderPOSIXDYLD::CanLoadImage() { return Status(); } void DynamicLoaderPOSIXDYLD::SetLoadedModule(const ModuleSP &module_sp, - addr_t link_map_addr) { + addr_t link_map_addr, + addr_t base_addr, + bool base_addr_is_offset) { llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex); - m_loaded_modules[module_sp] = link_map_addr; + m_loaded_modules[module_sp] = {link_map_addr, base_addr, base_addr_is_offset}; } void DynamicLoaderPOSIXDYLD::UnloadModule(const ModuleSP &module_sp) { @@ -202,8 +204,8 @@ void DynamicLoaderPOSIXDYLD::UnloadModule(const ModuleSP &module_sp) { m_loaded_modules.erase(module_sp); } -std::optional<lldb::addr_t> -DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const ModuleSP &module_sp) { +std::optional<DynamicLoaderPOSIXDYLD::LoadedModuleInfo> +DynamicLoaderPOSIXDYLD::GetLoadedModuleInfo(const ModuleSP &module_sp) { llvm::sys::ScopedReader lock(m_loaded_modules_rw_mutex); auto it = m_loaded_modules.find(module_sp); if (it != m_loaded_modules.end()) @@ -213,34 +215,32 @@ DynamicLoaderPOSIXDYLD::GetLoadedModuleLinkAddr(const ModuleSP &module_sp) { Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp, const ModuleSP &new_module_sp) { - // Where the old module was mapped, read before its sections go away. - addr_t base_addr = LLDB_INVALID_ADDRESS; - if (ObjectFile *object_file = old_module_sp->GetObjectFile()) { + // Load the replacement the same way the old module was loaded, which also + // carries the link map address across so its thread locals resolve. + LoadedModuleInfo info; + if (std::optional<LoadedModuleInfo> loaded = + GetLoadedModuleInfo(old_module_sp); + loaded && loaded->base_addr != LLDB_INVALID_ADDRESS) { + info = *loaded; + } else if (ObjectFile *object_file = old_module_sp->GetObjectFile()) { + // Post mortem processes build their module list from the core file rather + // than by walking the rendezvous, so there is nothing recorded here for + // them. Fall back to where the module says it is. Address base = object_file->GetBaseAddress(); if (base.IsValid()) - base_addr = base.GetLoadAddress(&m_process->GetTarget()); + info.base_addr = base.GetLoadAddress(&m_process->GetTarget()); + if (loaded) + info.link_map_addr = loaded->link_map_addr; } - if (base_addr == LLDB_INVALID_ADDRESS) + + if (info.base_addr == LLDB_INVALID_ADDRESS) return Status::FromErrorStringWithFormatv( "'{0}' is not loaded at a known address", old_module_sp->GetFileSpec()); - addr_t link_map_addr = LLDB_INVALID_ADDRESS; - { - // The link map address is what thread local lookups are found through, and - // it is keyed by module, so it has to be moved onto the replacement. - llvm::sys::ScopedWriter lock(m_loaded_modules_rw_mutex); - auto it = m_loaded_modules.find(old_module_sp); - if (it != m_loaded_modules.end()) { - link_map_addr = it->second; - m_loaded_modules.erase(it); - } - } - - UnloadSections(old_module_sp); - // Images are mapped in one piece here, so the recorded address is where the - // replacement goes, not an offset to slide it by. - UpdateLoadedSections(new_module_sp, link_map_addr, base_addr, - /*base_addr_is_offset=*/false); + UnloadModule(old_module_sp); + UnloadSectionsCommon(old_module_sp); + UpdateLoadedSections(new_module_sp, info.link_map_addr, info.base_addr, + info.base_addr_is_offset); return Status(); } @@ -248,7 +248,7 @@ void DynamicLoaderPOSIXDYLD::UpdateLoadedSections(ModuleSP module, addr_t link_map_addr, addr_t base_addr, bool base_addr_is_offset) { - SetLoadedModule(module, link_map_addr); + SetLoadedModule(module, link_map_addr, base_addr, base_addr_is_offset); UpdateLoadedSectionsCommon(module, base_addr, base_addr_is_offset); } @@ -874,8 +874,8 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp, const lldb::ThreadSP thread, lldb::addr_t tls_file_addr) { Log *log = GetLog(LLDBLog::DynamicLoader); - std::optional<addr_t> link_map_addr_opt = GetLoadedModuleLinkAddr(module_sp); - if (!link_map_addr_opt.has_value()) { + std::optional<LoadedModuleInfo> info = GetLoadedModuleInfo(module_sp); + if (!info.has_value()) { LLDB_LOG( log, "GetThreadLocalData error: module({0}) not found in loaded modules", @@ -883,7 +883,7 @@ DynamicLoaderPOSIXDYLD::GetThreadLocalData(const lldb::ModuleSP module_sp, return LLDB_INVALID_ADDRESS; } - addr_t link_map = link_map_addr_opt.value(); + addr_t link_map = info->link_map_addr; if (link_map == LLDB_INVALID_ADDRESS || link_map == 0) { LLDB_LOGF(log, "GetThreadLocalData error: invalid link map address=0x%" PRIx64, diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h index 53f68975e3efc..559a551cd5f73 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.h @@ -181,18 +181,28 @@ class DynamicLoaderPOSIXDYLD : public lldb_private::DynamicLoader { const DynamicLoaderPOSIXDYLD & operator=(const DynamicLoaderPOSIXDYLD &) = delete; + /// What each module was loaded with, enough to load another module the same + /// way. + struct LoadedModuleInfo { + lldb::addr_t link_map_addr = LLDB_INVALID_ADDRESS; + lldb::addr_t base_addr = LLDB_INVALID_ADDRESS; + bool base_addr_is_offset = false; + }; + /// Loaded module list. (link map for each module) /// This may be accessed in a multi-threaded context. Use the accessor methods /// to access `m_loaded_modules` safely. - std::map<lldb::ModuleWP, lldb::addr_t, std::owner_less<lldb::ModuleWP>> + std::map<lldb::ModuleWP, LoadedModuleInfo, std::owner_less<lldb::ModuleWP>> m_loaded_modules; llvm::sys::RWMutex m_loaded_modules_rw_mutex; void SetLoadedModule(const lldb::ModuleSP &module_sp, - lldb::addr_t link_map_addr); + lldb::addr_t link_map_addr, + lldb::addr_t base_addr = LLDB_INVALID_ADDRESS, + bool base_addr_is_offset = false); void UnloadModule(const lldb::ModuleSP &module_sp); - std::optional<lldb::addr_t> - GetLoadedModuleLinkAddr(const lldb::ModuleSP &module_sp); + std::optional<LoadedModuleInfo> + GetLoadedModuleInfo(const lldb::ModuleSP &module_sp); }; #endif // LLDB_SOURCE_PLUGINS_DYNAMICLOADER_POSIX_DYLD_DYNAMICLOADERPOSIXDYLD_H >From cf301c1774a66e182f606d2e69ba24ebbde981c2 Mon Sep 17 00:00:00 2001 From: Bar Soloveychik <[email protected]> Date: Mon, 10 Aug 2026 16:36:31 -0700 Subject: [PATCH 6/6] combined the functions --- .../DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp index 58597638d3507..9bc40aed2bd17 100644 --- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp +++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp @@ -237,8 +237,7 @@ Status DynamicLoaderPOSIXDYLD::ReplaceModule(const ModuleSP &old_module_sp, return Status::FromErrorStringWithFormatv( "'{0}' is not loaded at a known address", old_module_sp->GetFileSpec()); - UnloadModule(old_module_sp); - UnloadSectionsCommon(old_module_sp); + UnloadSections(old_module_sp); UpdateLoadedSections(new_module_sp, info.link_map_addr, info.base_addr, info.base_addr_is_offset); return Status(); _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
