[Lldb-commits] [lldb] Add support for resolving addresses in "target hook". (PR #212831)

Greg Clayton via lldb-commits lldb-commits at lists.llvm.org
Fri Sep 11 13:49:03 PDT 2026


https://github.com/clayborg updated https://github.com/llvm/llvm-project/pull/212831

>From 87dfbe0084248d0a74f1ea9a06daad963d584fc5 Mon Sep 17 00:00:00 2001
From: Greg Clayton <clayborg at gmail.com>
Date: Mon, 27 Jul 2026 10:15:34 -0700
Subject: [PATCH 1/2] Add support for resolving addresses in "target hook".

---
 lldb/bindings/python/python-swigsafecast.swig |   4 +
 lldb/bindings/python/python-wrapper.swig      |  16 ++-
 .../python/templates/scripted_hook.py         |  42 ++++++
 lldb/include/lldb/API/SBAddress.h             |   9 ++
 .../Interfaces/ScriptedHookInterface.h        |  30 +++-
 .../lldb/Interpreter/ScriptInterpreter.h      |   2 +
 lldb/include/lldb/Target/Target.h             |  14 +-
 lldb/source/Commands/CommandObjectTarget.cpp  |   9 +-
 lldb/source/Interpreter/ScriptInterpreter.cpp |   9 ++
 .../ScriptedHookPythonInterface.cpp           |  27 ++++
 .../Interfaces/ScriptedHookPythonInterface.h  |   5 +-
 .../Interfaces/ScriptedPythonInterface.cpp    |  14 ++
 .../Interfaces/ScriptedPythonInterface.h      |   8 ++
 .../Python/SWIGPythonBridge.h                 |   2 +
 lldb/source/Target/Target.cpp                 |  48 ++++++-
 .../target/module-hook/resolve-addr/Makefile  |   2 +
 .../resolve-addr/TestModuleHookResolveAddr.py | 128 ++++++++++++++++++
 .../module-hook/resolve-addr/addrhook.py      |  94 +++++++++++++
 .../target/module-hook/resolve-addr/main.cpp  |   4 +
 19 files changed, 455 insertions(+), 12 deletions(-)
 create mode 100644 lldb/test/API/commands/target/module-hook/resolve-addr/Makefile
 create mode 100644 lldb/test/API/commands/target/module-hook/resolve-addr/TestModuleHookResolveAddr.py
 create mode 100644 lldb/test/API/commands/target/module-hook/resolve-addr/addrhook.py
 create mode 100644 lldb/test/API/commands/target/module-hook/resolve-addr/main.cpp

diff --git a/lldb/bindings/python/python-swigsafecast.swig b/lldb/bindings/python/python-swigsafecast.swig
index a86dc44ce4106..ec3b36a7b1ec7 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -59,6 +59,10 @@ PythonObject SWIGBridge::ToSWIGWrapper(const StructuredDataImpl &data_impl) {
   return ToSWIGWrapper(std::unique_ptr<lldb::SBStructuredData>(new lldb::SBStructuredData(data_impl)));
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(const Address &addr) {
+  return ToSWIGHelper(new lldb::SBAddress(addr), SWIGTYPE_p_lldb__SBAddress);
+}
+
 PythonObject SWIGBridge::ToSWIGWrapper(lldb::ThreadSP thread_sp) {
   return ToSWIGHelper(new lldb::SBThread(std::move(thread_sp)),
                       SWIGTYPE_p_lldb__SBThread);
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 2392737402e20..e4b8fd3696d05 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -533,6 +533,18 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBSymbolContext(PyObjec
   return sb_ptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBAddress(PyObject * data) {
+  lldb::SBAddress *sb_ptr = nullptr;
+
+//   int valid_cast =
+//       SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBAddress, 0);
+
+//   if (valid_cast == -1)
+//     return NULL;
+
+//   return sb_ptr;
+// }
+
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBValue(PyObject * data) {
   lldb::SBValue *sb_ptr = NULL;
 
@@ -605,8 +617,8 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBTarget(PyObject *
   if (valid_cast == -1)
     return NULL;
 
-  return sb_ptr;                                                                                                                                                                                  
-}                                                                                                                                                                                                 
+  return sb_ptr;
+}
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
     const char *python_function_name, const char *session_dictionary_name,
     lldb::DebuggerSP debugger, const char *args,
diff --git a/lldb/examples/python/templates/scripted_hook.py b/lldb/examples/python/templates/scripted_hook.py
index 0725c2c9c5d91..4871e04e50000 100644
--- a/lldb/examples/python/templates/scripted_hook.py
+++ b/lldb/examples/python/templates/scripted_hook.py
@@ -12,6 +12,11 @@ class ScriptedHook(metaclass=ABCMeta):
     always be attached as a stop-hook; `handle_module_loaded` and
     `handle_module_unloaded` are optional and only called for hooks
     registered via `target hook add -P`.
+
+    Methods in this class hook into target events and also provide additional
+    hooks to add functionality to the target. Target events are sent to methods
+    whose names start with "handle_". Methods that extend target functionality
+    have names that start with "do_".
     """
 
     target: lldb.SBTarget
@@ -32,6 +37,9 @@ def __init__(self, target: lldb.SBTarget, args: lldb.SBStructuredData):
     def handle_module_loaded(self, stream: lldb.SBStream) -> None:
         """Called whenever a module is loaded into the target.
 
+        Type:
+            Target event notificaton.
+
         Args:
             stream (lldb.SBStream): The stream to which the hook can write
                 output that will be reported to the user.
@@ -41,6 +49,9 @@ def handle_module_loaded(self, stream: lldb.SBStream) -> None:
     def handle_module_unloaded(self, stream: lldb.SBStream) -> None:
         """Called whenever a module is unloaded from the target.
 
+        Type:
+            Target event notificaton.
+
         Args:
             stream (lldb.SBStream): The stream to which the hook can write
                 output that will be reported to the user.
@@ -54,6 +65,9 @@ def handle_stop(
         """Called whenever the process stops, before control is returned to
         the user.
 
+        Type:
+            Target event notificaton.
+
         Args:
             exe_ctx (lldb.SBExecutionContext): The execution context at the
                 point of the stop.
@@ -65,3 +79,31 @@ def handle_stop(
             returned to the user, `False` if the process should keep running.
         """
         pass
+
+
+    def do_resolve_addr(
+        self, load_addr: int, stream: lldb.SBStream) -> lldb.SBAddress:
+        """Called whenever the target is not able to resolve a load address to
+        a section offset address.
+
+        Type:
+            Target functionality extension.
+
+
+        Clients can implement a JIT loader plugin using this method. Anytime the
+        target fails to resolve an address, this method will be called. The
+        function can find the file for the module that contains the address,
+        load it into the target, and return the resolved address. If the
+        address cannot be resolved, return a default construct lldb.SBAddress.
+
+        Args:
+            load_addr (int): The load address to attempt to resolve.
+            stream (lldb.SBStream): The stream to which the hook can write
+                output that will be reported to the user.
+
+        Returns:
+            lldb.SBAddress: If the address was resolved, return a SBAddress
+            that has been resolved to a section offset address, or return a
+            default constructed SBAddress if the address could not be resolved.
+        """
+        pass
diff --git a/lldb/include/lldb/API/SBAddress.h b/lldb/include/lldb/API/SBAddress.h
index 430dad4862dbf..bb490ee9dfedd 100644
--- a/lldb/include/lldb/API/SBAddress.h
+++ b/lldb/include/lldb/API/SBAddress.h
@@ -12,6 +12,12 @@
 #include "lldb/API/SBDefines.h"
 #include "lldb/API/SBModule.h"
 
+namespace lldb_private {
+namespace python {
+class SWIGBridge;
+}
+} // namespace lldb_private
+
 namespace lldb {
 
 class LLDB_API SBAddress {
@@ -104,6 +110,9 @@ class LLDB_API SBAddress {
   friend class SBValue;
   friend class SBQueueItem;
 
+  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::python::SWIGBridge;
+
   lldb_private::Address *operator->();
 
   const lldb_private::Address *operator->() const;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
index 54d335122300d..e0d048e756905 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
@@ -10,7 +10,7 @@
 #define LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
 
 #include "lldb/lldb-private.h"
-
+#include "lldb/Core/Address.h"
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
@@ -18,12 +18,16 @@ class ScriptedHookInterface : public ScriptedInterface {
 public:
   /// Describes which hook callback methods the Python class implements.
   struct SupportedHookMethods {
+    // Target event callbacks.
     bool handle_module_loaded = false;
     bool handle_module_unloaded = false;
     bool handle_stop = false;
+    // Target functionality extensions.
+    bool do_resolve_addr = false;
 
     bool any() const {
-      return handle_module_loaded || handle_module_unloaded || handle_stop;
+      return handle_module_loaded || handle_module_unloaded || handle_stop ||
+          do_resolve_addr;
     }
   };
 
@@ -47,6 +51,28 @@ class ScriptedHookInterface : public ScriptedInterface {
                                           lldb::StreamSP &output_sp) {
     return true;
   }
+
+  /// Called when the target tried to resolve an address but wasn't able to
+  /// resolve it to an object file section. This allows plug-ins to resolve
+  /// an address on demand. JIT plug-ins can be completely implemented using
+  /// ScriptedHookInterface plug-ins and can lazily load the JIT'ed information
+  /// as needed instead of setting breakpoints
+  ///
+  /// \param[in] load_addr
+  ///   The load address to resolve.
+  ///
+  /// \param[out] addr
+  ///   The section offset address that was resolved if \a true is returned.
+  ///
+  /// \param[in] output_sp
+  ///   An output stream to use for logging.
+  ///
+  /// \return
+  ///   True if the address was resolved, false otherwise.
+  virtual std::optional<Address>
+  HandleResolveAddress(lldb::addr_t load_addr, lldb::StreamSP &output_sp) {
+    return std::nullopt;
+  }
 };
 } // namespace lldb_private
 
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 0e65cb4b8ac4a..519155cffd360 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -652,6 +652,8 @@ class ScriptInterpreter : public PluginInterface {
   SymbolContext
   GetOpaqueTypeFromSBSymbolContext(const lldb::SBSymbolContext &sym_ctx) const;
 
+  Address GetOpaqueTypeFromSBAddress(const lldb::SBAddress &addr) const;
+
   lldb::BreakpointSP
   GetOpaqueTypeFromSBBreakpoint(const lldb::SBBreakpoint &breakpoint) const;
 
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index fb43f432a08da..4062c123abb45 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1769,6 +1769,7 @@ class Target : public std::enable_shared_from_this<Target>,
       kModulesLoaded = (1u << 0),
       kModulesUnloaded = (1u << 1),
       kProcessStop = (1u << 2),
+      kResolveAddress = (1u << 3)
     };
 
     lldb::TargetSP &GetTarget() { return m_target_sp; }
@@ -1826,6 +1827,11 @@ class Target : public std::enable_shared_from_this<Target>,
       return StopHook::StopHookResult::NoPreference;
     }
 
+    virtual std::optional<Address> HandleResolveAddress(lldb::addr_t load_addr, 
+                                                        lldb::StreamSP &output_sp) {
+      return std::nullopt;
+    }
+
     virtual void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
 
   protected:
@@ -1896,7 +1902,9 @@ class Target : public std::enable_shared_from_this<Target>,
     void HandleModuleUnloaded(lldb::StreamSP output) override;
     StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
                                         lldb::StreamSP output) override;
-
+    std::optional<Address> 
+    HandleResolveAddress(lldb::addr_t load_addr, 
+                         lldb::StreamSP &output_sp) override;
     Status SetScriptCallback(const ScriptedMetadata &scripted_metadata);
 
   private:
@@ -1948,6 +1956,10 @@ class Target : public std::enable_shared_from_this<Target>,
   // control over the process for the first time.
   bool RunStopHooks(bool at_initial_stop = false);
 
+  /// Runs the resolve address hooks that have been registered with this
+  /// target. Returns true if the address was resolved.
+  std::optional<Address> RunResolveAddressHooks(lldb::addr_t load_addr);
+
   bool SetSuppresStopHooks(bool suppress) {
     bool old_value = m_suppress_stop_hooks;
     m_suppress_stop_hooks = suppress;
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index f77e87ad43aa2..31b1a22a84294 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5127,8 +5127,8 @@ class CommandObjectTargetStopHookDelete : public CommandObjectParsed {
 Deletes the stop hook by index.
 
 At any given stop, all enabled stop hooks that pass the stop filter will
-get a chance to run.  That means if one stop-hook deletes another stop hook 
-while executing, the deleted stop hook will still fire for the stop at which 
+get a chance to run.  That means if one stop-hook deletes another stop hook
+while executing, the deleted stop hook will still fire for the stop at which
 it was deleted.
         )");
     AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
@@ -5557,7 +5557,8 @@ Python class hooks:
             pass
         def handle_stop(self, exe_ctx, stream):
             return True  # True = should_stop, False = continue
-
+        def do_resolve_addr(self, load_addr, stream):
+            return lldb.SBAddress()  # Invalid defailt constructed address.
 Filter options:
 ---------------
   Filters (-s, -f, -l, -e, -c, -n, -x, -t, -T, -q) restrict when the hook
@@ -5913,6 +5914,8 @@ Valid trigger names: load, unload, stop.
       return Target::Hook::kModulesUnloaded;
     if (name == "stop")
       return Target::Hook::kProcessStop;
+    if (name == "resolve-addr")
+      return Target::Hook::kResolveAddress;
     return 0;
   }
 
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 4f6095d097d10..2be78ad5f1511 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -7,6 +7,8 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/API/SBAddress.h"
+#include "lldb/Core/Address.h"
 #include "lldb/Core/Debugger.h"
 #include "lldb/Host/ConnectionFileDescriptor.h"
 #include "lldb/Host/Pipe.h"
@@ -152,6 +154,13 @@ SymbolContext ScriptInterpreter::GetOpaqueTypeFromSBSymbolContext(
   return {};
 }
 
+Address ScriptInterpreter::GetOpaqueTypeFromSBAddress(
+    const lldb::SBAddress &sb_addr) const {
+  if (sb_addr.m_opaque_up)
+    return *sb_addr.m_opaque_up;
+  return {};
+}
+
 std::optional<lldb_private::MemoryRegionInfo>
 ScriptInterpreter::GetOpaqueTypeFromSBMemoryRegionInfo(
     const lldb::SBMemoryRegionInfo &mem_region) const {
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
index df4146657c53e..71155e168f48b 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
@@ -49,6 +49,8 @@ ScriptedHookPythonInterface::GetSupportedMethods() {
   methods.handle_module_unloaded =
       implementor.HasAttribute("handle_module_unloaded");
   methods.handle_stop = implementor.HasAttribute("handle_stop");
+  methods.do_resolve_addr =
+      implementor.HasAttribute("do_resolve_addr");
   return methods;
 }
 
@@ -64,12 +66,20 @@ void ScriptedHookPythonInterface::HandleModuleLoaded(
     lldb::StreamSP &output_sp) {
   Status error;
   Dispatch("handle_module_loaded", error, output_sp);
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "handle_module_loaded failed: {0}",
+             error.AsCString());
+  }
 }
 
 void ScriptedHookPythonInterface::HandleModuleUnloaded(
     lldb::StreamSP &output_sp) {
   Status error;
   Dispatch("handle_module_unloaded", error, output_sp);
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "handle_module_unloaded failed: {0}",
+             error.AsCString());
+  }
 }
 
 llvm::Expected<bool>
@@ -83,6 +93,8 @@ ScriptedHookPythonInterface::HandleStop(ExecutionContext &exe_ctx,
 
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "handle_stop failed: {0}",
+             error.AsCString());
     if (!obj)
       return true;
     return error.ToError();
@@ -91,6 +103,21 @@ ScriptedHookPythonInterface::HandleStop(ExecutionContext &exe_ctx,
   return obj->GetBooleanValue();
 }
 
+std::optional<Address>
+ScriptedHookPythonInterface::HandleResolveAddress(lldb::addr_t load_addr,
+                                                  lldb::StreamSP &output_sp) {
+  Status error;
+  Address addr =
+      Dispatch<Address>("do_resolve_addr", error, load_addr, output_sp);
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "do_resolve_addr failed: {0}",
+             error.AsCString());
+  }
+  if (addr.IsSectionOffset())
+    return addr;
+  return std::nullopt;
+}
+
 void ScriptedHookPythonInterface::Initialize() {
   const std::vector<llvm::StringRef> ci_usages = {
       "target hook add -P <script-name> [-k key -v value ...]"};
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
index da15a4bb287d9..6b6403e3fd1fc 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
@@ -33,14 +33,15 @@ class ScriptedHookPythonInterface : public ScriptedHookInterface,
     return llvm::SmallVector<AbstractMethodRequirement>({{"handle_stop", 2}});
   }
 
-  /// Check which of the three hook methods the Python class implements.
+  /// Check which of the hook methods the Python class implements.
   SupportedHookMethods GetSupportedMethods() override;
 
   void HandleModuleLoaded(lldb::StreamSP &output_sp) override;
   void HandleModuleUnloaded(lldb::StreamSP &output_sp) override;
   llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
                                   lldb::StreamSP &output_sp) override;
-
+  std::optional<Address> HandleResolveAddress(lldb::addr_t load_addr, 
+                                              lldb::StreamSP &output_sp) override;
   static void Initialize();
   static void Terminate();
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
index 61ceb40dd9d32..9a99cf73e3783 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -14,6 +14,8 @@
 
 #include "../ScriptInterpreterPythonImpl.h"
 #include "ScriptedPythonInterface.h"
+#include "lldb/API/SBAddress.h"
+#include "lldb/Core/Address.h"
 #include "lldb/Symbol/SymbolContext.h"
 #include "lldb/ValueObject/ValueObjectList.h"
 #include <optional>
@@ -118,6 +120,18 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<SymbolContext>(
   return {};
 }
 
+template <>
+Address ScriptedPythonInterface::ExtractValueFromPythonObject<Address>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBAddress *sb_addr = reinterpret_cast<lldb::SBAddress *>(
+          python::LLDBSWIGPython_CastPyObjectToSBAddress(p.get())))
+    return m_interpreter.GetOpaqueTypeFromSBAddress(*sb_addr);
+  error = Status::FromErrorString(
+      "Couldn't cast lldb::SBAddress to lldb_private::Address.");
+
+  return {};
+}
+
 template <>
 lldb::DataExtractorSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::DataExtractorSP>(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index aaa0b6a0f7a59..984fcaf6b8989 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -648,6 +648,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(Address &arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
   python::PythonObject Transform(lldb::StreamSP arg) {
     return python::SWIGBridge::ToSWIGWrapper(arg.get());
   }
@@ -768,6 +772,10 @@ SymbolContext
 ScriptedPythonInterface::ExtractValueFromPythonObject<SymbolContext>(
     python::PythonObject &p, Status &error);
 
+template <>
+Address ScriptedPythonInterface::ExtractValueFromPythonObject<Address>(
+    python::PythonObject &p, Status &error);
+
 template <>
 lldb::StreamSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StreamSP>(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 07e0da1dcf70d..839d421b9e855 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -86,6 +86,7 @@ class SWIGBridge {
   static PythonObject ToSWIGWrapper(lldb::BreakpointSP breakpoint_sp);
   static PythonObject ToSWIGWrapper(Status &&status);
   static PythonObject ToSWIGWrapper(const StructuredDataImpl &data_impl);
+  static PythonObject ToSWIGWrapper(const Address &addr);
   static PythonObject ToSWIGWrapper(lldb::ThreadSP thread_sp);
   static PythonObject ToSWIGWrapper(lldb::StackFrameSP frame_sp);
   static PythonObject ToSWIGWrapper(lldb::StackFrameListSP frames_sp);
@@ -252,6 +253,7 @@ void *LLDBSWIGPython_CastPyObjectToSBStream(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBThread(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBFrame(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBSymbolContext(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBAddress(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBValue(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBValueList(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject *data);
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 148d7e0b30dbb..1e7fed14386dc 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -3481,8 +3481,14 @@ Status Target::Install(ProcessLaunchInfo *launch_info) {
 
 bool Target::ResolveLoadAddress(addr_t load_addr, Address &so_addr,
                                 uint32_t stop_id, bool allow_section_end) {
-  return m_section_load_history.ResolveLoadAddress(stop_id, load_addr, so_addr,
-                                                   allow_section_end);
+  if (m_section_load_history.ResolveLoadAddress(stop_id, load_addr, so_addr,
+                                                allow_section_end))
+    return true;
+  if (std::optional<Address> opt_addr = RunResolveAddressHooks(load_addr)) {
+    so_addr = *opt_addr;
+    return true;
+  }
+  return false;
 }
 
 bool Target::ResolveFileAddress(lldb::addr_t file_addr,
@@ -4668,6 +4674,8 @@ Status Target::HookScripted::SetScriptCallback(
     m_trigger_mask |= kModulesUnloaded;
   if (methods.handle_stop)
     m_trigger_mask |= kProcessStop;
+  if (methods.do_resolve_addr)
+    m_trigger_mask |= kResolveAddress;
 
   return {};
 }
@@ -4709,6 +4717,14 @@ Target::HookScripted::HandleStop(ExecutionContext &exc_ctx,
                              : StopHook::StopHookResult::RequestContinue;
 }
 
+std::optional<Address>
+Target::HookScripted::HandleResolveAddress(lldb::addr_t load_addr,
+                                           lldb::StreamSP &output_sp) {
+  if (!m_interface_sp)
+    return std::nullopt;
+  return m_interface_sp->HandleResolveAddress(load_addr, output_sp);
+}
+
 llvm::StringRef Target::HookScripted::GetScriptClassName() const {
   if (m_interface_sp && m_interface_sp->GetScriptedMetadata())
     return m_interface_sp->GetScriptedMetadata()->GetClassName();
@@ -4812,6 +4828,34 @@ void Target::SetAllHooksEnabledState(bool enabled) {
     hook->SetIsEnabled(enabled);
 }
 
+std::optional<Address> Target::RunResolveAddressHooks(lldb::addr_t load_addr) {
+  if (m_hooks.empty())
+    return std::nullopt;
+
+  // Copy active hooks into a local vector before iterating, in case a
+  // callback modifies m_hooks (same pattern as RunStopHooks).
+  std::vector<HookSP> active_hooks;
+  for (auto &[_, hook_sp] : m_hooks) {
+    if (hook_sp->IsEnabled() && hook_sp->FiresOn(Hook::kResolveAddress))
+      active_hooks.push_back(hook_sp);
+  }
+
+  if (active_hooks.empty())
+    return std::nullopt;
+
+  StreamSP output_sp = m_debugger.GetAsyncOutputStream();
+
+  for (auto &hook_sp : active_hooks) {
+    if (std::optional<Address> opt_addr = hook_sp->HandleResolveAddress(load_addr, output_sp)) {
+      assert(opt_addr->IsSectionOffset());
+      return *opt_addr;
+    }
+  }
+
+  output_sp->Flush();
+  return std::nullopt;
+}
+
 void Target::RunModuleHooks(bool is_load) {
   if (m_hooks.empty())
     return;
diff --git a/lldb/test/API/commands/target/module-hook/resolve-addr/Makefile b/lldb/test/API/commands/target/module-hook/resolve-addr/Makefile
new file mode 100644
index 0000000000000..3d0b98f13f3d7
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/resolve-addr/Makefile
@@ -0,0 +1,2 @@
+CXX_SOURCES := main.cpp
+include Makefile.rules
diff --git a/lldb/test/API/commands/target/module-hook/resolve-addr/TestModuleHookResolveAddr.py b/lldb/test/API/commands/target/module-hook/resolve-addr/TestModuleHookResolveAddr.py
new file mode 100644
index 0000000000000..1a0a0e8a9e57e
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/resolve-addr/TestModuleHookResolveAddr.py
@@ -0,0 +1,128 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+def make_json_objfile(outpath, triple):
+    '''
+    Create a JSON object file at `outpath` for the given `triple` that can be
+    used to test the "target hook add" where
+    '''
+    data = {
+        "triple": triple,
+        "uuid": "ED6F6CBD-7357-3D32-B3E9-FDA98E07D863",
+        "type": "sharedlibrary",
+        "sections": [
+            {
+                "user_id": 0x200,
+                "name": ".text",
+                "address": 0,
+                "size": 0x1000,
+                "flags": 0x202,
+                "file_offset": 0,
+                "file_size": 0,
+                "read": True,
+                "write": False,
+                "execute": True,
+            }
+        ],
+        "symbols": [
+            {
+                "name": "foo",
+                "type": "code",
+                "address": 0x0,
+                "size": 0x100,
+            },
+            {
+                "name": "bar",
+                "type": "code",
+                "address": 0x100,
+                "size": 0x200,
+            },
+            {
+                "name": "baz",
+                "type": "code",
+                "address": 0x300,
+                "size": 0x300,
+            },
+        ],
+    }
+    with open(outpath, "w") as file:
+        json.dump(data, file, indent=4)
+        return True
+    return False
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_resolve_addr(self):
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        addrhook_python_path = self.getSourcePath("addrhook.py")
+        addrhook_module_path = self.getBuildArtifact("addrhook.json")
+        target = self.dbg.CreateTarget(exe)
+
+        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+            self, "// Set a breakpoint here", lldb.SBFileSpec("main.cpp", False)
+        )
+
+        # We need to find a memory region that has no permissions that doesn't
+        # start at 0x0 and is at least 0x1000 bytes in size. We'll use this
+        # region to load our JSON object file to ensure it doesn't collide with
+        # any other modules in the target. We'll use the base address of this
+        # region as the base address for our JSON object file.
+        region_to_use = None
+        curr_addr = 0x0
+        while True:
+            region = lldb.SBMemoryRegionInfo()
+            error = target.process.GetMemoryRegionInfo(curr_addr, region)
+            if error.Fail():
+                print(f'error: "{error}"')
+                break
+            base_addr = region.GetRegionBase()
+            end_addr = region.GetRegionEnd()
+            print(f"Found memory region: [{base_addr:#x}-{end_addr:#x})")
+            # We don't want a memory region that starts at 0x0, invalid ranges
+            # and any regions that are too small.
+            if (base_addr > 0x0) and ((end_addr - base_addr) >= 0x1000):
+                if not region.IsReadable() and not region.IsWritable() and not region.IsExecutable():
+                    print(f"Found a memory region with no permissions at {region}")
+                    region_to_use = region
+                    break
+            if end_addr == 0xffffffffffffffff:
+                break
+            curr_addr = end_addr
+
+        # If we didn't find a memory region with no permissions, then skip the
+        # test.
+        if region_to_use is None:
+            return self.skipTest("No memory region with no permissions found")
+
+        json_module_load_addr = region_to_use.GetRegionBase()
+        # Load the target hook script file.
+        self.dbg.HandleCommand(f'command script import "{addrhook_python_path}"')
+        # Setup the "target hook" to use the addrhook.py script and pass in the
+        # path to the JSON object file and the base address to load it at as
+        # extra arguments to the script.
+        self.dbg.HandleCommand(f'target hook add --script-class addrhook.Hooks -k path -v "{addrhook_module_path}" -k base_addr -v {json_module_load_addr:#x}')
+
+        # Verify we can resolve addresses for "foo", "bar" and "baz" symbols in
+        # the JSON object file.
+        addr = target.ResolveLoadAddress(json_module_load_addr + 0x0)
+        self.assertTrue(addr.IsValid(), "Address should be valid")
+        self.assertTrue(addr.GetSection().IsValid(), f"Address ({addr}) should have a valid section")
+        self.assertEqual(addr.GetSection().GetName(), ".text", "Address should be in .text section")
+        self.assertEqual(addr.GetSymbol().GetName(), "foo", "Address should resolve to symbol 'foo'")
+
+        addr = target.ResolveLoadAddress(json_module_load_addr + 0x100)
+        self.assertTrue(addr.IsValid(), "Address should be valid")
+        self.assertTrue(addr.GetSection().IsValid(), "Address should have a valid section")
+        self.assertEqual(addr.GetSection().GetName(), ".text", "Address should be in .text section")
+        self.assertEqual(addr.GetSymbol().GetName(), "bar", "Address should resolve to symbol 'bar'")
+
+        addr = target.ResolveLoadAddress(json_module_load_addr + 0x300)
+        self.assertTrue(addr.IsValid(), "Address should be valid")
+        self.assertTrue(addr.GetSection().IsValid(), "Address should have a valid section")
+        self.assertEqual(addr.GetSection().GetName(), ".text", "Address should be in .text section")
+        self.assertEqual(addr.GetSymbol().GetName(), "baz", "Address should resolve to symbol 'baz'")
diff --git a/lldb/test/API/commands/target/module-hook/resolve-addr/addrhook.py b/lldb/test/API/commands/target/module-hook/resolve-addr/addrhook.py
new file mode 100644
index 0000000000000..9b0d148de8435
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/resolve-addr/addrhook.py
@@ -0,0 +1,94 @@
+import lldb
+import json
+
+from lldb.plugins.scripted_hook import ScriptedHook
+
+# Make a JSON object file at `outpath` for the given `triple` that can be
+# used to test the "target hook add" where the target hook will be called to
+# resolve an address that is not in any of the target's modules.
+def make_json_objfile(outpath, triple):
+    data = {
+        "triple": triple,
+        "uuid": "ED6F6CBD-7357-3D32-B3E9-FDA98E07D863",
+        "type": "sharedlibrary",
+        "sections": [
+            {
+                "user_id": 0x200,
+                "name": ".text",
+                "address": 0x0,
+                "size": 0x1000,
+                "flags": 0x202,
+                "file_offset": 0,
+                "file_size": 0,
+                "read": True,
+                "write": False,
+                "execute": True,
+            }
+        ],
+        "symbols": [
+            {
+                "name": "foo",
+                "type": "code",
+                "address": 0x0,
+                "size": 0x100,
+            },
+            {
+                "name": "bar",
+                "type": "code",
+                "address": 0x100,
+                "size": 0x200,
+            },
+            {
+                "name": "baz",
+                "type": "code",
+                "address": 0x300,
+                "size": 0x300,
+            },
+        ],
+    }
+    with open(outpath, "w") as file:
+        json.dump(data, file, indent=4)
+        return True
+    return False
+
+class Hooks(ScriptedHook):
+    def __init__(self, target, extra_args, internal_dict):
+        super().__init__(target, extra_args)
+        print(f'extra_args type is "{type(extra_args)}"')
+        self.path = str(extra_args.GetValueForKey("path"))
+        self.base_addr = int(extra_args.GetValueForKey("base_addr"))
+        print(f'self.path = "{self.path}"')
+        print(f'self.base_addr = {self.base_addr:#x}')
+
+    def handle_stop(self,
+                    exe_ctx: lldb.SBExecutionContext,
+                    stream: lldb.SBStream) -> bool:
+        # This method is required to be implemented. Return true to stop the
+        # process, or false to continue.
+        return True  # Stop
+
+    def do_resolve_addr(self,
+                        load_addr: int,
+                        stream: lldb.SBStream) -> lldb.SBAddress:
+        '''
+        Called when the target hook needs to resolve an address when the target
+        was not able to resolve the address. We will create a JSON object
+        file and load it into the target, then resolve the address in this JSON
+        object file.
+        '''
+        module_load_addr = self.base_addr
+        module_load_addr_end = self.base_addr + 0x1000
+        if module_load_addr <= load_addr and load_addr < module_load_addr_end:
+            if self.target.module['addrhook.json'] is None:
+                if make_json_objfile(self.path, self.target.triple):
+                    print(f"Successfully created JSON object file at '{self.path}'")
+                    module = self.target.AddModule(self.path, None, None)
+                    if module:
+                        print(f"Module loaded successfully, setting load address for .text section to {module_load_addr:#x}")
+                        self.target.SetSectionLoadAddress(
+                            module.FindSection(".text"),
+                            module_load_addr)
+                addr = self.target.ResolveLoadAddress(load_addr)
+                print(f"Resolved address {load_addr:#x} to {addr}")
+                return addr
+        return lldb.SBAddress()  # Return an invalid address if we didn't resolve it
diff --git a/lldb/test/API/commands/target/module-hook/resolve-addr/main.cpp b/lldb/test/API/commands/target/module-hook/resolve-addr/main.cpp
new file mode 100644
index 0000000000000..cd4d683139e6f
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/resolve-addr/main.cpp
@@ -0,0 +1,4 @@
+int main()
+{
+  return 0; // Set a breakpoint here
+}

>From ab3a54c64e18c5e3892d172c586abf0cd73a55e8 Mon Sep 17 00:00:00 2001
From: Greg Clayton <clayborg at gmail.com>
Date: Fri, 11 Sep 2026 13:48:32 -0700
Subject: [PATCH 2/2] Fix LLDBSWIGPython_CastPyObjectToSBAddress.

---
 lldb/bindings/python/python-wrapper.swig | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index e4b8fd3696d05..7decddf286f9f 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -536,14 +536,14 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBSymbolContext(PyObjec
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBAddress(PyObject * data) {
   lldb::SBAddress *sb_ptr = nullptr;
 
-//   int valid_cast =
-//       SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBAddress, 0);
+  int valid_cast =
+      SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBAddress, 0);
 
-//   if (valid_cast == -1)
-//     return NULL;
+  if (valid_cast == -1)
+    return NULL;
 
-//   return sb_ptr;
-// }
+  return sb_ptr;
+}
 
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBValue(PyObject * data) {
   lldb::SBValue *sb_ptr = NULL;



More information about the lldb-commits mailing list