[llvm] [ORC] Free ELF debug objects in dealloc action of corresponding code (wip) (PR #168522)

Stefan Gränitz via llvm-commits llvm-commits at lists.llvm.org
Tue Nov 18 03:56:19 PST 2025


https://github.com/weliveindetail created https://github.com/llvm/llvm-project/pull/168522

We don't want to keep tracking allocations of debug objects in the controller after linking finished. So far, the plugin did that to free the target memory once the corresponding code is unlinked. This patch removes the tracking and instead attaches a dealloc action for it to the target memory of the code.

>From 42183aa346c588c5a9626cb800fc2c513d6a35cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Tue, 18 Nov 2025 10:34:31 +0100
Subject: [PATCH 1/2] [ORC] Tailor ELF debugger support plugin to load-address
 patching only

---
 .../Orc/Debugging/ELFDebugObjectPlugin.h      |  34 +-
 .../Orc/Debugging/ELFDebugObjectPlugin.cpp    | 598 +++++++-----------
 2 files changed, 227 insertions(+), 405 deletions(-)

diff --git a/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h b/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
index d946a029fd2ec..92dbfe1c79e6e 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
@@ -23,7 +23,6 @@
 #include "llvm/Support/MemoryBufferRef.h"
 #include "llvm/TargetParser/Triple.h"
 
-#include <functional>
 #include <map>
 #include <memory>
 #include <mutex>
@@ -33,35 +32,24 @@ namespace orc {
 
 class DebugObject;
 
-/// Creates and manages DebugObjects for JITLink artifacts.
-///
-/// DebugObjects are created when linking for a MaterializationResponsibility
-/// starts. They are pending as long as materialization is in progress.
-///
-/// There can only be one pending DebugObject per MaterializationResponsibility.
-/// If materialization fails, pending DebugObjects are discarded.
-///
-/// Once executable code for the MaterializationResponsibility is emitted, the
-/// corresponding DebugObject is finalized to target memory and the provided
-/// DebugObjectRegistrar is notified. Ownership of DebugObjects remains with the
-/// plugin.
+/// Debugger support for ELF platforms with the GDB JIT Interface. The plugin
+/// emits and manages a separate debug object allocation in addition to the
+/// LinkGraph's own allocation and it notifies the debugger when necessary.
 ///
 class LLVM_ABI ELFDebugObjectPlugin : public ObjectLinkingLayer::Plugin {
 public:
-  /// Create the plugin to submit DebugObjects for JITLink artifacts. For all
-  /// options the recommended setting is true.
+  /// Create the plugin for the given session and set additional options
   ///
   /// RequireDebugSections:
-  ///   Submit debug objects to the executor only if they contain actual debug
-  ///   info. Turning this off may allow minimal debugging based on raw symbol
-  ///   names. Note that this may cause significant memory and transport
-  ///   overhead for objects built with a release configuration.
+  ///   Emit debug objects only if the LinkGraph contains debug info. Turning
+  ///   this off allows minimal debugging based on raw symbol names, but it
+  ///   comes with significant overhead for release configurations.
   ///
   /// AutoRegisterCode:
   ///   Notify the debugger for each new debug object. This is a good default
   ///   mode, but it may cause significant overhead when adding many modules in
-  ///   sequence. When turning this off, the user has to issue the call to
-  ///   __jit_debug_register_code() on the executor side manually.
+  ///   sequence. Otherwise the user must call __jit_debug_register_code() in
+  ///   the debug session manually.
   ///
   ELFDebugObjectPlugin(ExecutionSession &ES, bool RequireDebugSections,
                        bool AutoRegisterCode, Error &Err);
@@ -69,7 +57,7 @@ class LLVM_ABI ELFDebugObjectPlugin : public ObjectLinkingLayer::Plugin {
 
   void notifyMaterializing(MaterializationResponsibility &MR,
                            jitlink::LinkGraph &G, jitlink::JITLinkContext &Ctx,
-                           MemoryBufferRef InputObject) override;
+                           MemoryBufferRef InputObj) override;
 
   Error notifyFailed(MaterializationResponsibility &MR) override;
   Error notifyRemovingResources(JITDylib &JD, ResourceKey K) override;
@@ -94,6 +82,8 @@ class LLVM_ABI ELFDebugObjectPlugin : public ObjectLinkingLayer::Plugin {
   ExecutorAddr RegistrationAction;
   bool RequireDebugSections;
   bool AutoRegisterCode;
+
+  DebugObject *getPendingDebugObj(MaterializationResponsibility &MR);
 };
 
 } // namespace orc
diff --git a/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp b/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
index 9f556b0d07a8b..966482fef32a5 100644
--- a/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
+++ b/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
@@ -17,11 +17,17 @@
 #include "llvm/ADT/StringMap.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/BinaryFormat/ELF.h"
+#include "llvm/ExecutionEngine/JITLink/JITLink.h"
 #include "llvm/ExecutionEngine/JITLink/JITLinkDylib.h"
 #include "llvm/ExecutionEngine/JITLink/JITLinkMemoryManager.h"
+#include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h"
+#include "llvm/ExecutionEngine/Orc/Shared/MemoryFlags.h"
 #include "llvm/ExecutionEngine/Orc/Shared/OrcRTBridge.h"
+#include "llvm/IR/Instructions.h"
 #include "llvm/Object/ELFObjectFile.h"
+#include "llvm/Object/Error.h"
 #include "llvm/Support/Errc.h"
+#include "llvm/Support/Error.h"
 #include "llvm/Support/MSVCErrorWorkarounds.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Process.h"
@@ -37,111 +43,19 @@ using namespace llvm::object;
 namespace llvm {
 namespace orc {
 
-class DebugObjectSection {
-public:
-  virtual void setTargetMemoryRange(SectionRange Range) = 0;
-  virtual void dump(raw_ostream &OS, StringRef Name) {}
-  virtual ~DebugObjectSection() = default;
-};
-
-template <typename ELFT>
-class ELFDebugObjectSection : public DebugObjectSection {
-public:
-  // BinaryFormat ELF is not meant as a mutable format. We can only make changes
-  // that don't invalidate the file structure.
-  ELFDebugObjectSection(const typename ELFT::Shdr *Header)
-      : Header(const_cast<typename ELFT::Shdr *>(Header)) {}
-
-  void setTargetMemoryRange(SectionRange Range) override;
-  void dump(raw_ostream &OS, StringRef Name) override;
-
-  Error validateInBounds(StringRef Buffer, const char *Name) const;
-
-private:
-  typename ELFT::Shdr *Header;
-};
-
-template <typename ELFT>
-void ELFDebugObjectSection<ELFT>::setTargetMemoryRange(SectionRange Range) {
-  // All recorded sections are candidates for load-address patching.
-  Header->sh_addr =
-      static_cast<typename ELFT::uint>(Range.getStart().getValue());
-}
-
-template <typename ELFT>
-Error ELFDebugObjectSection<ELFT>::validateInBounds(StringRef Buffer,
-                                                    const char *Name) const {
-  const uint8_t *Start = Buffer.bytes_begin();
-  const uint8_t *End = Buffer.bytes_end();
-  const uint8_t *HeaderPtr = reinterpret_cast<uint8_t *>(Header);
-  if (HeaderPtr < Start || HeaderPtr + sizeof(typename ELFT::Shdr) > End)
-    return make_error<StringError>(
-        formatv("{0} section header at {1:x16} not within bounds of the "
-                "given debug object buffer [{2:x16} - {3:x16}]",
-                Name, &Header->sh_addr, Start, End),
-        inconvertibleErrorCode());
-  if (Header->sh_offset + Header->sh_size > Buffer.size())
-    return make_error<StringError>(
-        formatv("{0} section data [{1:x16} - {2:x16}] not within bounds of "
-                "the given debug object buffer [{3:x16} - {4:x16}]",
-                Name, Start + Header->sh_offset,
-                Start + Header->sh_offset + Header->sh_size, Start, End),
-        inconvertibleErrorCode());
-  return Error::success();
-}
-
-template <typename ELFT>
-void ELFDebugObjectSection<ELFT>::dump(raw_ostream &OS, StringRef Name) {
-  if (uint64_t Addr = Header->sh_addr) {
-    OS << formatv("  {0:x16} {1}\n", Addr, Name);
-  } else {
-    OS << formatv("                     {0}\n", Name);
-  }
-}
-
-enum DebugObjectFlags : int {
-  // Request final target memory load-addresses for all sections.
-  ReportFinalSectionLoadAddresses = 1 << 0,
-
-  // We found sections with debug information when processing the input object.
-  HasDebugSections = 1 << 1,
-};
-
-/// The plugin creates a debug object from when JITLink starts processing the
-/// corresponding LinkGraph. It provides access to the pass configuration of
-/// the LinkGraph and calls the finalization function, once the resulting link
-/// artifact was emitted.
-///
+// Helper class to emit and fixup an individual debug object
 class DebugObject {
 public:
-  DebugObject(JITLinkMemoryManager &MemMgr, const JITLinkDylib *JD,
+  using FinalizedAlloc = JITLinkMemoryManager::FinalizedAlloc;
+
+  DebugObject(StringRef Name, SimpleSegmentAlloc Alloc, JITLinkContext &Ctx,
               ExecutionSession &ES)
-      : MemMgr(MemMgr), JD(JD), ES(ES), Flags(DebugObjectFlags{}) {
+      : Name(Name), WorkingMem(std::move(Alloc)),
+        MemMgr(Ctx.getMemoryManager()), ES(ES) {
     FinalizeFuture = FinalizePromise.get_future();
   }
 
-  bool hasFlags(DebugObjectFlags F) const { return Flags & F; }
-  void setFlags(DebugObjectFlags F) {
-    Flags = static_cast<DebugObjectFlags>(Flags | F);
-  }
-  void clearFlags(DebugObjectFlags F) {
-    Flags = static_cast<DebugObjectFlags>(Flags & ~F);
-  }
-
-  using FinalizeContinuation = std::function<void(Expected<ExecutorAddrRange>)>;
-  void finalizeAsync(FinalizeContinuation OnAsync);
-
-  void failMaterialization(Error Err) {
-    FinalizePromise.set_value(std::move(Err));
-  }
-
-  void reportTargetMem(ExecutorAddrRange TargetMem) {
-    FinalizePromise.set_value(TargetMem);
-  }
-
-  Expected<ExecutorAddrRange> awaitTargetMem() { return FinalizeFuture.get(); }
-
-  virtual ~DebugObject() {
+  ~DebugObject() {
     if (Alloc) {
       std::vector<FinalizedAlloc> Allocs;
       Allocs.push_back(std::move(Alloc));
@@ -150,259 +64,122 @@ class DebugObject {
     }
   }
 
-  virtual void reportSectionTargetMemoryRange(StringRef Name,
-                                              SectionRange TargetMem) {}
+  StringRef getName() const { return Name; }
 
-protected:
-  using InFlightAlloc = JITLinkMemoryManager::InFlightAlloc;
-  using FinalizedAlloc = JITLinkMemoryManager::FinalizedAlloc;
+  StringRef getBuffer() {
+    MutableArrayRef<char> Buffer = getMutBuffer();
+    return StringRef(Buffer.data(), Buffer.size());
+  }
 
-  virtual Expected<SimpleSegmentAlloc> finalizeWorkingMemory() = 0;
+  MutableArrayRef<char> getMutBuffer() {
+    auto SegInfo = WorkingMem.getSegInfo(MemProt::Read);
+    return SegInfo.WorkingMem;
+  }
 
-  JITLinkMemoryManager &MemMgr;
-  const JITLinkDylib *JD = nullptr;
-  ExecutionSession &ES;
+  SimpleSegmentAlloc &getTargetAlloc() { return WorkingMem; }
 
-  std::promise<MSVCPExpected<ExecutorAddrRange>> FinalizePromise;
-  std::future<MSVCPExpected<ExecutorAddrRange>> FinalizeFuture;
+  void trackFinalizedAlloc(FinalizedAlloc FA) { Alloc = std::move(FA); }
 
-private:
-  DebugObjectFlags Flags;
-  FinalizedAlloc Alloc;
-};
+  Expected<ExecutorAddrRange> awaitTargetMem() { return FinalizeFuture.get(); }
 
-// Finalize working memory and take ownership of the resulting allocation. Start
-// copying memory over to the target and pass on the result once we're done.
-// Ownership of the allocation remains with us for the rest of our lifetime.
-void DebugObject::finalizeAsync(FinalizeContinuation OnFinalize) {
-  assert(!this->Alloc && "Cannot finalize more than once");
-  if (auto SimpleSegAlloc = finalizeWorkingMemory()) {
-    auto ROSeg = SimpleSegAlloc->getSegInfo(MemProt::Read);
-    ExecutorAddrRange DebugObjRange(ROSeg.Addr, ROSeg.WorkingMem.size());
-    SimpleSegAlloc->finalize(
-        [this, DebugObjRange,
-         OnFinalize = std::move(OnFinalize)](Expected<FinalizedAlloc> FA) {
-          if (FA) {
-            // Note: FA->getAddress() is supposed to be the address of the
-            // memory range on the target, but InProcessMemoryManager returns
-            // the address of a FinalizedAllocInfo helper instead.
-            this->Alloc = std::move(*FA);
-            OnFinalize(DebugObjRange);
-          } else
-            OnFinalize(FA.takeError());
-        });
-  } else {
-    // We could report this error synchronously, but it's easier this way,
-    // because the FinalizePromise will be triggered unconditionally.
-    OnFinalize(SimpleSegAlloc.takeError());
+  void reportTargetMem(ExecutorAddrRange TargetMem) {
+    FinalizePromise.set_value(TargetMem);
   }
-}
-
-/// The current implementation of ELFDebugObject replicates the approach used in
-/// RuntimeDyld: It patches executable and data section headers in the given
-/// object buffer with load-addresses of their corresponding sections in target
-/// memory.
-///
-class ELFDebugObject : public DebugObject {
-public:
-  static Expected<std::unique_ptr<DebugObject>>
-  Create(MemoryBufferRef Buffer, JITLinkContext &Ctx, ExecutionSession &ES);
 
-  void reportSectionTargetMemoryRange(StringRef Name,
-                                      SectionRange TargetMem) override;
+  void failMaterialization(Error Err) {
+    FinalizePromise.set_value(std::move(Err));
+  }
 
-  StringRef getBuffer() const { return Buffer->getMemBufferRef().getBuffer(); }
+  void reportError(Error Err) { ES.reportError(std::move(Err)); }
 
-protected:
-  Expected<SimpleSegmentAlloc> finalizeWorkingMemory() override;
+  using GetLoadAddressFn = llvm::unique_function<ExecutorAddr(StringRef)>;
+  void visitSections(GetLoadAddressFn Callback);
 
   template <typename ELFT>
-  Error recordSection(StringRef Name,
-                      std::unique_ptr<ELFDebugObjectSection<ELFT>> Section);
-  DebugObjectSection *getSection(StringRef Name);
+  void visitSectionLoadAddresses(GetLoadAddressFn Callback);
 
 private:
-  template <typename ELFT>
-  static Expected<std::unique_ptr<ELFDebugObject>>
-  CreateArchType(MemoryBufferRef Buffer, JITLinkMemoryManager &MemMgr,
-                 const JITLinkDylib *JD, ExecutionSession &ES);
-
-  static std::unique_ptr<WritableMemoryBuffer>
-  CopyBuffer(MemoryBufferRef Buffer, Error &Err);
-
-  ELFDebugObject(std::unique_ptr<WritableMemoryBuffer> Buffer,
-                 JITLinkMemoryManager &MemMgr, const JITLinkDylib *JD,
-                 ExecutionSession &ES)
-      : DebugObject(MemMgr, JD, ES), Buffer(std::move(Buffer)) {
-    setFlags(ReportFinalSectionLoadAddresses);
-  }
+  std::string Name;
+  SimpleSegmentAlloc WorkingMem;
+  JITLinkMemoryManager &MemMgr;
+  ExecutionSession &ES;
 
-  std::unique_ptr<WritableMemoryBuffer> Buffer;
-  StringMap<std::unique_ptr<DebugObjectSection>> Sections;
-};
+  std::promise<MSVCPExpected<ExecutorAddrRange>> FinalizePromise;
+  std::future<MSVCPExpected<ExecutorAddrRange>> FinalizeFuture;
 
-static const std::set<StringRef> DwarfSectionNames = {
-#define HANDLE_DWARF_SECTION(ENUM_NAME, ELF_NAME, CMDLINE_NAME, OPTION)        \
-  ELF_NAME,
-#include "llvm/BinaryFormat/Dwarf.def"
-#undef HANDLE_DWARF_SECTION
+  FinalizedAlloc Alloc;
 };
 
-static bool isDwarfSection(StringRef SectionName) {
-  return DwarfSectionNames.count(SectionName) == 1;
-}
-
-std::unique_ptr<WritableMemoryBuffer>
-ELFDebugObject::CopyBuffer(MemoryBufferRef Buffer, Error &Err) {
-  ErrorAsOutParameter _(Err);
-  size_t Size = Buffer.getBufferSize();
-  StringRef Name = Buffer.getBufferIdentifier();
-  if (auto Copy = WritableMemoryBuffer::getNewUninitMemBuffer(Size, Name)) {
-    memcpy(Copy->getBufferStart(), Buffer.getBufferStart(), Size);
-    return Copy;
-  }
-
-  Err = errorCodeToError(make_error_code(errc::not_enough_memory));
-  return nullptr;
-}
-
 template <typename ELFT>
-Expected<std::unique_ptr<ELFDebugObject>>
-ELFDebugObject::CreateArchType(MemoryBufferRef Buffer,
-                               JITLinkMemoryManager &MemMgr,
-                               const JITLinkDylib *JD, ExecutionSession &ES) {
+void DebugObject::visitSectionLoadAddresses(GetLoadAddressFn Callback) {
   using SectionHeader = typename ELFT::Shdr;
 
-  Error Err = Error::success();
-  std::unique_ptr<ELFDebugObject> DebugObj(
-      new ELFDebugObject(CopyBuffer(Buffer, Err), MemMgr, JD, ES));
-  if (Err)
-    return std::move(Err);
-
-  Expected<ELFFile<ELFT>> ObjRef = ELFFile<ELFT>::create(DebugObj->getBuffer());
-  if (!ObjRef)
-    return ObjRef.takeError();
+  Expected<ELFFile<ELFT>> ObjRef = ELFFile<ELFT>::create(getBuffer());
+  if (!ObjRef) {
+    reportError(ObjRef.takeError());
+    return;
+  }
 
   Expected<ArrayRef<SectionHeader>> Sections = ObjRef->sections();
-  if (!Sections)
-    return Sections.takeError();
+  if (!Sections) {
+    reportError(Sections.takeError());
+    return;
+  }
 
   for (const SectionHeader &Header : *Sections) {
     Expected<StringRef> Name = ObjRef->getSectionName(Header);
-    if (!Name)
-      return Name.takeError();
+    if (!Name) {
+      reportError(Name.takeError());
+      return;
+    }
     if (Name->empty())
       continue;
-    if (isDwarfSection(*Name))
-      DebugObj->setFlags(HasDebugSections);
-
-    // Only record text and data sections (i.e. no bss, comments, rel, etc.)
-    if (Header.sh_type != ELF::SHT_PROGBITS &&
-        Header.sh_type != ELF::SHT_X86_64_UNWIND)
-      continue;
-    if (!(Header.sh_flags & ELF::SHF_ALLOC))
-      continue;
-
-    auto Wrapped = std::make_unique<ELFDebugObjectSection<ELFT>>(&Header);
-    if (Error Err = DebugObj->recordSection(*Name, std::move(Wrapped)))
-      return std::move(Err);
+    ExecutorAddr LoadAddress = Callback(*Name);
+    const_cast<SectionHeader &>(Header).sh_addr =
+        static_cast<typename ELFT::uint>(LoadAddress.getValue());
   }
 
-  return std::move(DebugObj);
+  LLVM_DEBUG({
+    dbgs() << "Section load-addresses in debug object for \"" << getName()
+           << "\":\n";
+    for (const SectionHeader &Header : *Sections) {
+      StringRef Name = cantFail(ObjRef->getSectionName(Header));
+      if (uint64_t Addr = Header.sh_addr) {
+        dbgs() << formatv("  {0:x16} {1}\n", Addr, Name);
+      } else {
+        dbgs() << formatv("                     {0}\n", Name);
+      }
+    }
+  });
 }
 
-Expected<std::unique_ptr<DebugObject>>
-ELFDebugObject::Create(MemoryBufferRef Buffer, JITLinkContext &Ctx,
-                       ExecutionSession &ES) {
+void DebugObject::visitSections(GetLoadAddressFn Callback) {
   unsigned char Class, Endian;
-  std::tie(Class, Endian) = getElfArchType(Buffer.getBuffer());
+  std::tie(Class, Endian) = getElfArchType(getBuffer());
 
-  if (Class == ELF::ELFCLASS32) {
+  switch (Class) {
+  case ELF::ELFCLASS32:
     if (Endian == ELF::ELFDATA2LSB)
-      return CreateArchType<ELF32LE>(Buffer, Ctx.getMemoryManager(),
-                                     Ctx.getJITLinkDylib(), ES);
+      return visitSectionLoadAddresses<ELF32LE>(std::move(Callback));
     if (Endian == ELF::ELFDATA2MSB)
-      return CreateArchType<ELF32BE>(Buffer, Ctx.getMemoryManager(),
-                                     Ctx.getJITLinkDylib(), ES);
-    return nullptr;
-  }
-  if (Class == ELF::ELFCLASS64) {
+      return visitSectionLoadAddresses<ELF32BE>(std::move(Callback));
+    return reportError(createStringError(
+        object_error::invalid_file_type,
+        "Invalid endian in 32-bit ELF object file: %x", Endian));
+
+  case ELF::ELFCLASS64:
     if (Endian == ELF::ELFDATA2LSB)
-      return CreateArchType<ELF64LE>(Buffer, Ctx.getMemoryManager(),
-                                     Ctx.getJITLinkDylib(), ES);
+      return visitSectionLoadAddresses<ELF64LE>(std::move(Callback));
     if (Endian == ELF::ELFDATA2MSB)
-      return CreateArchType<ELF64BE>(Buffer, Ctx.getMemoryManager(),
-                                     Ctx.getJITLinkDylib(), ES);
-    return nullptr;
-  }
-  return nullptr;
-}
-
-Expected<SimpleSegmentAlloc> ELFDebugObject::finalizeWorkingMemory() {
-  LLVM_DEBUG({
-    dbgs() << "Section load-addresses in debug object for \""
-           << Buffer->getBufferIdentifier() << "\":\n";
-    for (const auto &KV : Sections)
-      KV.second->dump(dbgs(), KV.first());
-  });
-
-  // TODO: This works, but what actual alignment requirements do we have?
-  unsigned PageSize = sys::Process::getPageSizeEstimate();
-  size_t Size = Buffer->getBufferSize();
-
-  // Allocate working memory for debug object in read-only segment.
-  auto Alloc = SimpleSegmentAlloc::Create(
-      MemMgr, ES.getSymbolStringPool(), ES.getTargetTriple(), JD,
-      {{MemProt::Read, {Size, Align(PageSize)}}});
-  if (!Alloc)
-    return Alloc;
-
-  // Initialize working memory with a copy of our object buffer.
-  auto SegInfo = Alloc->getSegInfo(MemProt::Read);
-  memcpy(SegInfo.WorkingMem.data(), Buffer->getBufferStart(), Size);
-  Buffer.reset();
-
-  return Alloc;
-}
-
-void ELFDebugObject::reportSectionTargetMemoryRange(StringRef Name,
-                                                    SectionRange TargetMem) {
-  if (auto *DebugObjSection = getSection(Name))
-    DebugObjSection->setTargetMemoryRange(TargetMem);
-}
-
-template <typename ELFT>
-Error ELFDebugObject::recordSection(
-    StringRef Name, std::unique_ptr<ELFDebugObjectSection<ELFT>> Section) {
-  if (Error Err = Section->validateInBounds(this->getBuffer(), Name.data()))
-    return Err;
-  bool Inserted = Sections.try_emplace(Name, std::move(Section)).second;
-  if (!Inserted)
-    LLVM_DEBUG(dbgs() << "Skipping debug registration for section '" << Name
-                      << "' in object " << Buffer->getBufferIdentifier()
-                      << " (duplicate name)\n");
-  return Error::success();
-}
-
-DebugObjectSection *ELFDebugObject::getSection(StringRef Name) {
-  auto It = Sections.find(Name);
-  return It == Sections.end() ? nullptr : It->second.get();
-}
-
-/// Creates a debug object based on the input object file from
-/// ObjectLinkingLayerJITLinkContext.
-///
-static Expected<std::unique_ptr<DebugObject>>
-createDebugObjectFromBuffer(ExecutionSession &ES, LinkGraph &G,
-                            JITLinkContext &Ctx, MemoryBufferRef ObjBuffer) {
-  switch (G.getTargetTriple().getObjectFormat()) {
-  case Triple::ELF:
-    return ELFDebugObject::Create(ObjBuffer, Ctx, ES);
+      return visitSectionLoadAddresses<ELF64BE>(std::move(Callback));
+    return reportError(createStringError(
+        object_error::invalid_file_type,
+        "Invalid endian in 64-bit ELF object file: %x", Endian));
 
   default:
-    // TODO: Once we add support for other formats, we might want to split this
-    // into multiple files.
-    return nullptr;
+    return reportError(createStringError(object_error::invalid_file_type,
+                                         "Invalid arch in ELF object file: %x",
+                                         Class));
   }
 }
 
@@ -419,91 +196,146 @@ ELFDebugObjectPlugin::ELFDebugObjectPlugin(ExecutionSession &ES,
 
 ELFDebugObjectPlugin::~ELFDebugObjectPlugin() = default;
 
+static const std::set<StringRef> DwarfSectionNames = {
+#define HANDLE_DWARF_SECTION(ENUM_NAME, ELF_NAME, CMDLINE_NAME, OPTION)        \
+  ELF_NAME,
+#include "llvm/BinaryFormat/Dwarf.def"
+#undef HANDLE_DWARF_SECTION
+};
+
+static bool isDwarfSection(StringRef SectionName) {
+  return DwarfSectionNames.count(SectionName) == 1;
+}
+
 void ELFDebugObjectPlugin::notifyMaterializing(
     MaterializationResponsibility &MR, LinkGraph &G, JITLinkContext &Ctx,
-    MemoryBufferRef ObjBuffer) {
-  std::lock_guard<std::mutex> Lock(PendingObjsLock);
-  assert(PendingObjs.count(&MR) == 0 &&
-         "Cannot have more than one pending debug object per "
-         "MaterializationResponsibility");
+    MemoryBufferRef InputObj) {
+  if (G.getTargetTriple().getObjectFormat() != Triple::ELF)
+    return;
 
-  if (auto DebugObj = createDebugObjectFromBuffer(ES, G, Ctx, ObjBuffer)) {
-    // Not all link artifacts allow debugging.
-    if (*DebugObj == nullptr)
-      return;
-    if (RequireDebugSections && !(**DebugObj).hasFlags(HasDebugSections)) {
-      LLVM_DEBUG(dbgs() << "Skipping debug registration for LinkGraph '"
-                        << G.getName() << "': no debug info\n");
-      return;
-    }
-    PendingObjs[&MR] = std::move(*DebugObj);
-  } else {
-    ES.reportError(DebugObj.takeError());
+  // Step 1: We copy the raw input object into the working memory of a
+  // single-segment read-only allocation
+  size_t Size = InputObj.getBufferSize();
+  auto Alignment = sys::Process::getPageSizeEstimate();
+  SimpleSegmentAlloc::Segment Segment{Size, Align(Alignment)};
+
+  auto Alloc = SimpleSegmentAlloc::Create(
+      Ctx.getMemoryManager(), ES.getSymbolStringPool(), ES.getTargetTriple(),
+      Ctx.getJITLinkDylib(), {{MemProt::Read, Segment}});
+  if (!Alloc) {
+    ES.reportError(Alloc.takeError());
+    return;
   }
+
+  std::lock_guard<std::mutex> Lock(PendingObjsLock);
+  assert(PendingObjs.count(&MR) == 0 && "One debug object per materialization");
+  PendingObjs[&MR] = std::make_unique<DebugObject>(
+      InputObj.getBufferIdentifier(), std::move(*Alloc), Ctx, ES);
+
+  MutableArrayRef<char> Buffer = PendingObjs[&MR]->getMutBuffer();
+  memcpy(Buffer.data(), InputObj.getBufferStart(), Size);
+}
+
+DebugObject *
+ELFDebugObjectPlugin::getPendingDebugObj(MaterializationResponsibility &MR) {
+  std::lock_guard<std::mutex> Lock(PendingObjsLock);
+  auto It = PendingObjs.find(&MR);
+  return It == PendingObjs.end() ? nullptr : It->second.get();
 }
 
 void ELFDebugObjectPlugin::modifyPassConfig(MaterializationResponsibility &MR,
                                             LinkGraph &G,
                                             PassConfiguration &PassConfig) {
-  // Not all link artifacts have associated debug objects.
-  std::lock_guard<std::mutex> Lock(PendingObjsLock);
-  auto It = PendingObjs.find(&MR);
-  if (It == PendingObjs.end())
+  if (!getPendingDebugObj(MR))
     return;
 
-  DebugObject &DebugObj = *It->second;
-  if (DebugObj.hasFlags(ReportFinalSectionLoadAddresses)) {
-    PassConfig.PostAllocationPasses.push_back(
-        [&DebugObj](LinkGraph &Graph) -> Error {
-          for (const Section &GraphSection : Graph.sections())
-            DebugObj.reportSectionTargetMemoryRange(GraphSection.getName(),
-                                                    SectionRange(GraphSection));
-          return Error::success();
+  PassConfig.PostAllocationPasses.push_back([this, &MR](LinkGraph &G) -> Error {
+    size_t SectionsPatched = 0;
+    bool HasDebugSections = false;
+    DebugObject *DebugObj = getPendingDebugObj(MR);
+    assert(DebugObj && "Don't inject passes if we have no debug object");
+
+    // Step 2: Once the target memory layout is ready, we write the
+    // addresses of the LinkGraph sections into the load-address fields of the
+    // section headers in our debug object allocation
+    DebugObj->visitSections(
+        [&G, &SectionsPatched, &HasDebugSections](StringRef Name) {
+          SectionsPatched += 1;
+          if (isDwarfSection(Name))
+            HasDebugSections = true;
+          Section *S = G.findSectionByName(Name);
+          assert(S && "No graph section for object section");
+          return SectionRange(*S).getStart();
         });
 
-    PassConfig.PreFixupPasses.push_back(
-        [this, &DebugObj, &MR](LinkGraph &G) -> Error {
-          DebugObj.finalizeAsync([this, &DebugObj,
-                                  &MR](Expected<ExecutorAddrRange> TargetMem) {
-            if (!TargetMem) {
-              DebugObj.failMaterialization(TargetMem.takeError());
-              return;
-            }
-            // Update tracking info
-            Error Err = MR.withResourceKeyDo([&](ResourceKey K) {
-              std::lock_guard<std::mutex> LockPending(PendingObjsLock);
-              std::lock_guard<std::mutex> LockRegistered(RegisteredObjsLock);
-              auto It = PendingObjs.find(&MR);
-              RegisteredObjs[K].push_back(std::move(It->second));
-              PendingObjs.erase(It);
-            });
-
-            if (Err)
-              DebugObj.failMaterialization(std::move(Err));
-
-            // Unblock post-fixup pass
-            DebugObj.reportTargetMem(*TargetMem);
-          });
-          return Error::success();
-        });
+    if (!SectionsPatched) {
+      LLVM_DEBUG(dbgs() << "Skipping debug registration for LinkGraph '"
+                        << G.getName() << "': no debug info\n");
+      return Error::success();
+    }
 
-    PassConfig.PostFixupPasses.push_back(
-        [this, &DebugObj](LinkGraph &G) -> Error {
-          Expected<ExecutorAddrRange> R = DebugObj.awaitTargetMem();
-          if (!R)
-            return R.takeError();
-          if (R->empty())
-            return Error::success();
-
-          using namespace shared;
-          G.allocActions().push_back(
-              {cantFail(WrapperFunctionCall::Create<
-                        SPSArgList<SPSExecutorAddrRange, bool>>(
-                   RegistrationAction, *R, AutoRegisterCode)),
-               {/* no deregistration */}});
-          return Error::success();
-        });
-  }
+    if (RequireDebugSections && !HasDebugSections) {
+      LLVM_DEBUG(dbgs() << "Skipping debug registration for LinkGraph '"
+                        << G.getName() << "': no debug info\n");
+      return Error::success();
+    }
+
+    // Step 3: We start copying the debug object into target memory
+    auto &Alloc = DebugObj->getTargetAlloc();
+
+    // FIXME: FA->getAddress() below is supposed to be the address of the memory
+    // range on the target, but InProcessMemoryManager returns the address of a
+    // FinalizedAllocInfo helper instead
+    auto ROSeg = Alloc.getSegInfo(MemProt::Read);
+    ExecutorAddrRange R(ROSeg.Addr, ROSeg.WorkingMem.size());
+    Alloc.finalize([this, R, &MR](Expected<DebugObject::FinalizedAlloc> FA) {
+      DebugObject *DebugObj = getPendingDebugObj(MR);
+      if (!FA)
+        DebugObj->failMaterialization(FA.takeError());
+
+      // Keep allocation alive until the corresponding code is removed
+      DebugObj->trackFinalizedAlloc(std::move(*FA));
+
+      // Unblock post-fixup pass
+      DebugObj->reportTargetMem(R);
+    });
+
+    return Error::success();
+  });
+
+  PassConfig.PostFixupPasses.push_back([this, &MR](LinkGraph &G) -> Error {
+    // Step 4: We wait for the debug object copy to finish, so we can
+    // register the memory range with the GDB JIT Interface in an allocation
+    // action of the LinkGraph's own allocation
+    DebugObject *DebugObj = getPendingDebugObj(MR);
+    Expected<ExecutorAddrRange> R = DebugObj->awaitTargetMem();
+    if (!R)
+      return R.takeError();
+
+    // Step 5: We have to keep the allocation alive until the corresponding
+    // code is removed
+    Error Err = MR.withResourceKeyDo([&](ResourceKey K) {
+      std::lock_guard<std::mutex> LockPending(PendingObjsLock);
+      std::lock_guard<std::mutex> LockRegistered(RegisteredObjsLock);
+      auto It = PendingObjs.find(&MR);
+      RegisteredObjs[K].push_back(std::move(It->second));
+      PendingObjs.erase(It);
+    });
+
+    if (Err)
+      return Err;
+
+    if (R->empty())
+      return Error::success();
+
+    using namespace shared;
+    G.allocActions().push_back(
+        {cantFail(WrapperFunctionCall::Create<
+                  SPSArgList<SPSExecutorAddrRange, bool>>(
+             RegistrationAction, *R, AutoRegisterCode)),
+         {/* no deregistration */}});
+    return Error::success();
+  });
 }
 
 Error ELFDebugObjectPlugin::notifyFailed(MaterializationResponsibility &MR) {

>From f414cfa0a0043a579315ba48bb068496d75b3665 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Tue, 18 Nov 2025 10:55:43 +0100
Subject: [PATCH 2/2] [ORC] Free ELF debug objects in dealloc action of
 corresponding code

---
 .../Orc/Debugging/ELFDebugObjectPlugin.h      |  24 ++--
 .../Orc/Debugging/ELFDebugObjectPlugin.cpp    | 110 +++++++-----------
 2 files changed, 53 insertions(+), 81 deletions(-)

diff --git a/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h b/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
index 92dbfe1c79e6e..7a6e02d910dfa 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.h
@@ -16,6 +16,7 @@
 #include "llvm/ExecutionEngine/JITLink/JITLink.h"
 #include "llvm/ExecutionEngine/Orc/Core.h"
 #include "llvm/ExecutionEngine/Orc/ObjectLinkingLayer.h"
+#include "llvm/ExecutionEngine/Orc/Shared/AllocationActions.h"
 #include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h"
 #include "llvm/Support/Compiler.h"
 #include "llvm/Support/Error.h"
@@ -59,31 +60,34 @@ class LLVM_ABI ELFDebugObjectPlugin : public ObjectLinkingLayer::Plugin {
                            jitlink::LinkGraph &G, jitlink::JITLinkContext &Ctx,
                            MemoryBufferRef InputObj) override;
 
+  Error notifyEmitted(MaterializationResponsibility &MR) override;
   Error notifyFailed(MaterializationResponsibility &MR) override;
-  Error notifyRemovingResources(JITDylib &JD, ResourceKey K) override;
-
-  void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey,
-                                   ResourceKey SrcKey) override;
 
   void modifyPassConfig(MaterializationResponsibility &MR,
                         jitlink::LinkGraph &LG,
                         jitlink::PassConfiguration &PassConfig) override;
 
+  Error notifyRemovingResources(JITDylib &JD, ResourceKey K) override {
+    return Error::success();
+  }
+
+  void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey,
+                                   ResourceKey SrcKey) override {}
+
 private:
   ExecutionSession &ES;
-
-  using OwnedDebugObject = std::unique_ptr<DebugObject>;
-  std::map<MaterializationResponsibility *, OwnedDebugObject> PendingObjs;
-  std::map<ResourceKey, std::vector<OwnedDebugObject>> RegisteredObjs;
+  ExecutorAddr RegistrationAction;
+  ExecutorAddr DeallocAction;
+  ExecutorAddr TargetMemMgr;
 
   std::mutex PendingObjsLock;
-  std::mutex RegisteredObjsLock;
+  std::map<MaterializationResponsibility *, DebugObject> PendingObjs;
 
-  ExecutorAddr RegistrationAction;
   bool RequireDebugSections;
   bool AutoRegisterCode;
 
   DebugObject *getPendingDebugObj(MaterializationResponsibility &MR);
+  shared::AllocActionCallPair createAllocActions(ExecutorAddrRange R);
 };
 
 } // namespace orc
diff --git a/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp b/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
index 966482fef32a5..26126adb594aa 100644
--- a/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
+++ b/llvm/lib/ExecutionEngine/Orc/Debugging/ELFDebugObjectPlugin.cpp
@@ -55,15 +55,6 @@ class DebugObject {
     FinalizeFuture = FinalizePromise.get_future();
   }
 
-  ~DebugObject() {
-    if (Alloc) {
-      std::vector<FinalizedAlloc> Allocs;
-      Allocs.push_back(std::move(Alloc));
-      if (Error Err = MemMgr.deallocate(std::move(Allocs)))
-        ES.reportError(std::move(Err));
-    }
-  }
-
   StringRef getName() const { return Name; }
 
   StringRef getBuffer() {
@@ -78,8 +69,6 @@ class DebugObject {
 
   SimpleSegmentAlloc &getTargetAlloc() { return WorkingMem; }
 
-  void trackFinalizedAlloc(FinalizedAlloc FA) { Alloc = std::move(FA); }
-
   Expected<ExecutorAddrRange> awaitTargetMem() { return FinalizeFuture.get(); }
 
   void reportTargetMem(ExecutorAddrRange TargetMem) {
@@ -106,8 +95,6 @@ class DebugObject {
 
   std::promise<MSVCPExpected<ExecutorAddrRange>> FinalizePromise;
   std::future<MSVCPExpected<ExecutorAddrRange>> FinalizeFuture;
-
-  FinalizedAlloc Alloc;
 };
 
 template <typename ELFT>
@@ -190,8 +177,11 @@ ELFDebugObjectPlugin::ELFDebugObjectPlugin(ExecutionSession &ES,
       AutoRegisterCode(AutoRegisterCode) {
   // Pass bootstrap symbol for registration function to enable debugging
   ErrorAsOutParameter _(&Err);
-  Err = ES.getExecutorProcessControl().getBootstrapSymbols(
-      {{RegistrationAction, rt::RegisterJITLoaderGDBAllocActionName}});
+  Err = ES.getExecutorProcessControl().getBootstrapSymbols({
+      {RegistrationAction, rt::RegisterJITLoaderGDBAllocActionName},
+      {DeallocAction, rt::SimpleExecutorMemoryManagerReleaseWrapperName},
+      {TargetMemMgr, rt::SimpleExecutorMemoryManagerInstanceName},
+  });
 }
 
 ELFDebugObjectPlugin::~ELFDebugObjectPlugin() = default;
@@ -228,19 +218,17 @@ void ELFDebugObjectPlugin::notifyMaterializing(
   }
 
   std::lock_guard<std::mutex> Lock(PendingObjsLock);
-  assert(PendingObjs.count(&MR) == 0 && "One debug object per materialization");
-  PendingObjs[&MR] = std::make_unique<DebugObject>(
-      InputObj.getBufferIdentifier(), std::move(*Alloc), Ctx, ES);
-
-  MutableArrayRef<char> Buffer = PendingObjs[&MR]->getMutBuffer();
-  memcpy(Buffer.data(), InputObj.getBufferStart(), Size);
+  auto [It, Inserted] = PendingObjs.try_emplace(
+      &MR, InputObj.getBufferIdentifier(), std::move(*Alloc), Ctx, ES);
+  assert(Inserted && "One debug object per materialization");
+  memcpy(It->second.getMutBuffer().data(), InputObj.getBufferStart(), Size);
 }
 
 DebugObject *
 ELFDebugObjectPlugin::getPendingDebugObj(MaterializationResponsibility &MR) {
   std::lock_guard<std::mutex> Lock(PendingObjsLock);
   auto It = PendingObjs.find(&MR);
-  return It == PendingObjs.end() ? nullptr : It->second.get();
+  return It == PendingObjs.end() ? nullptr : &It->second;
 }
 
 void ELFDebugObjectPlugin::modifyPassConfig(MaterializationResponsibility &MR,
@@ -283,9 +271,10 @@ void ELFDebugObjectPlugin::modifyPassConfig(MaterializationResponsibility &MR,
     // Step 3: We start copying the debug object into target memory
     auto &Alloc = DebugObj->getTargetAlloc();
 
-    // FIXME: FA->getAddress() below is supposed to be the address of the memory
-    // range on the target, but InProcessMemoryManager returns the address of a
-    // FinalizedAllocInfo helper instead
+    // FIXME: The lookup in the segment info here is a workaround. The below
+    // FA->release() is supposed to provide the base address in target memory,
+    // but InProcessMemoryManager returns the address of a FinalizedAllocInfo
+    // helper instead.
     auto ROSeg = Alloc.getSegInfo(MemProt::Read);
     ExecutorAddrRange R(ROSeg.Addr, ROSeg.WorkingMem.size());
     Alloc.finalize([this, R, &MR](Expected<DebugObject::FinalizedAlloc> FA) {
@@ -293,8 +282,8 @@ void ELFDebugObjectPlugin::modifyPassConfig(MaterializationResponsibility &MR,
       if (!FA)
         DebugObj->failMaterialization(FA.takeError());
 
-      // Keep allocation alive until the corresponding code is removed
-      DebugObj->trackFinalizedAlloc(std::move(*FA));
+      // Dealloc action from the LinkGraph's allocation will free target memory
+      FA->release();
 
       // Unblock post-fixup pass
       DebugObj->reportTargetMem(R);
@@ -311,63 +300,42 @@ void ELFDebugObjectPlugin::modifyPassConfig(MaterializationResponsibility &MR,
     Expected<ExecutorAddrRange> R = DebugObj->awaitTargetMem();
     if (!R)
       return R.takeError();
-
-    // Step 5: We have to keep the allocation alive until the corresponding
-    // code is removed
-    Error Err = MR.withResourceKeyDo([&](ResourceKey K) {
-      std::lock_guard<std::mutex> LockPending(PendingObjsLock);
-      std::lock_guard<std::mutex> LockRegistered(RegisteredObjsLock);
-      auto It = PendingObjs.find(&MR);
-      RegisteredObjs[K].push_back(std::move(It->second));
-      PendingObjs.erase(It);
-    });
-
-    if (Err)
-      return Err;
-
     if (R->empty())
       return Error::success();
 
+    // Step 5: Use allocation actions to (1) register the debug object with the
+    // GDB JIT Interface and (2) free the debug object when the corresponding
+    // code is removed
     using namespace shared;
-    G.allocActions().push_back(
-        {cantFail(WrapperFunctionCall::Create<
-                  SPSArgList<SPSExecutorAddrRange, bool>>(
-             RegistrationAction, *R, AutoRegisterCode)),
-         {/* no deregistration */}});
+    G.allocActions().push_back(createAllocActions(*R));
     return Error::success();
   });
 }
 
-Error ELFDebugObjectPlugin::notifyFailed(MaterializationResponsibility &MR) {
+shared::AllocActionCallPair
+ELFDebugObjectPlugin::createAllocActions(ExecutorAddrRange R) {
+  using namespace shared;
+  // Add the target memory range to __jit_debug_descriptor
+  auto Init = cantFail(
+      WrapperFunctionCall::Create<SPSArgList<SPSExecutorAddrRange, bool>>(
+          RegistrationAction, R, AutoRegisterCode));
+  // Free the debug object's target memory block
+  auto Fini =
+      cantFail(WrapperFunctionCall::Create<
+               SPSArgList<SPSExecutorAddr, SPSSequence<SPSExecutorAddr>>>(
+          DeallocAction, TargetMemMgr, ArrayRef<ExecutorAddr>(R.Start)));
+  return {Init, Fini};
+}
+
+Error ELFDebugObjectPlugin::notifyEmitted(MaterializationResponsibility &MR) {
   std::lock_guard<std::mutex> Lock(PendingObjsLock);
   PendingObjs.erase(&MR);
   return Error::success();
 }
 
-void ELFDebugObjectPlugin::notifyTransferringResources(JITDylib &JD,
-                                                       ResourceKey DstKey,
-                                                       ResourceKey SrcKey) {
-  // Debug objects are stored by ResourceKey only after registration.
-  // Thus, pending objects don't need to be updated here.
-  std::lock_guard<std::mutex> Lock(RegisteredObjsLock);
-  auto SrcIt = RegisteredObjs.find(SrcKey);
-  if (SrcIt != RegisteredObjs.end()) {
-    // Resources from distinct MaterializationResponsibilitys can get merged
-    // after emission, so we can have multiple debug objects per resource key.
-    for (std::unique_ptr<DebugObject> &DebugObj : SrcIt->second)
-      RegisteredObjs[DstKey].push_back(std::move(DebugObj));
-    RegisteredObjs.erase(SrcIt);
-  }
-}
-
-Error ELFDebugObjectPlugin::notifyRemovingResources(JITDylib &JD,
-                                                    ResourceKey Key) {
-  // Removing the resource for a pending object fails materialization, so they
-  // get cleaned up in the notifyFailed() handler.
-  std::lock_guard<std::mutex> Lock(RegisteredObjsLock);
-  RegisteredObjs.erase(Key);
-
-  // TODO: Implement unregister notifications.
+Error ELFDebugObjectPlugin::notifyFailed(MaterializationResponsibility &MR) {
+  std::lock_guard<std::mutex> Lock(PendingObjsLock);
+  PendingObjs.erase(&MR);
   return Error::success();
 }
 



More information about the llvm-commits mailing list