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

Joseph Huber via llvm-commits llvm-commits at lists.llvm.org
Tue May 5 12:50:07 PDT 2026


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

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.


>From 7fc611fe4f67e305ae68d5c52fe4606858ae6261 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.
---
 lld/ELF/Driver.cpp             |  4 ++--
 llvm/include/llvm/LTO/Config.h |  4 ++++
 llvm/lib/LTO/LTOBackend.cpp    | 29 +++++++++++++++++++++++++++++
 3 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index ce8eb25a0d5da..2b872f5675a6e 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) {}
 
diff --git a/llvm/include/llvm/LTO/Config.h b/llvm/include/llvm/LTO/Config.h
index 2aeb902bcfccf..2523b49158e30 100644
--- a/llvm/include/llvm/LTO/Config.h
+++ b/llvm/include/llvm/LTO/Config.h
@@ -95,6 +95,10 @@ struct Config {
   /// want to know a priori all possible output files.
   bool AlwaysEmitRegularLTOObj = false;
 
+  /// When non-empty, LTO codegen will also emit textual assembly to this
+  /// prefix.
+  std::string SaveTempsAsmPrefix;
+
   /// If true, the LTO instance creates copies of the symbol names for LTO::run.
   /// The lld linker uses string saver to keep symbol names alive and doesn't
   /// need to create copies, so it can set this field to false.
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 7ee2557a68bd5..9736811a0c09f 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -43,6 +43,7 @@
 #include "llvm/Target/TargetMachine.h"
 #include "llvm/TargetParser/SubtargetFeature.h"
 #include "llvm/Transforms/IPO/WholeProgramDevirt.h"
+#include "llvm/Transforms/Utils/Cloning.h"
 #include "llvm/Transforms/Utils/FunctionImportUtils.h"
 #include "llvm/Transforms/Utils/SplitModule.h"
 #include <optional>
@@ -173,6 +174,7 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
     setHook("3.import", PostImportModuleHook);
     setHook("4.opt", PostOptModuleHook);
     setHook("5.precodegen", PreCodeGenModuleHook);
+    SaveTempsAsmPrefix = OutputFileName;
     CombinedIndexHook = SaveCombinedIndex;
   } else {
     if (SaveTempsArgs.contains("preopt"))
@@ -189,6 +191,8 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
       setHook("5.precodegen", PreCodeGenModuleHook);
     if (SaveTempsArgs.contains("combinedindex"))
       CombinedIndexHook = SaveCombinedIndex;
+    if (SaveTempsArgs.contains("asm"))
+      SaveTempsAsmPrefix = OutputFileName;
   }
 
   return Error::success();
@@ -423,6 +427,31 @@ static void codegen(const Config &Conf, TargetMachine *TM,
   if (Conf.PreCodeGenModuleHook && !Conf.PreCodeGenModuleHook(Task, Mod))
     return;
 
+  if (!Conf.SaveTempsAsmPrefix.empty()) {
+    std::string PathPrefix;
+    if (Mod.getModuleIdentifier() == "ld-temp.o") {
+      PathPrefix = Conf.SaveTempsAsmPrefix;
+      if (Task != (unsigned)-1)
+        PathPrefix += utostr(Task) + ".";
+    } else {
+      PathPrefix = Mod.getModuleIdentifier() + ".";
+    }
+    std::string AsmPath = PathPrefix + "s";
+    std::error_code EC;
+    raw_fd_ostream AsmOS(AsmPath, EC, sys::fs::OF_Text);
+    if (EC)
+      reportOpenError(AsmPath, EC.message());
+    auto ClonedMod = CloneModule(Mod);
+    legacy::PassManager AsmPasses;
+    TargetLibraryInfoImpl TLII(ClonedMod->getTargetTriple(),
+                               TM->Options.VecLib);
+    AsmPasses.add(new TargetLibraryInfoWrapperPass(TLII));
+    if (TM->addPassesToEmitFile(AsmPasses, AsmOS, nullptr,
+                                CodeGenFileType::AssemblyFile))
+      report_fatal_error("Failed to setup assembly codegen");
+    AsmPasses.run(*ClonedMod);
+  }
+
   if (EmbedBitcode == LTOBitcodeEmbedding::EmbedOptimized)
     llvm::embedBitcodeInModule(Mod, llvm::MemoryBufferRef(),
                                /*EmbedBitcode*/ true,



More information about the llvm-commits mailing list