[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 25 03:51:17 PST 2025


================
@@ -413,129 +177,165 @@ 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;
 
+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);
+  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;
 }
 
 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: 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.
----------------
weliveindetail wrote:

Yes, that's an interesting idea. It is the status quo that the debug object lives as long as the corresponding JIT'd code. It was a conservative decision and I hadn't thought about changing it. If we put it into the graph, we cannot change it anymore.

Maybe it's time to investigate the life-time requirements of the debug object. I would be surprised if debuggers don't copy these objects into their own address space. Eventually, they need mutable access to resolve relocations. The question is **when** this is happening, i.e. guaranteed to be done -- especially in LLDB as it attempts to do everything lazily.

I think it's best to wait a bit with this patch. When I find the time, I will check the behavior in GDB and LLDB. There might be room for a new flag to free the debug object memory right after registration. This is connected to the open question of cleaning up the list of debug objects, which I wanted to figure out anyway.

Thanks for you feedback!

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


More information about the llvm-commits mailing list