[llvm] [llvm-objdump] Add --substitute-path and --source-dir for --source (PR #201096)

via llvm-commits llvm-commits at lists.llvm.org
Fri Jun 12 02:43:15 PDT 2026


https://github.com/VladimirMedic updated https://github.com/llvm/llvm-project/pull/201096

>From fb1adebaec1ac1563566f661c1752355e37a550a Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Tue, 2 Jun 2026 12:24:53 +0200
Subject: [PATCH 1/7] [llvm-objdump] Add --substitute-path and --source-dir for
 --source

---
 llvm/docs/CommandGuide/llvm-objdump.rst       |  23 ++++
 .../tools/llvm-objdump/Inputs/source-path.c   |   9 ++
 .../tools/llvm-objdump/Inputs/source-path.ll  |  65 ++++++++++
 llvm/test/tools/llvm-objdump/source-path.test |  73 ++++++++++++
 llvm/tools/llvm-objdump/ObjdumpOpts.td        |  14 +++
 llvm/tools/llvm-objdump/SourcePrinter.cpp     | 111 +++++++++++++++++-
 llvm/tools/llvm-objdump/llvm-objdump.cpp      |  13 ++
 llvm/tools/llvm-objdump/llvm-objdump.h        |   4 +
 8 files changed, 310 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/tools/llvm-objdump/Inputs/source-path.c
 create mode 100644 llvm/test/tools/llvm-objdump/Inputs/source-path.ll
 create mode 100644 llvm/test/tools/llvm-objdump/source-path.test

diff --git a/llvm/docs/CommandGuide/llvm-objdump.rst b/llvm/docs/CommandGuide/llvm-objdump.rst
index d7e2ed0015950..f3e280a8987dc 100644
--- a/llvm/docs/CommandGuide/llvm-objdump.rst
+++ b/llvm/docs/CommandGuide/llvm-objdump.rst
@@ -238,6 +238,29 @@ OPTIONS
   initial directories from absolute paths. This option has no effect without
   :option:`--prefix`.
 
+.. option:: --substitute-path <from> <to>
+
+  When disassembling with the :option:`--source` option, replace ``from`` with
+  ``to`` at the start of the directory part of source file paths when looking up
+  sources. A rule is applied only if ``from`` ends at a directory separator in
+  the path. This option may be specified multiple times; rules are evaluated in
+  the order given and the first matching rule is used, as in GDB
+  ``set substitute-path``.
+
+.. option:: --source-dir=<dir>
+
+.. option:: -source-dir <dir>
+
+  When disassembling with the :option:`--source` option, add ``dir`` to the
+  front of the source search path when looking up source files. For each source
+  file, llvm-objdump tries the path from the debug info, then each search
+  directory with the full path appended, then each search directory with only
+  the file name.
+
+  Multiple directories may be given in one argument separated by ``:`` (``;``
+  on Windows). This option may be specified multiple times; each directory is
+  appended to the search path in the order given.
+
 .. option:: --print-imm-hex
 
   Use hex format when printing immediate values in disassembly output (default).
diff --git a/llvm/test/tools/llvm-objdump/Inputs/source-path.c b/llvm/test/tools/llvm-objdump/Inputs/source-path.c
new file mode 100644
index 0000000000000..d5ef46ff62673
--- /dev/null
+++ b/llvm/test/tools/llvm-objdump/Inputs/source-path.c
@@ -0,0 +1,9 @@
+int a = 1;
+int foo() {
+  return a;
+}
+
+int main() {
+  int *b = &a;
+  return *b + foo();
+}
diff --git a/llvm/test/tools/llvm-objdump/Inputs/source-path.ll b/llvm/test/tools/llvm-objdump/Inputs/source-path.ll
new file mode 100644
index 0000000000000..a202dda2d5f6e
--- /dev/null
+++ b/llvm/test/tools/llvm-objdump/Inputs/source-path.ll
@@ -0,0 +1,65 @@
+; NOTE: To use this file as input, replace SRC_COMPDIR with a directory path via sed.
+
+; ModuleID = 'source-path.bc'
+source_filename = "source-path.c"
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+ at a = global i32 1, align 4, !dbg !4
+
+; Function Attrs: nounwind uwtable
+define i32 @foo() #0 !dbg !9 {
+entry:
+  %0 = load i32, ptr @a, align 4, !dbg !12
+  ret i32 %0, !dbg !13
+}
+
+; Function Attrs: nounwind uwtable
+define i32 @main() #0 !dbg !14 {
+entry:
+  %retval = alloca i32, align 4
+  %b = alloca ptr, align 8
+  store i32 0, ptr %retval, align 4
+  call void @llvm.dbg.declare(metadata ptr %b, metadata !15, metadata !17), !dbg !18
+  store ptr @a, ptr %b, align 8, !dbg !18
+  %0 = load ptr, ptr %b, align 8, !dbg !19
+  %1 = load i32, ptr %0, align 4, !dbg !20
+  %call = call i32 @foo(), !dbg !21
+  %add = add nsw i32 %1, %call, !dbg !22
+  ret i32 %add, !dbg !23
+}
+
+; Function Attrs: nounwind readnone
+declare void @llvm.dbg.declare(metadata, metadata, metadata) #1
+
+attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "frame-pointer"="all" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
+attributes #1 = { nounwind readnone }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!6, !7}
+!llvm.ident = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang version 4.0.0", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, globals: !3)
+!1 = !DIFile(filename: "source-path.c", directory: "SRC_COMPDIR")
+!2 = !{}
+!3 = !{!4}
+!4 = !DIGlobalVariableExpression(var: !DIGlobalVariable(name: "a", scope: !0, file: !1, line: 1, type: !5, isLocal: false, isDefinition: true), expr: !DIExpression())
+!5 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
+!6 = !{i32 2, !"Dwarf Version", i32 4}
+!7 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = !{!"clang version 4.0.0"}
+!9 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 2, type: !10, isLocal: false, isDefinition: true, scopeLine: 2, isOptimized: false, unit: !0, retainedNodes: !2)
+!10 = !DISubroutineType(types: !11)
+!11 = !{!5}
+!12 = !DILocation(line: 3, column: 10, scope: !9)
+!13 = !DILocation(line: 3, column: 3, scope: !9)
+!14 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 6, type: !10, isLocal: false, isDefinition: true, scopeLine: 6, isOptimized: false, unit: !0, retainedNodes: !2)
+!15 = !DILocalVariable(name: "b", scope: !14, file: !1, line: 7, type: !16)
+!16 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64, align: 64)
+!17 = !DIExpression()
+!18 = !DILocation(line: 7, column: 8, scope: !14)
+!19 = !DILocation(line: 8, column: 11, scope: !14)
+!20 = !DILocation(line: 8, column: 10, scope: !14)
+!21 = !DILocation(line: 8, column: 15, scope: !14)
+!22 = !DILocation(line: 8, column: 13, scope: !14)
+!23 = !DILocation(line: 8, column: 3, scope: !14)
diff --git a/llvm/test/tools/llvm-objdump/source-path.test b/llvm/test/tools/llvm-objdump/source-path.test
new file mode 100644
index 0000000000000..676da131c69fa
--- /dev/null
+++ b/llvm/test/tools/llvm-objdump/source-path.test
@@ -0,0 +1,73 @@
+;; Test --substitute-path and --source-dir with --source and --line-numbers.
+;;
+;; The object records source paths under /wrong/Inputs, but the source file
+;; lives in this test's Inputs directory. Without path remapping, --source
+;; cannot load the file from disk.
+;;
+;; --source-dir: each flag is prepended to the search list, so later flags on
+;; the command line are tried first.
+;;
+;; --substitute-path: rules are tried in command-line order; the first match
+;; wins (same as GDB set substitute-path).
+
+; RUN: sed -e "s,SRC_COMPDIR,/wrong/Inputs,g" %p/Inputs/source-path.ll > %t.ll
+; RUN: llc -o %t.o -filetype=obj -mtriple=x86_64-pc-linux %t.ll
+; REQUIRES: x86-registered-target
+
+; RUN: llvm-objdump --no-print-imm-hex -d --line-numbers %t.o | FileCheck %s --check-prefix=LINE
+; RUN: llvm-objdump --no-print-imm-hex -d --source %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
+
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+; RUN: llvm-objdump --no-print-imm-hex -d --source --source-dir %/p/Inputs %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wron %/p %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
+
+; First --source-dir misses (no source-path.c under %/p alone); second finds
+; Inputs/source-path.c via basename lookup.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --source-dir %/p/Inputs --source-dir %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; More specific rule listed first wins (GDB example: /wrong/Inputs before /wrong).
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong/Inputs %/p/Inputs \
+; RUN:   --substitute-path /wrong /nowhere %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; Broader rule listed first matches; later rule is not applied (miss on remapped path).
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong /nowhere \
+; RUN:   --substitute-path /wrong/Inputs %/p/Inputs %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING-SUBST -DFILE=%t.o
+
+; First substitute rule is a no-match; second matching rule remaps the path.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wron /nowhere \
+; RUN:   --substitute-path /wrong %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+; LINE: <foo>:
+; LINE-NEXT: ; foo():
+; LINE-NEXT: ; {{.*}}source-path.c:2
+; LINE-NEXT: pushq
+; LINE: ; {{.*}}source-path.c:3
+; LINE-NEXT: movq
+; LINE-NEXT: movl
+; LINE-NEXT: popq
+; LINE-NEXT: retq
+
+; SOURCE-MISSING: warning: '[[FILE]]': failed to find source {{[/\\]}}wrong{{[/\\]}}Inputs{{[/\\]}}source-path.c
+
+; SOURCE-MISSING-SUBST: warning: '[[FILE]]': failed to find source {{[/\\]}}nowhere{{[/\\]}}Inputs{{[/\\]}}source-path.c
+
+; SOURCE: <foo>:
+; SOURCE-NEXT: ; int foo() {
+; SOURCE-NEXT: pushq
+; SOURCE: ; return a;
+; SOURCE-NEXT: movq
+; SOURCE-NEXT: movl
+; SOURCE-NEXT: popq
+; SOURCE-NEXT: retq
diff --git a/llvm/tools/llvm-objdump/ObjdumpOpts.td b/llvm/tools/llvm-objdump/ObjdumpOpts.td
index 73f4c48abb19f..3541d9a71dec5 100644
--- a/llvm/tools/llvm-objdump/ObjdumpOpts.td
+++ b/llvm/tools/llvm-objdump/ObjdumpOpts.td
@@ -246,6 +246,20 @@ def : Flag<["-"], "w">, Alias<wide>;
 
 defm prefix : Eq<"prefix", "Add prefix to absolute paths">,
               MetaVarName<"prefix">;
+defm source_dir :
+  Eq<"source-dir",
+     "Add a directory to the front of the source search path when looking up "
+     "source files. Multiple directories may be given in one argument "
+     "separated by ':' (';' on Windows)">,
+  MetaVarName<"<dir>">;
+def : Separate<["-"], "source-dir">, Alias<source_dir>;
+
+def substitute_path : MultiArg<["--"], "substitute-path", 2>,
+  MetaVarName<"<from> <to>">,
+  HelpText<"Replace <from> with <to> at the start of the directory part of "
+           "source file paths when looking up sources. The rule is applied "
+           "only if <from> ends at a directory separator in the path. May be "
+           "specified multiple times.">;
 defm prefix_strip
     : Eq<"prefix-strip", "Strip out initial directories from absolute "
                          "paths. No effect without --prefix">,
diff --git a/llvm/tools/llvm-objdump/SourcePrinter.cpp b/llvm/tools/llvm-objdump/SourcePrinter.cpp
index a92b955a49cde..a212b495162c0 100644
--- a/llvm/tools/llvm-objdump/SourcePrinter.cpp
+++ b/llvm/tools/llvm-objdump/SourcePrinter.cpp
@@ -15,16 +15,113 @@
 #include "SourcePrinter.h"
 #include "llvm-objdump.h"
 #include "llvm/ADT/SmallSet.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/DebugInfo/DWARF/DWARFExpressionPrinter.h"
 #include "llvm/DebugInfo/DWARF/LowLevel/DWARFExpression.h"
 #include "llvm/Demangle/Demangle.h"
+#include "llvm/Support/FileSystem.h"
 #include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/Path.h"
 
 #define DEBUG_TYPE "objdump"
 
 namespace llvm {
 namespace objdump {
 
+static bool sourceFileExists(StringRef Path) {
+  if (sys::fs::exists(Path) && !sys::fs::is_directory(Path))
+    return true;
+
+  return false;
+}
+
+static void normalizeSourcePath(SmallVectorImpl<char> &Path) {
+  sys::path::native(Path);
+  sys::path::remove_dots(Path, /*remove_dot_dot=*/true);
+}
+
+static std::optional<std::string> trySourcePath(StringRef Path) {
+  SmallString<256> Normalized(Path);
+  normalizeSourcePath(Normalized);
+  if (sourceFileExists(Normalized))
+    return std::string(Normalized);
+  return std::nullopt;
+}
+
+static std::optional<std::string>
+searchSourceWithDirs(StringRef FileName, ArrayRef<StringRef> SearchDirs,
+                     bool TryLiteralFirst) {
+  if (TryLiteralFirst)
+    if (auto Path = trySourcePath(FileName))
+      return Path;
+
+  StringRef PathSuffix = sys::path::relative_path(FileName);
+  StringRef BaseName = sys::path::filename(FileName);
+  for (StringRef Dir : SearchDirs) {
+    SmallString<256> Candidate(Dir);
+    sys::path::append(Candidate, PathSuffix);
+    if (auto Path = trySourcePath(Candidate))
+      return Path;
+  }
+  for (StringRef Dir : SearchDirs) {
+    SmallString<256> Candidate(Dir);
+    sys::path::append(Candidate, BaseName);
+    if (auto Path = trySourcePath(Candidate))
+      return Path;
+  }
+  return std::nullopt;
+}
+
+static std::optional<std::string>
+findSourceFilePath(StringRef FileName, ArrayRef<StringRef> SearchDirs) {
+  if (FileName.empty() || FileName == DILineInfo::BadString)
+    return std::nullopt;
+
+  if (sys::path::is_absolute_gnu(FileName))
+    return searchSourceWithDirs(FileName, SearchDirs, /*TryLiteralFirst=*/true);
+  return searchSourceWithDirs(FileName, SearchDirs, /*TryLiteralFirst=*/false);
+}
+
+static std::string applySubstitutePaths(StringRef FileName) {
+  if (SubstitutePaths.empty())
+    return FileName.str();
+
+  StringRef BaseName = sys::path::filename(FileName);
+  SmallString<256> Directory(sys::path::parent_path(FileName));
+  normalizeSourcePath(Directory);
+
+  for (const auto &[From, To] : SubstitutePaths) {
+    if (From.empty())
+      continue;
+
+    SmallString<256> FromPath(From);
+    normalizeSourcePath(FromPath);
+    StringRef Dir = Directory;
+    if (!Dir.starts_with(FromPath))
+      continue;
+    if (Dir.size() > FromPath.size() &&
+        !sys::path::is_separator(Dir[FromPath.size()]))
+      continue;
+
+    SmallString<256> NewDir(To);
+    StringRef Suffix = Dir.substr(FromPath.size());
+    while (!Suffix.empty() && sys::path::is_separator(Suffix.front()))
+      Suffix = Suffix.drop_front();
+    if (!Suffix.empty())
+      sys::path::append(NewDir, Suffix);
+    normalizeSourcePath(NewDir);
+
+    if (NewDir.empty())
+      return BaseName.str();
+    SmallString<256> Result(NewDir);
+    sys::path::append(Result, BaseName);
+    normalizeSourcePath(Result);
+    return std::string(Result);
+  }
+
+  return FileName.str();
+}
+
 bool InlinedFunction::liveAtAddress(object::SectionedAddress Addr) const {
   if (!Range.valid())
     return false;
@@ -608,8 +705,16 @@ bool SourcePrinter::cacheSource(const DILineInfo &LineInfo) {
   if (LineInfo.Source) {
     Buffer = MemoryBuffer::getMemBuffer(*LineInfo.Source);
   } else {
-    auto BufferOrError =
-        MemoryBuffer::getFile(LineInfo.FileName, /*IsText=*/true);
+    std::string PathToOpen = LineInfo.FileName;
+    if (!SourceDirs.empty()) {
+      SmallVector<StringRef, 8> SearchDirs;
+      for (const std::string &Dir : SourceDirs)
+        SearchDirs.push_back(Dir);
+      if (auto Resolved = findSourceFilePath(LineInfo.FileName, SearchDirs))
+        PathToOpen = std::move(*Resolved);
+    }
+
+    auto BufferOrError = MemoryBuffer::getFile(PathToOpen, /*IsText=*/true);
     if (!BufferOrError) {
       if (MissingSources.insert(LineInfo.FileName).second)
         reportWarning("failed to find source " + LineInfo.FileName,
@@ -654,6 +759,8 @@ void SourcePrinter::printSourceLine(formatted_raw_ostream &OS,
                       toString(ExpectedLineInfo.takeError()),
                   ObjectFilename);
   }
+  if (!objdump::SubstitutePaths.empty())
+    LineInfo.FileName = applySubstitutePaths(LineInfo.FileName);
 
   if (!objdump::Prefix.empty() &&
       sys::path::is_absolute_gnu(LineInfo.FileName)) {
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.cpp b/llvm/tools/llvm-objdump/llvm-objdump.cpp
index 1da9b5771fd3f..1311bf9625fd2 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.cpp
+++ b/llvm/tools/llvm-objdump/llvm-objdump.cpp
@@ -344,6 +344,8 @@ bool objdump::UnwindInfo;
 bool objdump::UnwindShowWODPool;
 std::string objdump::Prefix;
 uint32_t objdump::PrefixStrip;
+std::vector<std::pair<std::string, std::string>> objdump::SubstitutePaths;
+std::vector<std::string> objdump::SourceDirs;
 
 DebugFormat objdump::DbgVariables = DFDisabled;
 DebugFormat objdump::DbgInlinedFunctions = DFDisabled;
@@ -3904,6 +3906,17 @@ static void parseObjdumpOptions(const llvm::opt::InputArgList &InputArgs) {
   UnwindShowWODPool = InputArgs.hasArg(OBJDUMP_unwind_show_wod_pool);
   Prefix = InputArgs.getLastArgValue(OBJDUMP_prefix).str();
   parseIntArg(InputArgs, OBJDUMP_prefix_strip, PrefixStrip);
+  for (const opt::Arg *A : InputArgs.filtered(OBJDUMP_substitute_path))
+    SubstitutePaths.emplace_back(A->getValue(0), A->getValue(1));
+  for (StringRef Dirs : InputArgs.getAllArgValues(OBJDUMP_source_dir)) {
+#if defined(_WIN32)
+    for (StringRef Dir : llvm::split(Dirs, ";"))
+#else
+    for (StringRef Dir : llvm::split(Dirs, ":"))
+#endif
+      if (!Dir.empty())
+        SourceDirs.insert(SourceDirs.end(), Dir.str());
+  }
   if (const opt::Arg *A = InputArgs.getLastArg(OBJDUMP_debug_vars_EQ)) {
     DbgVariables = StringSwitch<DebugFormat>(A->getValue())
                        .Case("ascii", DFASCII)
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.h b/llvm/tools/llvm-objdump/llvm-objdump.h
index 208bac8be3b3f..2241e64a6d81b 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.h
+++ b/llvm/tools/llvm-objdump/llvm-objdump.h
@@ -18,6 +18,8 @@
 #include "llvm/Support/FormattedStream.h"
 #include <functional>
 #include <memory>
+#include <utility>
+#include <vector>
 
 namespace llvm {
 class StringRef;
@@ -66,6 +68,8 @@ extern std::vector<std::string> MAttrs;
 extern std::string MCPU;
 extern std::string Prefix;
 extern uint32_t PrefixStrip;
+extern std::vector<std::pair<std::string, std::string>> SubstitutePaths;
+extern std::vector<std::string> SourceDirs;
 extern bool PrintImmHex;
 extern bool PrintLines;
 extern bool PrintSource;

>From 7838cb729fd56950f84ef5eb729b7558b1057d4c Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Fri, 5 Jun 2026 12:23:46 +0200
Subject: [PATCH 2/7] Addressing review comments.

---
 llvm/docs/CommandGuide/llvm-objdump.rst       |  9 +--
 .../tools/llvm-objdump/Inputs/source-path.c   |  9 ---
 .../tools/llvm-objdump/Inputs/source-path.ll  | 65 -----------------
 llvm/test/tools/llvm-objdump/source-path.test | 73 -------------------
 llvm/tools/llvm-objdump/ObjdumpOpts.td        | 12 ++-
 llvm/tools/llvm-objdump/SourcePrinter.cpp     |  7 +-
 llvm/tools/llvm-objdump/llvm-objdump.cpp      | 21 +++---
 llvm/tools/llvm-objdump/llvm-objdump.h        |  2 +-
 8 files changed, 21 insertions(+), 177 deletions(-)
 delete mode 100644 llvm/test/tools/llvm-objdump/Inputs/source-path.c
 delete mode 100644 llvm/test/tools/llvm-objdump/Inputs/source-path.ll
 delete mode 100644 llvm/test/tools/llvm-objdump/source-path.test

diff --git a/llvm/docs/CommandGuide/llvm-objdump.rst b/llvm/docs/CommandGuide/llvm-objdump.rst
index f3e280a8987dc..b3069515f8b6a 100644
--- a/llvm/docs/CommandGuide/llvm-objdump.rst
+++ b/llvm/docs/CommandGuide/llvm-objdump.rst
@@ -39,8 +39,6 @@ combined with other commands:
 
 .. option:: --disassemble-symbols=<symbol1[,symbol2,...]>
 
-.. option:: --disassemble=symbol1 --disassemble=symbol2 ...
-
   Disassemble only the specified symbols. Takes demangled symbol names when
   :option:`--demangle` is specified, otherwise takes mangled symbol names.
   Implies :option:`--disassemble`.
@@ -247,7 +245,7 @@ OPTIONS
   the order given and the first matching rule is used, as in GDB
   ``set substitute-path``.
 
-.. option:: --source-dir=<dir>
+.. option:: --source-dir <dir>
 
 .. option:: -source-dir <dir>
 
@@ -257,9 +255,8 @@ OPTIONS
   directory with the full path appended, then each search directory with only
   the file name.
 
-  Multiple directories may be given in one argument separated by ``:`` (``;``
-  on Windows). This option may be specified multiple times; each directory is
-  appended to the search path in the order given.
+  This option may be specified multiple times; each ``--source-dir`` adds one
+  directory. Later options on the command line are searched before earlier ones.
 
 .. option:: --print-imm-hex
 
diff --git a/llvm/test/tools/llvm-objdump/Inputs/source-path.c b/llvm/test/tools/llvm-objdump/Inputs/source-path.c
deleted file mode 100644
index d5ef46ff62673..0000000000000
--- a/llvm/test/tools/llvm-objdump/Inputs/source-path.c
+++ /dev/null
@@ -1,9 +0,0 @@
-int a = 1;
-int foo() {
-  return a;
-}
-
-int main() {
-  int *b = &a;
-  return *b + foo();
-}
diff --git a/llvm/test/tools/llvm-objdump/Inputs/source-path.ll b/llvm/test/tools/llvm-objdump/Inputs/source-path.ll
deleted file mode 100644
index a202dda2d5f6e..0000000000000
--- a/llvm/test/tools/llvm-objdump/Inputs/source-path.ll
+++ /dev/null
@@ -1,65 +0,0 @@
-; NOTE: To use this file as input, replace SRC_COMPDIR with a directory path via sed.
-
-; ModuleID = 'source-path.bc'
-source_filename = "source-path.c"
-target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
-target triple = "x86_64-unknown-linux-gnu"
-
- at a = global i32 1, align 4, !dbg !4
-
-; Function Attrs: nounwind uwtable
-define i32 @foo() #0 !dbg !9 {
-entry:
-  %0 = load i32, ptr @a, align 4, !dbg !12
-  ret i32 %0, !dbg !13
-}
-
-; Function Attrs: nounwind uwtable
-define i32 @main() #0 !dbg !14 {
-entry:
-  %retval = alloca i32, align 4
-  %b = alloca ptr, align 8
-  store i32 0, ptr %retval, align 4
-  call void @llvm.dbg.declare(metadata ptr %b, metadata !15, metadata !17), !dbg !18
-  store ptr @a, ptr %b, align 8, !dbg !18
-  %0 = load ptr, ptr %b, align 8, !dbg !19
-  %1 = load i32, ptr %0, align 4, !dbg !20
-  %call = call i32 @foo(), !dbg !21
-  %add = add nsw i32 %1, %call, !dbg !22
-  ret i32 %add, !dbg !23
-}
-
-; Function Attrs: nounwind readnone
-declare void @llvm.dbg.declare(metadata, metadata, metadata) #1
-
-attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "frame-pointer"="all" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
-attributes #1 = { nounwind readnone }
-
-!llvm.dbg.cu = !{!0}
-!llvm.module.flags = !{!6, !7}
-!llvm.ident = !{!8}
-
-!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang version 4.0.0", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, globals: !3)
-!1 = !DIFile(filename: "source-path.c", directory: "SRC_COMPDIR")
-!2 = !{}
-!3 = !{!4}
-!4 = !DIGlobalVariableExpression(var: !DIGlobalVariable(name: "a", scope: !0, file: !1, line: 1, type: !5, isLocal: false, isDefinition: true), expr: !DIExpression())
-!5 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
-!6 = !{i32 2, !"Dwarf Version", i32 4}
-!7 = !{i32 2, !"Debug Info Version", i32 3}
-!8 = !{!"clang version 4.0.0"}
-!9 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 2, type: !10, isLocal: false, isDefinition: true, scopeLine: 2, isOptimized: false, unit: !0, retainedNodes: !2)
-!10 = !DISubroutineType(types: !11)
-!11 = !{!5}
-!12 = !DILocation(line: 3, column: 10, scope: !9)
-!13 = !DILocation(line: 3, column: 3, scope: !9)
-!14 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 6, type: !10, isLocal: false, isDefinition: true, scopeLine: 6, isOptimized: false, unit: !0, retainedNodes: !2)
-!15 = !DILocalVariable(name: "b", scope: !14, file: !1, line: 7, type: !16)
-!16 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64, align: 64)
-!17 = !DIExpression()
-!18 = !DILocation(line: 7, column: 8, scope: !14)
-!19 = !DILocation(line: 8, column: 11, scope: !14)
-!20 = !DILocation(line: 8, column: 10, scope: !14)
-!21 = !DILocation(line: 8, column: 15, scope: !14)
-!22 = !DILocation(line: 8, column: 13, scope: !14)
-!23 = !DILocation(line: 8, column: 3, scope: !14)
diff --git a/llvm/test/tools/llvm-objdump/source-path.test b/llvm/test/tools/llvm-objdump/source-path.test
deleted file mode 100644
index 676da131c69fa..0000000000000
--- a/llvm/test/tools/llvm-objdump/source-path.test
+++ /dev/null
@@ -1,73 +0,0 @@
-;; Test --substitute-path and --source-dir with --source and --line-numbers.
-;;
-;; The object records source paths under /wrong/Inputs, but the source file
-;; lives in this test's Inputs directory. Without path remapping, --source
-;; cannot load the file from disk.
-;;
-;; --source-dir: each flag is prepended to the search list, so later flags on
-;; the command line are tried first.
-;;
-;; --substitute-path: rules are tried in command-line order; the first match
-;; wins (same as GDB set substitute-path).
-
-; RUN: sed -e "s,SRC_COMPDIR,/wrong/Inputs,g" %p/Inputs/source-path.ll > %t.ll
-; RUN: llc -o %t.o -filetype=obj -mtriple=x86_64-pc-linux %t.ll
-; REQUIRES: x86-registered-target
-
-; RUN: llvm-objdump --no-print-imm-hex -d --line-numbers %t.o | FileCheck %s --check-prefix=LINE
-; RUN: llvm-objdump --no-print-imm-hex -d --source %t.o 2>&1 | \
-; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
-
-; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong %/p %t.o | \
-; RUN:   FileCheck %s --check-prefix=SOURCE
-; RUN: llvm-objdump --no-print-imm-hex -d --source --source-dir %/p/Inputs %t.o | \
-; RUN:   FileCheck %s --check-prefix=SOURCE
-
-; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wron %/p %t.o 2>&1 | \
-; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
-
-; First --source-dir misses (no source-path.c under %/p alone); second finds
-; Inputs/source-path.c via basename lookup.
-; RUN: llvm-objdump --no-print-imm-hex -d --source \
-; RUN:   --source-dir %/p/Inputs --source-dir %/p %t.o | \
-; RUN:   FileCheck %s --check-prefix=SOURCE
-
-; More specific rule listed first wins (GDB example: /wrong/Inputs before /wrong).
-; RUN: llvm-objdump --no-print-imm-hex -d --source \
-; RUN:   --substitute-path /wrong/Inputs %/p/Inputs \
-; RUN:   --substitute-path /wrong /nowhere %t.o | \
-; RUN:   FileCheck %s --check-prefix=SOURCE
-
-; Broader rule listed first matches; later rule is not applied (miss on remapped path).
-; RUN: llvm-objdump --no-print-imm-hex -d --source \
-; RUN:   --substitute-path /wrong /nowhere \
-; RUN:   --substitute-path /wrong/Inputs %/p/Inputs %t.o 2>&1 | \
-; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING-SUBST -DFILE=%t.o
-
-; First substitute rule is a no-match; second matching rule remaps the path.
-; RUN: llvm-objdump --no-print-imm-hex -d --source \
-; RUN:   --substitute-path /wron /nowhere \
-; RUN:   --substitute-path /wrong %/p %t.o | \
-; RUN:   FileCheck %s --check-prefix=SOURCE
-; LINE: <foo>:
-; LINE-NEXT: ; foo():
-; LINE-NEXT: ; {{.*}}source-path.c:2
-; LINE-NEXT: pushq
-; LINE: ; {{.*}}source-path.c:3
-; LINE-NEXT: movq
-; LINE-NEXT: movl
-; LINE-NEXT: popq
-; LINE-NEXT: retq
-
-; SOURCE-MISSING: warning: '[[FILE]]': failed to find source {{[/\\]}}wrong{{[/\\]}}Inputs{{[/\\]}}source-path.c
-
-; SOURCE-MISSING-SUBST: warning: '[[FILE]]': failed to find source {{[/\\]}}nowhere{{[/\\]}}Inputs{{[/\\]}}source-path.c
-
-; SOURCE: <foo>:
-; SOURCE-NEXT: ; int foo() {
-; SOURCE-NEXT: pushq
-; SOURCE: ; return a;
-; SOURCE-NEXT: movq
-; SOURCE-NEXT: movl
-; SOURCE-NEXT: popq
-; SOURCE-NEXT: retq
diff --git a/llvm/tools/llvm-objdump/ObjdumpOpts.td b/llvm/tools/llvm-objdump/ObjdumpOpts.td
index 3541d9a71dec5..67df89273ff7c 100644
--- a/llvm/tools/llvm-objdump/ObjdumpOpts.td
+++ b/llvm/tools/llvm-objdump/ObjdumpOpts.td
@@ -246,12 +246,10 @@ def : Flag<["-"], "w">, Alias<wide>;
 
 defm prefix : Eq<"prefix", "Add prefix to absolute paths">,
               MetaVarName<"prefix">;
-defm source_dir :
-  Eq<"source-dir",
-     "Add a directory to the front of the source search path when looking up "
-     "source files. Multiple directories may be given in one argument "
-     "separated by ':' (';' on Windows)">,
-  MetaVarName<"<dir>">;
+def source_dir : Separate<["--"], "source-dir">,
+  MetaVarName<"<dir>">,
+  HelpText<"Add a directory to the front of the source search path when "
+           "looking up source files. May be specified multiple times.">;
 def : Separate<["-"], "source-dir">, Alias<source_dir>;
 
 def substitute_path : MultiArg<["--"], "substitute-path", 2>,
@@ -259,7 +257,7 @@ def substitute_path : MultiArg<["--"], "substitute-path", 2>,
   HelpText<"Replace <from> with <to> at the start of the directory part of "
            "source file paths when looking up sources. The rule is applied "
            "only if <from> ends at a directory separator in the path. May be "
-           "specified multiple times.">;
+           "specified multiple times">;
 defm prefix_strip
     : Eq<"prefix-strip", "Strip out initial directories from absolute "
                          "paths. No effect without --prefix">,
diff --git a/llvm/tools/llvm-objdump/SourcePrinter.cpp b/llvm/tools/llvm-objdump/SourcePrinter.cpp
index a212b495162c0..0b271d72f7be7 100644
--- a/llvm/tools/llvm-objdump/SourcePrinter.cpp
+++ b/llvm/tools/llvm-objdump/SourcePrinter.cpp
@@ -14,7 +14,6 @@
 
 #include "SourcePrinter.h"
 #include "llvm-objdump.h"
-#include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/DebugInfo/DWARF/DWARFExpressionPrinter.h"
 #include "llvm/DebugInfo/DWARF/LowLevel/DWARFExpression.h"
@@ -91,9 +90,6 @@ static std::string applySubstitutePaths(StringRef FileName) {
   normalizeSourcePath(Directory);
 
   for (const auto &[From, To] : SubstitutePaths) {
-    if (From.empty())
-      continue;
-
     SmallString<256> FromPath(From);
     normalizeSourcePath(FromPath);
     StringRef Dir = Directory;
@@ -710,7 +706,8 @@ bool SourcePrinter::cacheSource(const DILineInfo &LineInfo) {
       SmallVector<StringRef, 8> SearchDirs;
       for (const std::string &Dir : SourceDirs)
         SearchDirs.push_back(Dir);
-      if (auto Resolved = findSourceFilePath(LineInfo.FileName, SearchDirs))
+      if (std::optional<std::string> Resolved =
+              findSourceFilePath(LineInfo.FileName, SearchDirs))
         PathToOpen = std::move(*Resolved);
     }
 
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.cpp b/llvm/tools/llvm-objdump/llvm-objdump.cpp
index 1311bf9625fd2..2272fe6ee2e1b 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.cpp
+++ b/llvm/tools/llvm-objdump/llvm-objdump.cpp
@@ -84,7 +84,6 @@
 #include <set>
 #include <system_error>
 #include <unordered_map>
-#include <utility>
 
 using namespace llvm;
 using namespace llvm::object;
@@ -3906,17 +3905,17 @@ static void parseObjdumpOptions(const llvm::opt::InputArgList &InputArgs) {
   UnwindShowWODPool = InputArgs.hasArg(OBJDUMP_unwind_show_wod_pool);
   Prefix = InputArgs.getLastArgValue(OBJDUMP_prefix).str();
   parseIntArg(InputArgs, OBJDUMP_prefix_strip, PrefixStrip);
-  for (const opt::Arg *A : InputArgs.filtered(OBJDUMP_substitute_path))
-    SubstitutePaths.emplace_back(A->getValue(0), A->getValue(1));
-  for (StringRef Dirs : InputArgs.getAllArgValues(OBJDUMP_source_dir)) {
-#if defined(_WIN32)
-    for (StringRef Dir : llvm::split(Dirs, ";"))
-#else
-    for (StringRef Dir : llvm::split(Dirs, ":"))
-#endif
-      if (!Dir.empty())
-        SourceDirs.insert(SourceDirs.end(), Dir.str());
+  for (const opt::Arg *A : InputArgs.filtered(OBJDUMP_substitute_path)) {
+    StringRef From = A->getValue(0);
+    StringRef To = A->getValue(1);
+    if (From.empty())
+      reportCmdLineError(A->getSpelling() +
+                         ": <from> must not be empty");
+    SubstitutePaths.emplace_back(From.str(), A->getValue(1));
   }
+  for (StringRef Dir : InputArgs.getAllArgValues(OBJDUMP_source_dir))
+    if (!Dir.empty())
+      SourceDirs.insert(SourceDirs.begin(), Dir.str());
   if (const opt::Arg *A = InputArgs.getLastArg(OBJDUMP_debug_vars_EQ)) {
     DbgVariables = StringSwitch<DebugFormat>(A->getValue())
                        .Case("ascii", DFASCII)
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.h b/llvm/tools/llvm-objdump/llvm-objdump.h
index 2241e64a6d81b..a68dc29409ac1 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.h
+++ b/llvm/tools/llvm-objdump/llvm-objdump.h
@@ -68,8 +68,8 @@ extern std::vector<std::string> MAttrs;
 extern std::string MCPU;
 extern std::string Prefix;
 extern uint32_t PrefixStrip;
-extern std::vector<std::pair<std::string, std::string>> SubstitutePaths;
 extern std::vector<std::string> SourceDirs;
+extern std::vector<std::pair<std::string, std::string>> SubstitutePaths;
 extern bool PrintImmHex;
 extern bool PrintLines;
 extern bool PrintSource;

>From 05947b1015b560f75b9304e63e55c4a423d2642f Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Fri, 5 Jun 2026 14:35:44 +0200
Subject: [PATCH 3/7] Adding missing test file, fixed dosc and opts.

---
 llvm/docs/CommandGuide/llvm-objdump.rst       |   4 +-
 .../tools/llvm-objdump/X86/source-path.test   | 100 ++++++++++++++++++
 llvm/tools/llvm-objdump/ObjdumpOpts.td        |   2 +-
 3 files changed, 103 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/tools/llvm-objdump/X86/source-path.test

diff --git a/llvm/docs/CommandGuide/llvm-objdump.rst b/llvm/docs/CommandGuide/llvm-objdump.rst
index b3069515f8b6a..e7a171210b2a3 100644
--- a/llvm/docs/CommandGuide/llvm-objdump.rst
+++ b/llvm/docs/CommandGuide/llvm-objdump.rst
@@ -39,6 +39,8 @@ combined with other commands:
 
 .. option:: --disassemble-symbols=<symbol1[,symbol2,...]>
 
+.. option:: --disassemble=symbol1 --disassemble=symbol2 ...
+
   Disassemble only the specified symbols. Takes demangled symbol names when
   :option:`--demangle` is specified, otherwise takes mangled symbol names.
   Implies :option:`--disassemble`.
@@ -245,8 +247,6 @@ OPTIONS
   the order given and the first matching rule is used, as in GDB
   ``set substitute-path``.
 
-.. option:: --source-dir <dir>
-
 .. option:: -source-dir <dir>
 
   When disassembling with the :option:`--source` option, add ``dir`` to the
diff --git a/llvm/test/tools/llvm-objdump/X86/source-path.test b/llvm/test/tools/llvm-objdump/X86/source-path.test
new file mode 100644
index 0000000000000..e407dc368d3aa
--- /dev/null
+++ b/llvm/test/tools/llvm-objdump/X86/source-path.test
@@ -0,0 +1,100 @@
+;; Test --substitute-path and --source-dir with --source and --line-numbers.
+;;
+;; The object records source paths under /wrong/Inputs, but the source file
+;; lives in this test's Inputs directory. Without path remapping, --source
+;; cannot load the file from disk.
+;;
+;; --source-dir: one directory per flag; later flags on the command line are
+;; searched before earlier ones.
+;;
+;; --substitute-path: rules are tried in command-line order; the first match
+;; wins (same as GDB set substitute-path).
+
+; RUN: sed -e "s,SRC_COMPDIR,/wrong/Inputs,g" %p/Inputs/source-interleave.ll > %t.ll
+; RUN: llc -o %t.o -filetype=obj -mtriple=x86_64-pc-linux %t.ll
+; REQUIRES: x86-registered-target
+
+; RUN: llvm-objdump --no-print-imm-hex -d --line-numbers %t.o | FileCheck %s --check-prefix=LINE
+; RUN: llvm-objdump --no-print-imm-hex -d --source %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
+
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+; RUN: llvm-objdump --no-print-imm-hex -d --source --source-dir %/p/Inputs %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wron %/p %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
+
+; First --source-dir misses (no source file under %/p alone); second finds
+; Inputs/source-interleave-x86_64.c via basename lookup.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --source-dir %/p/Inputs --source-dir %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; More specific rule listed first wins (GDB example: /wrong/Inputs before /wrong).
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong/Inputs %/p/Inputs \
+; RUN:   --substitute-path /wrong /nowhere %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; Broader rule listed first matches; later rule is not applied (miss on remapped path).
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong /nowhere \
+; RUN:   --substitute-path /wrong/Inputs %/p/Inputs %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING-SUBST -DFILE=%t.o
+
+; First substitute rule is a no-match; second matching rule remaps the path.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wron /nowhere \
+; RUN:   --substitute-path /wrong %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+; Empty <to> strips the matched prefix (GDB allows this); use --source-dir to
+; resolve the resulting relative path.
+; RUN: echo '--substitute-path' > %t-empty-to.rsp
+; RUN: echo '/wrong' >> %t-empty-to.rsp
+; RUN: echo '""' >> %t-empty-to.rsp
+; RUN: llvm-objdump --no-print-imm-hex -d --source @%t-empty-to.rsp \
+; RUN:   --source-dir %/p/Inputs %t.o | FileCheck %s --check-prefix=SOURCE
+
+; Empty <to> without --source-dir: relative path is not resolved; warn once, no crash.
+; RUN: llvm-objdump --no-print-imm-hex -d --source @%t-empty-to.rsp %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SOURCE-EMPTY-TO -DFILE=%t.o
+
+; Empty <from> is rejected (use a response file so the shell does not drop "").
+; RUN: echo '--substitute-path' > %t-empty-from.rsp
+; RUN: echo '""' >> %t-empty-from.rsp
+; RUN: echo '%/p' >> %t-empty-from.rsp
+; RUN: not llvm-objdump @%t-empty-from.rsp %t.o 2>&1 | \
+; RUN:   FileCheck %s --check-prefix=SUBST-ERR-FROM
+
+; SUBST-ERR-FROM: llvm-objdump{{.*}}: error: --substitute-path: <from> must not be empty
+
+; LINE: <foo>:
+; LINE-NEXT: ; foo():
+; LINE-NEXT: ; {{.*}}source-interleave-x86_64.c:2
+; LINE-NEXT: pushq
+; LINE: ; {{.*}}source-interleave-x86_64.c:3
+; LINE-NEXT: movq
+; LINE-NEXT: movl
+; LINE-NEXT: popq
+; LINE-NEXT: retq
+
+; SOURCE-MISSING: warning: '[[FILE]]': failed to find source {{[/\\]}}wrong{{[/\\]}}Inputs{{[/\\]}}source-interleave-x86_64.c
+
+; SOURCE-MISSING-SUBST: warning: '[[FILE]]': failed to find source {{[/\\]}}nowhere{{[/\\]}}Inputs{{[/\\]}}source-interleave-x86_64.c
+
+; SOURCE-EMPTY-TO: <foo>:
+; SOURCE-EMPTY-TO: warning: '[[FILE]]': failed to find source {{.*}}Inputs{{[/\\]}}source-interleave-x86_64.c
+; SOURCE-EMPTY-TO: pushq
+; SOURCE-EMPTY-TO-NOT: ; int foo
+
+; SOURCE: <foo>:
+; SOURCE-NEXT: ; int foo() {
+; SOURCE-NEXT: pushq
+; SOURCE: ; return a;
+; SOURCE-NEXT: movq
+; SOURCE-NEXT: movl
+; SOURCE-NEXT: popq
+; SOURCE-NEXT: retq
diff --git a/llvm/tools/llvm-objdump/ObjdumpOpts.td b/llvm/tools/llvm-objdump/ObjdumpOpts.td
index 67df89273ff7c..e22f58236ab9a 100644
--- a/llvm/tools/llvm-objdump/ObjdumpOpts.td
+++ b/llvm/tools/llvm-objdump/ObjdumpOpts.td
@@ -249,7 +249,7 @@ defm prefix : Eq<"prefix", "Add prefix to absolute paths">,
 def source_dir : Separate<["--"], "source-dir">,
   MetaVarName<"<dir>">,
   HelpText<"Add a directory to the front of the source search path when "
-           "looking up source files. May be specified multiple times.">;
+           "looking up source files. May be specified multiple times">;
 def : Separate<["-"], "source-dir">, Alias<source_dir>;
 
 def substitute_path : MultiArg<["--"], "substitute-path", 2>,

>From 779f0d4e965a46b525fff8c3b39694c1a8070fc7 Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Fri, 5 Jun 2026 14:42:11 +0200
Subject: [PATCH 4/7] Alphabetical order in options.

---
 llvm/docs/CommandGuide/llvm-objdump.rst | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/llvm/docs/CommandGuide/llvm-objdump.rst b/llvm/docs/CommandGuide/llvm-objdump.rst
index e7a171210b2a3..fa68a7718fda6 100644
--- a/llvm/docs/CommandGuide/llvm-objdump.rst
+++ b/llvm/docs/CommandGuide/llvm-objdump.rst
@@ -238,6 +238,14 @@ OPTIONS
   initial directories from absolute paths. This option has no effect without
   :option:`--prefix`.
 
+.. option:: -source-dir <dir>
+
+  When disassembling with the :option:`--source` option, add ``dir`` to the
+  front of the source search path when looking up source files. For each source
+  file, llvm-objdump tries the path from the debug info, then each search
+  directory with the full path appended, then each search directory with only
+  the file name.
+  
 .. option:: --substitute-path <from> <to>
 
   When disassembling with the :option:`--source` option, replace ``from`` with
@@ -247,14 +255,6 @@ OPTIONS
   the order given and the first matching rule is used, as in GDB
   ``set substitute-path``.
 
-.. option:: -source-dir <dir>
-
-  When disassembling with the :option:`--source` option, add ``dir`` to the
-  front of the source search path when looking up source files. For each source
-  file, llvm-objdump tries the path from the debug info, then each search
-  directory with the full path appended, then each search directory with only
-  the file name.
-
   This option may be specified multiple times; each ``--source-dir`` adds one
   directory. Later options on the command line are searched before earlier ones.
 

>From 009fed9fdea49b6fc8b8e331ecb303f3cff2dbc5 Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Fri, 5 Jun 2026 15:15:40 +0200
Subject: [PATCH 5/7] Remove warning, fix documentation.

---
 llvm/docs/CommandGuide/llvm-objdump.rst  | 7 +++----
 llvm/tools/llvm-objdump/llvm-objdump.cpp | 1 -
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/llvm/docs/CommandGuide/llvm-objdump.rst b/llvm/docs/CommandGuide/llvm-objdump.rst
index fa68a7718fda6..e385c257f0203 100644
--- a/llvm/docs/CommandGuide/llvm-objdump.rst
+++ b/llvm/docs/CommandGuide/llvm-objdump.rst
@@ -238,13 +238,15 @@ OPTIONS
   initial directories from absolute paths. This option has no effect without
   :option:`--prefix`.
 
-.. option:: -source-dir <dir>
+.. option:: --source-dir <dir>
 
   When disassembling with the :option:`--source` option, add ``dir`` to the
   front of the source search path when looking up source files. For each source
   file, llvm-objdump tries the path from the debug info, then each search
   directory with the full path appended, then each search directory with only
   the file name.
+  This option may be specified multiple times; each ``--source-dir`` adds one
+  directory. Later options on the command line are searched before earlier ones.
   
 .. option:: --substitute-path <from> <to>
 
@@ -255,9 +257,6 @@ OPTIONS
   the order given and the first matching rule is used, as in GDB
   ``set substitute-path``.
 
-  This option may be specified multiple times; each ``--source-dir`` adds one
-  directory. Later options on the command line are searched before earlier ones.
-
 .. option:: --print-imm-hex
 
   Use hex format when printing immediate values in disassembly output (default).
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.cpp b/llvm/tools/llvm-objdump/llvm-objdump.cpp
index 2272fe6ee2e1b..c04b24d6583b2 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.cpp
+++ b/llvm/tools/llvm-objdump/llvm-objdump.cpp
@@ -3907,7 +3907,6 @@ static void parseObjdumpOptions(const llvm::opt::InputArgList &InputArgs) {
   parseIntArg(InputArgs, OBJDUMP_prefix_strip, PrefixStrip);
   for (const opt::Arg *A : InputArgs.filtered(OBJDUMP_substitute_path)) {
     StringRef From = A->getValue(0);
-    StringRef To = A->getValue(1);
     if (From.empty())
       reportCmdLineError(A->getSpelling() +
                          ": <from> must not be empty");

>From 2f629c5fad54fc077e1eb52e490c5eed001d940c Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Wed, 10 Jun 2026 10:33:15 +0200
Subject: [PATCH 6/7] Remove extra includes, report error on empty dir in
 --source-path, add new test cases, fix some existing according to review.

---
 .../tools/llvm-objdump/X86/source-path.test   | 67 +++++++++++++------
 llvm/tools/llvm-objdump/llvm-objdump.cpp      |  9 ++-
 llvm/tools/llvm-objdump/llvm-objdump.h        |  2 -
 3 files changed, 54 insertions(+), 24 deletions(-)

diff --git a/llvm/test/tools/llvm-objdump/X86/source-path.test b/llvm/test/tools/llvm-objdump/X86/source-path.test
index e407dc368d3aa..bcc7ed92c37e0 100644
--- a/llvm/test/tools/llvm-objdump/X86/source-path.test
+++ b/llvm/test/tools/llvm-objdump/X86/source-path.test
@@ -26,51 +26,73 @@
 ; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wron %/p %t.o 2>&1 | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
 
-; First --source-dir misses (no source file under %/p alone); second finds
-; Inputs/source-interleave-x86_64.c via basename lookup.
+;; First --source-dir misses (no source file under %/p alone); second finds
+;; Inputs/source-interleave-x86_64.c via basename lookup.
 ; RUN: llvm-objdump --no-print-imm-hex -d --source \
 ; RUN:   --source-dir %/p/Inputs --source-dir %/p %t.o | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE
 
-; More specific rule listed first wins (GDB example: /wrong/Inputs before /wrong).
+;; More specific rule listed first wins (GDB example: /wrong/Inputs before /wrong).
 ; RUN: llvm-objdump --no-print-imm-hex -d --source \
 ; RUN:   --substitute-path /wrong/Inputs %/p/Inputs \
 ; RUN:   --substitute-path /wrong /nowhere %t.o | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE
 
-; Broader rule listed first matches; later rule is not applied (miss on remapped path).
+;; Broader rule listed first matches; later rule is not applied (miss on remapped path).
 ; RUN: llvm-objdump --no-print-imm-hex -d --source \
 ; RUN:   --substitute-path /wrong /nowhere \
 ; RUN:   --substitute-path /wrong/Inputs %/p/Inputs %t.o 2>&1 | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING-SUBST -DFILE=%t.o
 
-; First substitute rule is a no-match; second matching rule remaps the path.
+;; First substitute rule is a no-match; second matching rule remaps the path.
 ; RUN: llvm-objdump --no-print-imm-hex -d --source \
 ; RUN:   --substitute-path /wron /nowhere \
 ; RUN:   --substitute-path /wrong %/p %t.o | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE
 
-; Empty <to> strips the matched prefix (GDB allows this); use --source-dir to
-; resolve the resulting relative path.
-; RUN: echo '--substitute-path' > %t-empty-to.rsp
-; RUN: echo '/wrong' >> %t-empty-to.rsp
-; RUN: echo '""' >> %t-empty-to.rsp
-; RUN: llvm-objdump --no-print-imm-hex -d --source @%t-empty-to.rsp \
+;; Empty <to> strips the matched prefix; use --source-dir to
+;; resolve the resulting relative path.
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong ""  \
 ; RUN:   --source-dir %/p/Inputs %t.o | FileCheck %s --check-prefix=SOURCE
 
-; Empty <to> without --source-dir: relative path is not resolved; warn once, no crash.
-; RUN: llvm-objdump --no-print-imm-hex -d --source @%t-empty-to.rsp %t.o 2>&1 | \
+;; Empty <to> without --source-dir: relative path is not resolved; warn once, no crash.
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong "" %t.o 2>&1 | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE-EMPTY-TO -DFILE=%t.o
 
-; Empty <from> is rejected (use a response file so the shell does not drop "").
-; RUN: echo '--substitute-path' > %t-empty-from.rsp
-; RUN: echo '""' >> %t-empty-from.rsp
-; RUN: echo '%/p' >> %t-empty-from.rsp
-; RUN: not llvm-objdump @%t-empty-from.rsp %t.o 2>&1 | \
+;; Empty <from> is rejected .
+; RUN: not llvm-objdump --substitute-path "" %/p  %t.o 2>&1 | \
 ; RUN:   FileCheck %s --check-prefix=SUBST-ERR-FROM
 
 ; SUBST-ERR-FROM: llvm-objdump{{.*}}: error: --substitute-path: <from> must not be empty
 
+;; --substitute-path runs before --prefix: remap /wrong/Inputs to the
+;; root-relative /Inputs, then prepend the test directory.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong/Inputs /Inputs --prefix %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+; RUN: llvm-objdump --no-print-imm-hex -d --line-numbers \
+; RUN:   --substitute-path /wrong/Inputs /Inputs --prefix %/p %t.o | \
+; RUN:   FileCheck %s --check-prefix=LINE-SUBST-PREFIX -DPREFIX=%/p
+
+;; --prefix alone cannot fix /wrong/Inputs, but --source-dir finds the file
+;; via basename lookup under the search directory.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --prefix %/p --source-dir %/p/Inputs %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+;; All three: substitute and prefix produce a missing path; --source-dir
+;; resolves the file from the search directory.
+; RUN: llvm-objdump --no-print-imm-hex -d --source \
+; RUN:   --substitute-path /wrong/Inputs /missing \
+; RUN:   --prefix %/p --source-dir %/p/Inputs %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
+;; When substitute-path yields a relative path, --prefix is ignored (as in
+;; source-interleave-prefix.test) but --source-dir still resolves the file.
+; RUN: llvm-objdump --no-print-imm-hex -d --source --substitute-path /wrong "" %t.o \
+; RUN:   --prefix myprefix --source-dir %/p/Inputs %t.o | \
+; RUN:   FileCheck %s --check-prefix=SOURCE
+
 ; LINE: <foo>:
 ; LINE-NEXT: ; foo():
 ; LINE-NEXT: ; {{.*}}source-interleave-x86_64.c:2
@@ -86,7 +108,7 @@
 ; SOURCE-MISSING-SUBST: warning: '[[FILE]]': failed to find source {{[/\\]}}nowhere{{[/\\]}}Inputs{{[/\\]}}source-interleave-x86_64.c
 
 ; SOURCE-EMPTY-TO: <foo>:
-; SOURCE-EMPTY-TO: warning: '[[FILE]]': failed to find source {{.*}}Inputs{{[/\\]}}source-interleave-x86_64.c
+; SOURCE-EMPTY-TO: warning: '[[FILE]]': failed to find source Inputs{{[/\\]}}source-interleave-x86_64.c
 ; SOURCE-EMPTY-TO: pushq
 ; SOURCE-EMPTY-TO-NOT: ; int foo
 
@@ -98,3 +120,10 @@
 ; SOURCE-NEXT: movl
 ; SOURCE-NEXT: popq
 ; SOURCE-NEXT: retq
+
+; LINE-SUBST-PREFIX: <foo>:
+; LINE-SUBST-PREFIX-NEXT: ; foo():
+; LINE-SUBST-PREFIX-NEXT: ; [[PREFIX]]{{[/\\]}}Inputs{{[/\\]}}source-interleave-x86_64.c:2
+; LINE-SUBST-PREFIX-NEXT: pushq
+; LINE-SUBST-PREFIX: ; [[PREFIX]]{{[/\\]}}Inputs{{[/\\]}}source-interleave-x86_64.c:3
+; LINE-SUBST-PREFIX-NEXT: movq
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.cpp b/llvm/tools/llvm-objdump/llvm-objdump.cpp
index c04b24d6583b2..6da338edf44e4 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.cpp
+++ b/llvm/tools/llvm-objdump/llvm-objdump.cpp
@@ -3912,9 +3912,12 @@ static void parseObjdumpOptions(const llvm::opt::InputArgList &InputArgs) {
                          ": <from> must not be empty");
     SubstitutePaths.emplace_back(From.str(), A->getValue(1));
   }
-  for (StringRef Dir : InputArgs.getAllArgValues(OBJDUMP_source_dir))
-    if (!Dir.empty())
-      SourceDirs.insert(SourceDirs.begin(), Dir.str());
+  for (StringRef Dir : InputArgs.getAllArgValues(OBJDUMP_source_dir)) {
+    if (Dir.empty())
+      reportCmdLineError("--source-dir argument must not be empty");
+    SourceDirs.insert(SourceDirs.begin(), Dir.str());
+  }
+
   if (const opt::Arg *A = InputArgs.getLastArg(OBJDUMP_debug_vars_EQ)) {
     DbgVariables = StringSwitch<DebugFormat>(A->getValue())
                        .Case("ascii", DFASCII)
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.h b/llvm/tools/llvm-objdump/llvm-objdump.h
index a68dc29409ac1..b52f92721386f 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.h
+++ b/llvm/tools/llvm-objdump/llvm-objdump.h
@@ -18,8 +18,6 @@
 #include "llvm/Support/FormattedStream.h"
 #include <functional>
 #include <memory>
-#include <utility>
-#include <vector>
 
 namespace llvm {
 class StringRef;

>From b36b95d0ed5c308c63fa270a77dae83d80e71ca2 Mon Sep 17 00:00:00 2001
From: VladimirMedic <vladimir.medic at gmail.com>
Date: Fri, 12 Jun 2026 11:42:51 +0200
Subject: [PATCH 7/7] Change search order for --source-dir, remove extra
 comamnds from test.

---
 llvm/test/tools/llvm-objdump/X86/source-path.test | 8 ++------
 llvm/tools/llvm-objdump/llvm-objdump.cpp          | 2 +-
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/llvm/test/tools/llvm-objdump/X86/source-path.test b/llvm/test/tools/llvm-objdump/X86/source-path.test
index bcc7ed92c37e0..48f188be04f05 100644
--- a/llvm/test/tools/llvm-objdump/X86/source-path.test
+++ b/llvm/test/tools/llvm-objdump/X86/source-path.test
@@ -4,16 +4,12 @@
 ;; lives in this test's Inputs directory. Without path remapping, --source
 ;; cannot load the file from disk.
 ;;
-;; --source-dir: one directory per flag; later flags on the command line are
-;; searched before earlier ones.
+;; --source-dir: one directory per flag; flags on the command line are
+;; searched in order fitst-to-last.
 ;;
 ;; --substitute-path: rules are tried in command-line order; the first match
 ;; wins (same as GDB set substitute-path).
 
-; RUN: sed -e "s,SRC_COMPDIR,/wrong/Inputs,g" %p/Inputs/source-interleave.ll > %t.ll
-; RUN: llc -o %t.o -filetype=obj -mtriple=x86_64-pc-linux %t.ll
-; REQUIRES: x86-registered-target
-
 ; RUN: llvm-objdump --no-print-imm-hex -d --line-numbers %t.o | FileCheck %s --check-prefix=LINE
 ; RUN: llvm-objdump --no-print-imm-hex -d --source %t.o 2>&1 | \
 ; RUN:   FileCheck %s --check-prefix=SOURCE-MISSING -DFILE=%t.o
diff --git a/llvm/tools/llvm-objdump/llvm-objdump.cpp b/llvm/tools/llvm-objdump/llvm-objdump.cpp
index 6da338edf44e4..d1c41d4302b9c 100644
--- a/llvm/tools/llvm-objdump/llvm-objdump.cpp
+++ b/llvm/tools/llvm-objdump/llvm-objdump.cpp
@@ -3915,7 +3915,7 @@ static void parseObjdumpOptions(const llvm::opt::InputArgList &InputArgs) {
   for (StringRef Dir : InputArgs.getAllArgValues(OBJDUMP_source_dir)) {
     if (Dir.empty())
       reportCmdLineError("--source-dir argument must not be empty");
-    SourceDirs.insert(SourceDirs.begin(), Dir.str());
+    SourceDirs.insert(SourceDirs.end(), Dir.str());
   }
 
   if (const opt::Arg *A = InputArgs.getLastArg(OBJDUMP_debug_vars_EQ)) {



More information about the llvm-commits mailing list