[Lldb-commits] [lldb] [lldb] Fix 'target modules add' not sending Z0 breakpoint packets (PR #186223)
satyanarayana reddy janga via lldb-commits
lldb-commits at lists.llvm.org
Thu Mar 12 12:52:25 PDT 2026
https://github.com/satyajanga created https://github.com/llvm/llvm-project/pull/186223
When a module was added via 'target modules add', breakpoints would resolve (BreakpointLocation created) but no Z0 gdb-remote packet was sent to actually set the breakpoint on the remote target.
The root cause: 'target modules add' calls Target::GetOrCreateModule with notify=true, which triggers NotifyModuleAdded -> ModulesDidLoad -> UpdateBreakpoints -> ResolveBreakpointSite. However, the module's section load addresses were never registered in the SectionLoadList, so Address::GetLoadAddress() returned LLDB_INVALID_ADDRESS during breakpoint site resolution, preventing BreakpointSite creation and the corresponding Z0 packet.
In contrast, 'target modules load --slide 0' explicitly called Module::SetLoadAddress to populate the SectionLoadList, which is why it worked as a workaround.
Fix this by adding a DynamicLoader::ModuleAdded() virtual hook that is called from Target::NotifyModuleAdded before ModulesDidLoad. DynamicLoaderStatic overrides this to set section load addresses from file addresses (slide 0) for modules that have no load addresses yet. This ensures the SectionLoadList is populated before breakpoint resolution runs, so BreakpointSites are created and Z0 packets are sent.
The existing load-address-setting logic in LoadAllImagesAtFileAddresses is extracted into a shared helper (SetModuleLoadAddressFromFileAddresses) used by both the DidAttach/DidLaunch path and the new ModuleAdded hook.
>From ca2a7b6ebd4bb521aff80111d0f59340551a33a2 Mon Sep 17 00:00:00 2001
From: satyajanga <satyajanga at fb.com>
Date: Thu, 12 Mar 2026 12:47:21 -0700
Subject: [PATCH] [lldb] Fix 'target modules add' not sending Z0 breakpoint
packets
When a module was added via 'target modules add', breakpoints would
resolve (BreakpointLocation created) but no Z0 gdb-remote packet was
sent to actually set the breakpoint on the remote target.
The root cause: 'target modules add' calls Target::GetOrCreateModule
with notify=true, which triggers NotifyModuleAdded -> ModulesDidLoad ->
UpdateBreakpoints -> ResolveBreakpointSite. However, the module's
section load addresses were never registered in the SectionLoadList,
so Address::GetLoadAddress() returned LLDB_INVALID_ADDRESS during
breakpoint site resolution, preventing BreakpointSite creation and
the corresponding Z0 packet.
In contrast, 'target modules load --slide 0' explicitly called
Module::SetLoadAddress to populate the SectionLoadList, which is why
it worked as a workaround.
Fix this by adding a DynamicLoader::ModuleAdded() virtual hook that
is called from Target::NotifyModuleAdded before ModulesDidLoad.
DynamicLoaderStatic overrides this to set section load addresses from
file addresses (slide 0) for modules that have no load addresses yet.
This ensures the SectionLoadList is populated before breakpoint
resolution runs, so BreakpointSites are created and Z0 packets are
sent.
The existing load-address-setting logic in LoadAllImagesAtFileAddresses
is extracted into a shared helper (SetModuleLoadAddressFromFileAddresses)
used by both the DidAttach/DidLaunch path and the new ModuleAdded hook.
---
lldb/include/lldb/Target/DynamicLoader.h | 14 +++
.../Static/DynamicLoaderStatic.cpp | 81 +++++++++--------
.../Static/DynamicLoaderStatic.h | 6 ++
lldb/source/Target/Target.cpp | 10 +++
.../TestTargetModulesAddBreakpoint.py | 87 +++++++++++++++++++
.../gdb_remote_client/module_with_func.yaml | 28 ++++++
6 files changed, 192 insertions(+), 34 deletions(-)
create mode 100644 lldb/test/API/functionalities/gdb_remote_client/TestTargetModulesAddBreakpoint.py
create mode 100644 lldb/test/API/functionalities/gdb_remote_client/module_with_func.yaml
diff --git a/lldb/include/lldb/Target/DynamicLoader.h b/lldb/include/lldb/Target/DynamicLoader.h
index d1d832f759748..5829431140a7d 100644
--- a/lldb/include/lldb/Target/DynamicLoader.h
+++ b/lldb/include/lldb/Target/DynamicLoader.h
@@ -330,6 +330,20 @@ class DynamicLoader : public PluginInterface {
return false;
}
+ /// Called when a module has been added to the target, before
+ /// Target::ModulesDidLoad is invoked. This gives the dynamic loader a
+ /// chance to set section load addresses for newly added modules so that
+ /// breakpoints can resolve to valid load addresses during the subsequent
+ /// ModulesDidLoad notification.
+ ///
+ /// The default implementation does nothing. Subclasses like
+ /// DynamicLoaderStatic override this to set load addresses from file
+ /// addresses for modules that have no load addresses yet.
+ ///
+ /// \param[in] module_sp
+ /// The module that was just added to the target.
+ virtual void ModuleAdded(const lldb::ModuleSP &module_sp) {}
+
/// Return whether the dynamic loader is fully initialized and it's safe to
/// call its APIs.
///
diff --git a/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.cpp b/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.cpp
index 643c9653f26ec..9e1923290beff 100644
--- a/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.cpp
+++ b/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.cpp
@@ -76,6 +76,50 @@ void DynamicLoaderStatic::DidAttach() { LoadAllImagesAtFileAddresses(); }
/// attaching to a process.
void DynamicLoaderStatic::DidLaunch() { LoadAllImagesAtFileAddresses(); }
+bool DynamicLoaderStatic::SetModuleLoadAddressFromFileAddresses(
+ const ModuleSP &module_sp) {
+ if (!module_sp)
+ return false;
+
+ Target &target = m_process->GetTarget();
+
+ // If this module has a section with a load address set in
+ // the target, assume all necessary work is already done. There
+ // may be sections without a load address set intentionally
+ // and we don't want to mutate that.
+ // For a module with no load addresses set, set the load addresses
+ // to slide == 0, the same as the file addresses, in the target.
+ ObjectFile *image_object_file = module_sp->GetObjectFile();
+ if (image_object_file) {
+ SectionList *section_list = image_object_file->GetSectionList();
+ if (section_list) {
+ const size_t num_sections = section_list->GetSize();
+ for (size_t sect_idx = 0; sect_idx < num_sections; ++sect_idx) {
+ SectionSP section_sp(section_list->GetSectionAtIndex(sect_idx));
+ if (section_sp) {
+ if (target.GetSectionLoadAddress(section_sp) !=
+ LLDB_INVALID_ADDRESS) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ bool changed = false;
+ module_sp->SetLoadAddress(target, 0, true /*value_is_offset*/, changed);
+ return changed;
+}
+
+void DynamicLoaderStatic::ModuleAdded(const ModuleSP &module_sp) {
+ // When a module is added to the target after attach/launch, set its
+ // section load addresses from file addresses (slide 0) if no load
+ // addresses have been set yet. This ensures breakpoints can resolve
+ // to valid load addresses during the subsequent ModulesDidLoad
+ // notification, enabling Z0 packets to be sent for remote targets.
+ SetModuleLoadAddressFromFileAddresses(module_sp);
+}
+
void DynamicLoaderStatic::LoadAllImagesAtFileAddresses() {
const ModuleList &module_list = m_process->GetTarget().GetImages();
@@ -84,43 +128,12 @@ void DynamicLoaderStatic::LoadAllImagesAtFileAddresses() {
// Disable JIT for static dynamic loader targets
m_process->SetCanJIT(false);
- Target &target = m_process->GetTarget();
for (ModuleSP module_sp : module_list.Modules()) {
- if (module_sp) {
- bool changed = false;
- bool no_load_addresses = true;
- // If this module has a section with a load address set in
- // the target, assume all necessary work is already done. There
- // may be sections without a load address set intentionally
- // and we don't want to mutate that.
- // For a module with no load addresses set, set the load addresses
- // to slide == 0, the same as the file addresses, in the target.
- ObjectFile *image_object_file = module_sp->GetObjectFile();
- if (image_object_file) {
- SectionList *section_list = image_object_file->GetSectionList();
- if (section_list) {
- const size_t num_sections = section_list->GetSize();
- for (size_t sect_idx = 0; sect_idx < num_sections; ++sect_idx) {
- SectionSP section_sp(section_list->GetSectionAtIndex(sect_idx));
- if (section_sp) {
- if (target.GetSectionLoadAddress(section_sp) !=
- LLDB_INVALID_ADDRESS) {
- no_load_addresses = false;
- break;
- }
- }
- }
- }
- }
- if (no_load_addresses)
- module_sp->SetLoadAddress(target, 0, true /*value_is_offset*/, changed);
-
- if (changed)
- loaded_module_list.AppendIfNeeded(module_sp);
- }
+ if (SetModuleLoadAddressFromFileAddresses(module_sp))
+ loaded_module_list.AppendIfNeeded(module_sp);
}
- target.ModulesDidLoad(loaded_module_list);
+ m_process->GetTarget().ModulesDidLoad(loaded_module_list);
}
ThreadPlanSP
diff --git a/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.h b/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.h
index dac19dcd38d7d..ffe76c109d3d3 100644
--- a/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.h
+++ b/lldb/source/Plugins/DynamicLoader/Static/DynamicLoaderStatic.h
@@ -43,11 +43,17 @@ class DynamicLoaderStatic : public lldb_private::DynamicLoader {
lldb_private::Status CanLoadImage() override;
+ void ModuleAdded(const lldb::ModuleSP &module_sp) override;
+
// PluginInterface protocol
llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
private:
void LoadAllImagesAtFileAddresses();
+
+ /// Set section load addresses from file addresses for a module that
+ /// has no load addresses set yet. Returns true if any addresses changed.
+ bool SetModuleLoadAddressFromFileAddresses(const lldb::ModuleSP &module_sp);
};
#endif // LLDB_SOURCE_PLUGINS_DYNAMICLOADER_STATIC_DYNAMICLOADERSTATIC_H
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index f1560a1e74259..75feea3e73bb0 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -46,6 +46,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"
@@ -1824,6 +1825,15 @@ void Target::NotifyModuleAdded(const ModuleList &module_list,
const ModuleSP &module_sp) {
// A module is being added to this target for the first time
if (m_valid) {
+ // Give the dynamic loader a chance to set section load addresses
+ // before ModulesDidLoad triggers breakpoint resolution. For example,
+ // DynamicLoaderStatic uses file addresses as load addresses so that
+ // breakpoints in statically-addressed modules can resolve immediately.
+ if (m_process_sp) {
+ if (DynamicLoader *dyld = m_process_sp->GetDynamicLoader())
+ dyld->ModuleAdded(module_sp);
+ }
+
ModuleList my_module_list;
my_module_list.Append(module_sp);
ModulesDidLoad(my_module_list);
diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestTargetModulesAddBreakpoint.py b/lldb/test/API/functionalities/gdb_remote_client/TestTargetModulesAddBreakpoint.py
new file mode 100644
index 0000000000000..4b2a90c392091
--- /dev/null
+++ b/lldb/test/API/functionalities/gdb_remote_client/TestTargetModulesAddBreakpoint.py
@@ -0,0 +1,87 @@
+"""
+Test that 'target modules add' on a DynamicLoaderStatic target sets section
+load addresses from file addresses, enabling breakpoint sites and Z0 packets
+for remote targets.
+"""
+
+import lldb
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test.decorators import *
+from lldbsuite.test.gdbclientutils import *
+from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
+
+
+class TestTargetModulesAddBreakpoint(GDBRemoteTestBase):
+ class Responder(MockGDBServerResponder):
+ def setBreakpoint(self, packet):
+ return "OK"
+
+ def haltReason(self):
+ return "T05thread:01;"
+
+ def qfThreadInfo(self):
+ return "m1"
+
+ def qsThreadInfo(self):
+ return "l"
+
+ def test_breakpoint_after_modules_add(self):
+ """Test that setting a breakpoint after 'target modules add' sends Z0."""
+ self.server.responder = self.Responder()
+
+ target = self.createTarget("a.yaml")
+ process = self.connect(target)
+
+ # Add a second module containing 'func' at 0x4000.
+ module_path = self.getBuildArtifact("module_with_func")
+ self.yaml2obj("module_with_func.yaml", module_path)
+ self.runCmd("target modules add " + module_path)
+
+ # Set a breakpoint on the symbol in the added module.
+ bkpt = target.BreakpointCreateByName("func")
+ self.assertTrue(bkpt.IsValid())
+ self.assertEqual(bkpt.GetNumLocations(), 1)
+
+ # The breakpoint location should have a valid load address.
+ bp_loc = bkpt.GetLocationAtIndex(0)
+ self.assertEqual(bp_loc.GetLoadAddress(), 0x4000)
+
+ # Verify that a Z0 packet was sent for the breakpoint.
+ received = self.server.responder.packetLog.get_received()
+ Z0_packets = [p for p in received if p.startswith("Z0,")]
+ self.assertGreaterEqual(
+ len(Z0_packets), 1, "Expected Z0 packet but none was sent"
+ )
+ self.assertIn("Z0,4000,1", Z0_packets)
+
+ def test_breakpoint_before_modules_add(self):
+ """Test that a pending breakpoint resolves and sends Z0 when a module
+ is added via 'target modules add'."""
+ self.server.responder = self.Responder()
+
+ target = self.createTarget("a.yaml")
+ process = self.connect(target)
+
+ # Set a breakpoint before adding the module. It should be unresolved.
+ bkpt = target.BreakpointCreateByName("func")
+ self.assertTrue(bkpt.IsValid())
+ self.assertEqual(bkpt.GetNumLocations(), 0)
+
+ # Now add the module containing 'func'. DynamicLoaderStatic should
+ # set load addresses, and ModulesDidLoad should resolve the breakpoint.
+ module_path = self.getBuildArtifact("module_with_func")
+ self.yaml2obj("module_with_func.yaml", module_path)
+ self.runCmd("target modules add " + module_path)
+
+ # The breakpoint should now be resolved.
+ self.assertEqual(bkpt.GetNumLocations(), 1)
+ bp_loc = bkpt.GetLocationAtIndex(0)
+ self.assertEqual(bp_loc.GetLoadAddress(), 0x4000)
+
+ # Verify that a Z0 packet was sent.
+ received = self.server.responder.packetLog.get_received()
+ Z0_packets = [p for p in received if p.startswith("Z0,")]
+ self.assertGreaterEqual(
+ len(Z0_packets), 1, "Expected Z0 packet but none was sent"
+ )
+ self.assertIn("Z0,4000,1", Z0_packets)
diff --git a/lldb/test/API/functionalities/gdb_remote_client/module_with_func.yaml b/lldb/test/API/functionalities/gdb_remote_client/module_with_func.yaml
new file mode 100644
index 0000000000000..97e9ea97d4b8d
--- /dev/null
+++ b/lldb/test/API/functionalities/gdb_remote_client/module_with_func.yaml
@@ -0,0 +1,28 @@
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_EXEC
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x4000
+ AddressAlign: 0x4
+ Content: "c3c3c3c3"
+Symbols:
+ - Name: func
+ Type: STT_FUNC
+ Section: .text
+ Binding: STB_GLOBAL
+ Value: 0x4000
+ Size: 0x4
+ProgramHeaders:
+ - Type: PT_LOAD
+ Flags: [ PF_X, PF_R ]
+ VAddr: 0x4000
+ PAddr: 0x4000
+ Align: 0x4
+ FirstSec: .text
+ LastSec: .text
More information about the lldb-commits
mailing list