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

via llvm-commits llvm-commits at lists.llvm.org
Tue Jun 2 04:21:39 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-binary-utilities

Author: VladimirMedic

<details>
<summary>Changes</summary>

Add GDB-style options so llvm-objdump --source can find source files when debug-info paths do not match the local filesystem:

--substitute-path <from> <to> — Replace from with to at the start of the directory part of a source path. Rules are tried in order; the first match wins (same as GDB set substitute-path). A rule applies only if from ends at a directory separator.
--source-dir <dir> — Add directories to the source search path. For each file, try the debug-info path, then each search dir + relative path, then each search dir + basename. Repeatable; later flags are searched first.
Path lookup normalizes separators and ./.. components for matching and fs::exists, but leaves paths unchanged in warnings when no rule matches. Uses sys::path::relative_path when combining search dirs with absolute debug paths so a drive/root prefix does not discard the search directory.

Documented in llvm-objdump.rst. Tests in llvm/test/tools/llvm-objdump/source-path.test.

Test plan

llvm-lit llvm/test/tools/llvm-objdump/source-path.test
Manual: llvm-objdump --source --substitute-path /build/tree /src/tree obj
Manual: llvm-objdump --source --source-dir /src/tree obj

---
Full diff: https://github.com/llvm/llvm-project/pull/201096.diff


8 Files Affected:

- (modified) llvm/docs/CommandGuide/llvm-objdump.rst (+23) 
- (added) llvm/test/tools/llvm-objdump/Inputs/source-path.c (+9) 
- (added) llvm/test/tools/llvm-objdump/Inputs/source-path.ll (+65) 
- (added) llvm/test/tools/llvm-objdump/source-path.test (+73) 
- (modified) llvm/tools/llvm-objdump/ObjdumpOpts.td (+14) 
- (modified) llvm/tools/llvm-objdump/SourcePrinter.cpp (+109-2) 
- (modified) llvm/tools/llvm-objdump/llvm-objdump.cpp (+13) 
- (modified) llvm/tools/llvm-objdump/llvm-objdump.h (+4) 


``````````diff
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;

``````````

</details>


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


More information about the llvm-commits mailing list