[lld] [llvm] [LTO][lld] Emit textual assembly code with '--save-temps' (PR #195922)

Joseph Huber via llvm-commits llvm-commits at lists.llvm.org
Wed May 6 08:25:00 PDT 2026


https://github.com/jhuber6 updated https://github.com/llvm/llvm-project/pull/195922

>From 8844a3ddfa9fbfa1058f0bc4f58cf2fad9f6c2d4 Mon Sep 17 00:00:00 2001
From: Joseph Huber <huberjn at outlook.com>
Date: Tue, 5 May 2026 14:41:54 -0500
Subject: [PATCH] [LTO][lld] Emit textual assembly code with '--save-temps'

Summary:
Right now `--save-temps` does not yield `.s` files like you would
recieve if run through `clang foo.bc --save-temps`. This PR adds a
simple hook that outputs a `.s` as well.

The main motivation for this is for the AMD GPU target and offloading
where `--lto-emit-asm` does not work because the linker step occurs in
the middle of the compilation. This means the lack of output causes the
rest of the compilation to fail.

The implementation here just uses `cloneModule` because the typical
pipeline just emits directly to an object file. An alternative approach
would be to emit textual assembly, then create an MC object to turn that
into an object file. I thought this was overkill and likely slower
overall. The only reason I think this approach would be useful is if we
wanted to expose a callback on the target machine, but that would
probably slow down the rest of the pipeline. I will leave this up to
reviewers.

Not by default
---
 lld/ELF/Driver.cpp                |  24 ++---
 lld/test/ELF/lto/save-temps-eq.ll |   8 ++
 llvm/include/llvm/LTO/Config.h    |   5 +
 llvm/lib/LTO/LTOBackend.cpp       | 153 ++++++++++++++++++++++++++----
 4 files changed, 157 insertions(+), 33 deletions(-)

diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index ce8eb25a0d5da..8229ca7a23e0d 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -621,8 +621,8 @@ static void checkZOptions(Ctx &ctx, opt::InputArgList &args) {
 }
 
 constexpr const char *saveTempsValues[] = {
-    "resolution", "preopt",     "promote", "internalize",  "import",
-    "opt",        "precodegen", "prelink", "combinedindex"};
+    "resolution", "preopt",     "promote", "internalize",   "import",
+    "opt",        "precodegen", "prelink", "combinedindex", "asm"};
 
 LinkerDriver::LinkerDriver(Ctx &ctx) : ctx(ctx) {}
 
@@ -1551,16 +1551,16 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
       !args.hasArg(OPT_relocatable) || args.hasArg(OPT_force_group_allocation);
 
   if (args.hasArg(OPT_save_temps)) {
-    // --save-temps implies saving all temps.
-    ctx.arg.saveTempsArgs.insert_range(saveTempsValues);
-  } else {
-    for (auto *arg : args.filtered(OPT_save_temps_eq)) {
-      StringRef s = arg->getValue();
-      if (llvm::is_contained(saveTempsValues, s))
-        ctx.arg.saveTempsArgs.insert(s);
-      else
-        ErrAlways(ctx) << "unknown --save-temps value: " << s;
-    }
+    // --save-temps implies saving all temps except "asm".
+    ctx.arg.saveTempsArgs.insert_range(llvm::make_filter_range(
+        saveTempsValues, [](StringRef Str) { return Str != "asm"; }));
+  }
+  for (auto *arg : args.filtered(OPT_save_temps_eq)) {
+    StringRef s = arg->getValue();
+    if (llvm::is_contained(saveTempsValues, s))
+      ctx.arg.saveTempsArgs.insert(s);
+    else
+      ErrAlways(ctx) << "unknown --save-temps value: " << s;
   }
 
   ctx.arg.searchPaths = args::getStrings(args, OPT_library_path);
diff --git a/lld/test/ELF/lto/save-temps-eq.ll b/lld/test/ELF/lto/save-temps-eq.ll
index 9befc99af9253..240876041dc5a 100644
--- a/lld/test/ELF/lto/save-temps-eq.ll
+++ b/lld/test/ELF/lto/save-temps-eq.ll
@@ -14,6 +14,8 @@
 ;; Create the .all dir with save-temps saving everything, this will be used to compare
 ;; with the output from individualized save-temps later
 ; RUN: ld.lld main.o thin1.o --save-temps -o %t/all/a.out
+;; --save-temps should not produce .s files.
+; RUN: not ls *.s 2>/dev/null
 ; RUN: mv a.out.lto.* *.o.*.bc %t/all
 ;; Sanity check that everything got moved
 ; RUN: ls | count 2
@@ -94,6 +96,12 @@
 ; RUN: mv *.resolution.txt %t/all3
 ; RUN: ls | count 2
 
+;; Check asm.
+; RUN: ld.lld main.o thin1.o --save-temps=asm
+; RUN: cmp %t/all/a.out a.out && rm -f a.out
+; RUN: ls main.o.s thin1.o.s && rm main.o.s thin1.o.s
+; RUN: ls | count 2
+
 ;; If no files were left out from individual stages, the .all3 dir should be identical to .all
 ; RUN: diff -r %t/all %t/all3
 
diff --git a/llvm/include/llvm/LTO/Config.h b/llvm/include/llvm/LTO/Config.h
index 2aeb902bcfccf..76050fe6b6739 100644
--- a/llvm/include/llvm/LTO/Config.h
+++ b/llvm/include/llvm/LTO/Config.h
@@ -257,6 +257,11 @@ struct Config {
   /// splitting the module.
   ModuleHookFn PreCodeGenModuleHook;
 
+  /// This module hook is called before emitting textual assembly.
+  using SaveTempsAsmFn =
+      std::function<void(unsigned Task, const Module &, StringRef AsmText)>;
+  SaveTempsAsmFn SaveTempsAsmHook;
+
   /// A combined index hook is called after all per-module indexes have been
   /// combined (ThinLTO-specific). It can be used to implement -save-temps for
   /// the combined index.
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 7ee2557a68bd5..cec9e30787b9b 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -27,6 +27,18 @@
 #include "llvm/IR/PassManager.h"
 #include "llvm/IR/Verifier.h"
 #include "llvm/LTO/LTO.h"
+#include "llvm/MC/MCAsmBackend.h"
+#include "llvm/MC/MCAsmInfo.h"
+#include "llvm/MC/MCCodeEmitter.h"
+#include "llvm/MC/MCContext.h"
+#include "llvm/MC/MCInstrInfo.h"
+#include "llvm/MC/MCObjectFileInfo.h"
+#include "llvm/MC/MCObjectWriter.h"
+#include "llvm/MC/MCParser/MCAsmParser.h"
+#include "llvm/MC/MCParser/MCTargetAsmParser.h"
+#include "llvm/MC/MCRegisterInfo.h"
+#include "llvm/MC/MCStreamer.h"
+#include "llvm/MC/MCSubtargetInfo.h"
 #include "llvm/MC/TargetRegistry.h"
 #include "llvm/Object/ModuleSymbolTable.h"
 #include "llvm/Passes/PassBuilder.h"
@@ -36,6 +48,7 @@
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/SourceMgr.h"
 #include "llvm/Support/ThreadPool.h"
 #include "llvm/Support/ToolOutputFile.h"
 #include "llvm/Support/VirtualFileSystem.h"
@@ -105,36 +118,40 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
     }
   }
 
+  // If this is the combined module (not a ThinLTO backend compile) or the
+  // user hasn't requested using the input module's path, emit to a file
+  // named from the provided OutputFileName with the Task ID appended.
+  auto getPathPrefix = [=](const Module &M, unsigned Task) -> std::string {
+    if (M.getModuleIdentifier() == "ld-temp.o" || !UseInputModulePath) {
+      std::string Prefix = OutputFileName;
+      if (Task != (unsigned)-1)
+        Prefix += utostr(Task) + ".";
+      return Prefix;
+    }
+    return std::string(M.getModuleIdentifier()) + ".";
+  };
+
+  auto shouldSkipModule =
+      [SaveModNames = SmallVector<std::string, 1>(
+           SaveModulesList.begin(), SaveModulesList.end())](const Module &M) {
+        return !SaveModNames.empty() &&
+               !llvm::is_contained(
+                   SaveModNames,
+                   std::string(llvm::sys::path::filename(M.getName())));
+      };
+
   auto setHook = [&](std::string PathSuffix, ModuleHookFn &Hook) {
     // Keep track of the hook provided by the linker, which also needs to run.
     ModuleHookFn LinkerHook = Hook;
-    Hook = [=, SaveModNames = llvm::SmallVector<std::string, 1>(
-                   SaveModulesList.begin(), SaveModulesList.end())](
-               unsigned Task, const Module &M) {
-      // If SaveModulesList is not empty, only do save-temps if the module's
-      // filename (without path) matches a name in the list.
-      if (!SaveModNames.empty() &&
-          !llvm::is_contained(
-              SaveModNames,
-              std::string(llvm::sys::path::filename(M.getName()))))
+    Hook = [=](unsigned Task, const Module &M) {
+      if (shouldSkipModule(M))
         return false;
-
       // If the linker's hook returned false, we need to pass that result
       // through.
       if (LinkerHook && !LinkerHook(Task, M))
         return false;
 
-      std::string PathPrefix;
-      // If this is the combined module (not a ThinLTO backend compile) or the
-      // user hasn't requested using the input module's path, emit to a file
-      // named from the provided OutputFileName with the Task ID appended.
-      if (M.getModuleIdentifier() == "ld-temp.o" || !UseInputModulePath) {
-        PathPrefix = OutputFileName;
-        if (Task != (unsigned)-1)
-          PathPrefix += utostr(Task) + ".";
-      } else
-        PathPrefix = M.getModuleIdentifier() + ".";
-      std::string Path = PathPrefix + PathSuffix + ".bc";
+      std::string Path = getPathPrefix(M, Task) + PathSuffix + ".bc";
       std::error_code EC;
       raw_fd_ostream OS(Path, EC, sys::fs::OpenFlags::OF_None);
       // Because -save-temps is a debugging feature, we report the error
@@ -146,6 +163,19 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
     };
   };
 
+  auto setAsmHook = [&]() {
+    SaveTempsAsmHook = [=](unsigned Task, const Module &M, StringRef AsmText) {
+      if (shouldSkipModule(M))
+        return;
+      std::string Path = getPathPrefix(M, Task) + "s";
+      std::error_code EC;
+      raw_fd_ostream OS(Path, EC, sys::fs::OF_Text);
+      if (EC)
+        reportOpenError(Path, EC.message());
+      OS << AsmText;
+    };
+  };
+
   auto SaveCombinedIndex =
       [=](const ModuleSummaryIndex &Index,
           const DenseSet<GlobalValue::GUID> &GUIDPreservedSymbols) {
@@ -189,6 +219,8 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
       setHook("5.precodegen", PreCodeGenModuleHook);
     if (SaveTempsArgs.contains("combinedindex"))
       CombinedIndexHook = SaveCombinedIndex;
+    if (SaveTempsArgs.contains("asm"))
+      setAsmHook();
   }
 
   return Error::success();
@@ -416,6 +448,44 @@ bool lto::opt(const Config &Conf, TargetMachine *TM, unsigned Task, Module &Mod,
   return !Conf.PostOptModuleHook || Conf.PostOptModuleHook(Task, Mod);
 }
 
+// Converts textual assembly code into the requested object format.
+static void assemble(const Target *T, const TargetMachine *TM,
+                     StringRef AsmText, raw_pwrite_stream &OS) {
+  const llvm::Triple &TT = TM->getTargetTriple();
+  std::unique_ptr<MCRegisterInfo> MRI(T->createMCRegInfo(TT));
+  std::unique_ptr<MCAsmInfo> MAI(
+      T->createMCAsmInfo(*MRI, TT, TM->Options.MCOptions));
+  std::unique_ptr<MCSubtargetInfo> STI(T->createMCSubtargetInfo(
+      TT, TM->getTargetCPU(), TM->getTargetFeatureString()));
+  std::unique_ptr<MCInstrInfo> MCII(T->createMCInstrInfo());
+
+  MCContext Ctx(TT, *MAI, *MRI, *STI);
+  std::unique_ptr<MCObjectFileInfo> MOFI(
+      T->createMCObjectFileInfo(Ctx, /*PIC=*/false));
+  Ctx.setObjectFileInfo(MOFI.get());
+
+  std::unique_ptr<MCCodeEmitter> CE(T->createMCCodeEmitter(*MCII, Ctx));
+  std::unique_ptr<MCAsmBackend> MAB(
+      T->createMCAsmBackend(*STI, *MRI, TM->Options.MCOptions));
+  std::unique_ptr<MCObjectWriter> OW = MAB->createObjectWriter(OS);
+  std::unique_ptr<MCStreamer> Streamer(T->createMCObjectStreamer(
+      TT, Ctx, std::move(MAB), std::move(OW), std::move(CE), *STI));
+
+  auto Buf = MemoryBuffer::getMemBuffer(AsmText, "<save-temps-asm>", false);
+  SourceMgr SrcMgr;
+  SrcMgr.AddNewSourceBuffer(std::move(Buf), SMLoc());
+
+  std::unique_ptr<MCAsmParser> Parser(
+      createMCAsmParser(SrcMgr, Ctx, *Streamer, *MAI));
+  std::unique_ptr<MCTargetAsmParser> TAP(
+      T->createMCAsmParser(*STI, *Parser, *MCII));
+  if (!TAP)
+    report_fatal_error("Failed to create target asm parser");
+  Parser->setTargetParser(*TAP);
+  if (Parser->Run(false))
+    report_fatal_error("Failed to assemble save-temps assembly output");
+}
+
 static void codegen(const Config &Conf, TargetMachine *TM,
                     AddStreamFn AddStream, unsigned Task, Module &Mod,
                     const ModuleSummaryIndex &CombinedIndex) {
@@ -451,6 +521,47 @@ static void codegen(const Config &Conf, TargetMachine *TM,
                          EC.message());
   }
 
+  // Emit an intermediate assembly file phase if requested by the user.
+  if (Conf.SaveTempsAsmHook) {
+    SmallString<0> AsmBuf;
+    raw_svector_ostream AsmOS(AsmBuf);
+
+    legacy::PassManager CodeGenPasses;
+    TargetLibraryInfoImpl TLII(Mod.getTargetTriple(), TM->Options.VecLib);
+    CodeGenPasses.add(new TargetLibraryInfoWrapperPass(TLII));
+    CodeGenPasses.add(new RuntimeLibraryInfoWrapper(
+        Mod.getTargetTriple(), TM->Options.ExceptionModel,
+        TM->Options.FloatABIType, TM->Options.EABIVersion,
+        TM->Options.MCOptions.ABIName, TM->Options.VecLib));
+    if (!isEmptyModule(Mod))
+      CodeGenPasses.add(
+          createImmutableModuleSummaryIndexWrapperPass(&CombinedIndex));
+    if (Conf.PreCodeGenPassesHook)
+      Conf.PreCodeGenPassesHook(CodeGenPasses);
+    if (TM->addPassesToEmitFile(CodeGenPasses, AsmOS, nullptr,
+                                CodeGenFileType::AssemblyFile))
+      report_fatal_error("Failed to setup codegen");
+    CodeGenPasses.run(Mod);
+
+    Conf.SaveTempsAsmHook(Task, Mod, AsmBuf.str());
+
+    Expected<std::unique_ptr<CachedFileStream>> StreamOrErr =
+        AddStream(Task, Mod.getModuleIdentifier());
+    if (Error Err = StreamOrErr.takeError())
+      report_fatal_error(std::move(Err));
+    std::unique_ptr<CachedFileStream> &Stream = *StreamOrErr;
+    TM->Options.ObjectFilenameForDebug = Stream->ObjectPathName;
+
+    assemble(&TM->getTarget(), TM, AsmBuf.str(), *Stream->OS);
+
+    if (DwoOut)
+      DwoOut->keep();
+
+    if (Error Err = Stream->commit())
+      report_fatal_error(std::move(Err));
+    return;
+  }
+
   Expected<std::unique_ptr<CachedFileStream>> StreamOrErr =
       AddStream(Task, Mod.getModuleIdentifier());
   if (Error Err = StreamOrErr.takeError())



More information about the llvm-commits mailing list