[llvm] [ORC] Add utilities for limited symbolication of JIT backtraces (PR #175099)
Lang Hames via llvm-commits
llvm-commits at lists.llvm.org
Thu Jan 8 19:27:59 PST 2026
https://github.com/lhames updated https://github.com/llvm/llvm-project/pull/175099
>From 9ca31ce1ea15097f816ad3707cb1be218ea47c09 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Thu, 8 Jan 2026 17:46:29 +1100
Subject: [PATCH 1/3] [ORC][llvm-jitlink] Add utilities for limited
symbolication of JIT backtraces.
This patch adds tools for capturing symbol information from JIT'd code and
using it to symbolicate backtraces. This is useful for debugging crashes in
JIT-compiled code where traditional symbolication tools may not have access
to the JIT symbol table. These tools are not a general solution to the JIT
symbolication problem (that will require further integration with system
components like libunwind, the dynamic linker, and/or crash tracing tools),
but will aid in JIT debugging and development until a general solution is
available.
APIs Added:
1. SymbolTableDumpPlugin - A LinkGraphLinkingLayer::Plugin that captures
symbol information as code is JIT'd and writes it to a file.
- Create(StringRef Path) -> Expected<std::shared_ptr<SymbolTableDumpPlugin>>
Creates a plugin that appends symbol information to the specified file.
- Symbol table format:
"<link graph name>"
<address> <symbol name>
<address> <symbol name>
...
The plugin uses a PostAllocationPass to write symbols after addresses have
been assigned but before the code is finalized.
2. DumpedSymbolTable - A class for symbolicating backtraces using a
previously dumped symbol table.
- Create(StringRef Path) -> Expected<DumpedSymbolTable>
Loads and parses a symbol table from a file.
- symbolicate(StringRef Backtrace) -> std::string
Given text of a backtrace, for rows ending with a hex address, adds the
symbol name, offset, and defining graph name.
llvm-jitlink Command Line Options:
1. -write-symtab=<path>
Enables the SymbolTableDumpPlugin to write symbol information to the
specified file as objects are JIT'd. The symbol table can then be used
to symbolicate backtraces from crashes or signal handlers.
2. -symbolicate-with=<path>
Runs llvm-jitlink in symbolication mode. Reads the symbol table from
<path> and symbolicates backtraces read from stdin or input files.
Usage Examples:
$ llvm-jitlink -write-symtab=symbols.txt mycode.o
$ llvm-jitlink -symbolicate-with=symbols.txt - < backtrace.txt
---
.../llvm/ExecutionEngine/Orc/BacktraceTools.h | 99 ++++++++++++
.../ExecutionEngine/Orc/BacktraceTools.cpp | 147 ++++++++++++++++++
llvm/lib/ExecutionEngine/Orc/CMakeLists.txt | 1 +
.../Generic/backtrace-symbolication.ll | 27 ++++
llvm/tools/llvm-jitlink/llvm-jitlink.cpp | 57 +++++++
5 files changed, 331 insertions(+)
create mode 100644 llvm/include/llvm/ExecutionEngine/Orc/BacktraceTools.h
create mode 100644 llvm/lib/ExecutionEngine/Orc/BacktraceTools.cpp
create mode 100644 llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/BacktraceTools.h b/llvm/include/llvm/ExecutionEngine/Orc/BacktraceTools.h
new file mode 100644
index 0000000000000..454d7654b66bf
--- /dev/null
+++ b/llvm/include/llvm/ExecutionEngine/Orc/BacktraceTools.h
@@ -0,0 +1,99 @@
+//===-- BacktraceTools.h - Backtrace symbolication tools -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Tools for dumping symbol tables and symbolicating backtraces.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_EXECUTIONENGINE_ORC_BACKTRACETOOLS_H
+#define LLVM_EXECUTIONENGINE_ORC_BACKTRACETOOLS_H
+
+#include "llvm/ExecutionEngine/Orc/LinkGraphLinkingLayer.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+namespace llvm::orc {
+
+/// Dumps symbol tables from LinkGraphs to enable backtrace symbolication.
+///
+/// This plugin appends symbol information to a file in the following format:
+/// "<link graph name>"
+/// <address> <symbol name>
+/// <address> <symbol name>
+/// ...
+///
+/// Where addresses are in hexadecimal and symbol names are for defined symbols.
+class LLVM_ABI SymbolTableDumpPlugin : public LinkGraphLinkingLayer::Plugin {
+public:
+ /// Create a SymbolTableDumpPlugin that will append symbol information
+ /// to the file at the given path.
+ static Expected<std::shared_ptr<SymbolTableDumpPlugin>>
+ Create(StringRef Path);
+
+ /// Create a SymbolTableDumpPlugin. The resulting object is in an invalid
+ /// state if, upon return, EC != std::error_code().
+ /// Prefer SymbolTableDumpPlugin::Create.
+ SymbolTableDumpPlugin(StringRef Path, std::error_code &EC);
+
+ SymbolTableDumpPlugin(const SymbolTableDumpPlugin &) = delete;
+ SymbolTableDumpPlugin &operator=(const SymbolTableDumpPlugin &) = delete;
+ SymbolTableDumpPlugin(SymbolTableDumpPlugin &&) = delete;
+ SymbolTableDumpPlugin &operator=(SymbolTableDumpPlugin &&) = delete;
+
+ void modifyPassConfig(MaterializationResponsibility &MR,
+ jitlink::LinkGraph &G,
+ jitlink::PassConfiguration &Config) override;
+
+ Error notifyFailed(MaterializationResponsibility &MR) override {
+ return Error::success();
+ }
+
+ Error notifyRemovingResources(JITDylib &JD, ResourceKey K) override {
+ return Error::success();
+ }
+
+ void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey,
+ ResourceKey SrcKey) override {}
+
+private:
+ raw_fd_ostream OutputStream;
+ std::mutex DumpMutex;
+};
+
+/// A class for symbolicating backtraces using a previously dumped symbol table.
+class LLVM_ABI DumpedSymbolTable {
+public:
+ /// Create a DumpedSymbolTable from the given path.
+ static Expected<DumpedSymbolTable> Create(StringRef Path);
+
+ /// Given a backtrace, try to symbolicate any unsymbolicated lines using the
+ /// symbol addresses in the dumped symbol table.
+ LLVM_ABI std::string symbolicate(StringRef Backtrace);
+
+private:
+ DumpedSymbolTable(std::unique_ptr<MemoryBuffer> SymtabBuffer);
+
+ void parseBuffer();
+
+ struct SymbolInfo {
+ StringRef SymName;
+ StringRef GraphName;
+ };
+
+ std::map<uint64_t, SymbolInfo> SymbolInfos;
+ std::unique_ptr<MemoryBuffer> SymtabBuffer;
+};
+
+} // namespace llvm::orc
+
+#endif // LLVM_EXECUTIONENGINE_ORC_BACKTRACETOOLS_H
diff --git a/llvm/lib/ExecutionEngine/Orc/BacktraceTools.cpp b/llvm/lib/ExecutionEngine/Orc/BacktraceTools.cpp
new file mode 100644
index 0000000000000..fabc950451a59
--- /dev/null
+++ b/llvm/lib/ExecutionEngine/Orc/BacktraceTools.cpp
@@ -0,0 +1,147 @@
+//===------- BacktraceTools.cpp - Backtrace symbolication tools ----------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/ExecutionEngine/Orc/BacktraceTools.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/ExecutionEngine/JITLink/JITLink.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/MemoryBuffer.h"
+
+namespace llvm::orc {
+
+Expected<std::shared_ptr<SymbolTableDumpPlugin>>
+SymbolTableDumpPlugin::Create(StringRef Path) {
+ std::error_code EC;
+ auto P = std::make_shared<SymbolTableDumpPlugin>(Path, EC);
+ if (EC)
+ return createFileError(Path, EC);
+ return P;
+}
+
+SymbolTableDumpPlugin::SymbolTableDumpPlugin(StringRef Path,
+ std::error_code &EC)
+ : OutputStream(Path, EC) {}
+
+void SymbolTableDumpPlugin::modifyPassConfig(
+ MaterializationResponsibility &MR, jitlink::LinkGraph &G,
+ jitlink::PassConfiguration &Config) {
+
+ Config.PostAllocationPasses.push_back([this](jitlink::LinkGraph &G) -> Error {
+ std::scoped_lock<std::mutex> Lock(DumpMutex);
+
+ OutputStream << "\"" << G.getName() << "\"\n";
+ for (auto &Sec : G.sections()) {
+ // NoAlloc symbols don't exist in the executing process, so can't
+ // contribute to symbolication. (Note: We leave Finalize-liftime symbols
+ // in for now in case of crashes during finalization, but we should
+ // probably make this optional).
+ if (Sec.getMemLifetime() == MemLifetime::NoAlloc)
+ continue;
+
+ // Write out named symbols. Anonymous symbols are skipped, since they
+ // don't add any information for symbolication purposes.
+ for (auto *Sym : Sec.symbols()) {
+ if (Sym->hasName())
+ OutputStream << formatv("{0:x}", Sym->getAddress().getValue()) << " "
+ << Sym->getName() << "\n";
+ }
+ }
+
+ OutputStream.flush();
+ return Error::success();
+ });
+}
+
+Expected<DumpedSymbolTable> DumpedSymbolTable::Create(StringRef Path) {
+ auto MB = MemoryBuffer::getFile(Path);
+ if (!MB)
+ return createFileError(Path, MB.getError());
+
+ return DumpedSymbolTable(std::move(*MB));
+}
+
+DumpedSymbolTable::DumpedSymbolTable(std::unique_ptr<MemoryBuffer> SymtabBuffer)
+ : SymtabBuffer(std::move(SymtabBuffer)) {
+ parseBuffer();
+}
+
+void DumpedSymbolTable::parseBuffer() {
+ // Read the symbol table file
+ SmallVector<StringRef, 0> Rows;
+ SymtabBuffer->getBuffer().split(Rows, '\n');
+
+ StringRef CurGraph = "<unidentified>";
+ for (auto Row : Rows) {
+ Row = Row.trim();
+ if (Row.empty())
+ continue;
+
+ // Check for graph name line (enclosed in quotes)
+ if (Row.starts_with("\"") && Row.ends_with("\"")) {
+ CurGraph = Row.trim('"');
+ continue;
+ }
+
+ // Parse "address symbol_name" lines, ignoring malformed lines.
+ size_t SpacePos = Row.find(' ');
+ if (SpacePos == StringRef::npos)
+ continue;
+
+ StringRef AddrStr = Row.substr(0, SpacePos);
+ StringRef SymName = Row.substr(SpacePos + 1);
+
+ uint64_t Addr;
+ if (AddrStr.starts_with("0x"))
+ AddrStr = AddrStr.drop_front(2);
+ if (AddrStr.getAsInteger(16, Addr))
+ continue; // Skip malformed lines
+
+ SymbolInfos[Addr] = {SymName, CurGraph};
+ }
+}
+
+std::string DumpedSymbolTable::symbolicate(StringRef Backtrace) {
+ // Symbolicate the backtrace by replacing rows with empty symbol names
+ SmallVector<StringRef, 0> BacktraceRows;
+ Backtrace.split(BacktraceRows, '\n');
+
+ std::string Result;
+ raw_string_ostream Out(Result);
+ for (auto Row : BacktraceRows) {
+ // Look for a row ending with a hex number. If there's only one column, or
+ // if the last column is not a hex number, then just reproduce the input
+ // row.
+ auto [RowStart, AddrCol] = Row.rtrim().rsplit(' ');
+ if (AddrCol.starts_with("0x"))
+ AddrCol = AddrCol.drop_front(2);
+
+ uint64_t Addr;
+ if (AddrCol.empty() || AddrCol.getAsInteger(16, Addr)) {
+ Out << Row << "\n";
+ continue;
+ }
+
+ // Search for the address in all graphs
+ auto I = SymbolInfos.upper_bound(Addr);
+ if (I != SymbolInfos.begin()) {
+ // Found a symbol. Output modified line.
+ auto &[SymAddr, SymInfo] = *(--I);
+ Out << RowStart << " " << AddrCol << " " << SymInfo.SymName;
+ if (auto Delta = Addr - SymAddr)
+ Out << " + " << formatv("{0}", Delta);
+ Out << " (" << SymInfo.GraphName << ")\n";
+ } else
+ Out << Row << "\n";
+ }
+
+ return Result;
+}
+
+} // namespace llvm::orc
diff --git a/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt b/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
index 422b20c649c70..8c78c0250cfb8 100644
--- a/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
+++ b/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
@@ -8,6 +8,7 @@ endif()
add_llvm_component_library(LLVMOrcJIT
AbsoluteSymbols.cpp
+ BacktraceTools.cpp
COFF.cpp
COFFVCRuntimeSupport.cpp
COFFPlatform.cpp
diff --git a/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
new file mode 100644
index 0000000000000..e857f5d3d09ed
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
@@ -0,0 +1,27 @@
+; RUN: rm -rf %t && mkdir -p %t
+; RUN: llc -filetype=obj -o %t/crash.o %s
+; RUN: not --crash llvm-jitlink -write-symtab %t/crash.symtab.txt %t/crash.o \
+; RUN: > %t/backtrace.txt 2>&1
+; RUN: llvm-jitlink -symbolicate-with %t/crash.symtab.txt %t/backtrace.txt \
+; RUN: | FileCheck %s
+
+; Deliberately crash by dereferencing an environment variable that should never
+; be defined, then symbolicate the backtrace using the dumped symbol table.
+
+; CHECK: this_should_crash {{.*}} ({{.*}}crash.o)
+
+ at .str = private constant [52 x i8] c"a thousand curses upon anyone who dares define this\00", align 1
+
+define i32 @this_should_crash() {
+ %1 = call ptr @getenv(ptr noundef @.str)
+ %2 = load i8, ptr %1, align 1
+ %3 = sext i8 %2 to i32
+ ret i32 %3
+}
+
+declare ptr @getenv(ptr)
+
+define i32 @main(i32 %argc, ptr %argv) {
+ %r = call i32 @this_should_crash()
+ ret i32 %r
+}
diff --git a/llvm/tools/llvm-jitlink/llvm-jitlink.cpp b/llvm/tools/llvm-jitlink/llvm-jitlink.cpp
index b8de817ec16c8..0cf4a175622f8 100644
--- a/llvm/tools/llvm-jitlink/llvm-jitlink.cpp
+++ b/llvm/tools/llvm-jitlink/llvm-jitlink.cpp
@@ -16,6 +16,7 @@
#include "llvm/BinaryFormat/Magic.h"
#include "llvm/Config/llvm-config.h" // for LLVM_ON_UNIX, LLVM_ENABLE_THREADS
#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h"
+#include "llvm/ExecutionEngine/Orc/BacktraceTools.h"
#include "llvm/ExecutionEngine/Orc/COFFPlatform.h"
#include "llvm/ExecutionEngine/Orc/Debugging/DebugInfoSupport.h"
#include "llvm/ExecutionEngine/Orc/Debugging/DebuggerSupportPlugin.h"
@@ -140,6 +141,18 @@ static cl::list<std::string>
cl::desc("Link against library X with hidden visibility"),
cl::cat(JITLinkCategory));
+static cl::opt<std::string>
+ WriteSymbolTableTo("write-symtab",
+ cl::desc("Write the symbol table for the JIT'd program "
+ "to the specified file"),
+ cl::cat(JITLinkCategory));
+
+static cl::opt<std::string> SymbolicateWith(
+ "symbolicate-with",
+ cl::desc("Given a path to a symbol table file, symbolicate the given "
+ "backtrace(s)"),
+ cl::cat(JITLinkCategory));
+
static cl::list<std::string>
LibrariesWeak("weak-l",
cl::desc("Emulate weak link against library X. Must resolve "
@@ -1203,6 +1216,15 @@ Session::Session(std::unique_ptr<ExecutorProcessControl> EPC, Error &Err)
auto &TT = ES.getTargetTriple();
+ if (!WriteSymbolTableTo.empty()) {
+ if (auto STDump = SymbolTableDumpPlugin::Create(WriteSymbolTableTo))
+ ObjLayer.addPlugin(std::move(*STDump));
+ else {
+ Err = STDump.takeError();
+ return;
+ }
+ }
+
if (DebuggerSupport && TT.isOSBinFormatMachO()) {
if (!ProcessSymsJD) {
Err = make_error<StringError>("MachO debugging requires process symbols",
@@ -1666,6 +1688,12 @@ Session::findSymbolInfo(const orc::SymbolStringPtr &SymbolName,
} // end namespace llvm
static std::pair<Triple, SubtargetFeatures> getFirstFileTripleAndFeatures() {
+
+ // If we're running in symbolicate mode then just use the process triple.
+ if (!SymbolicateWith.empty())
+ return std::make_pair(Triple(sys::getProcessTriple()), SubtargetFeatures());
+
+ // Otherwise we need to inspect the input files.
static std::pair<Triple, SubtargetFeatures> FirstTTAndFeatures = []() {
assert(!InputFiles.empty() && "InputFiles can not be empty");
@@ -1878,6 +1906,14 @@ static Error sanitizeArguments(const Triple &TT, const char *ArgV0) {
}
}
+ if (!SymbolicateWith.empty()) {
+ if (!WriteSymbolTableTo.empty())
+ errs() << WriteSymbolTableTo.ArgStr << " specified with "
+ << SymbolicateWith.ArgStr << ", ignoring.";
+ if (InputFiles.empty())
+ InputFiles.push_back("-");
+ }
+
return Error::success();
}
@@ -2817,6 +2853,22 @@ static Expected<int> runWithoutRuntime(Session &S,
return S.ES.getExecutorProcessControl().runAsMain(EntryPointAddr, InputArgv);
}
+static Error symbolicateBacktraces() {
+ auto Symtab = DumpedSymbolTable::Create(SymbolicateWith);
+ if (!Symtab)
+ return Symtab.takeError();
+
+ for (auto InputFile : InputFiles) {
+ auto BacktraceBuffer = MemoryBuffer::getFileOrSTDIN(InputFile);
+ if (!BacktraceBuffer)
+ return createFileError(InputFile, BacktraceBuffer.getError());
+
+ outs() << Symtab->symbolicate((*BacktraceBuffer)->getBuffer());
+ }
+
+ return Error::success();
+}
+
namespace {
struct JITLinkTimers {
TimerGroup JITLinkTG{"llvm-jitlink timers", "timers for llvm-jitlink phases"};
@@ -2844,6 +2896,11 @@ int main(int argc, char *argv[]) {
auto [TT, Features] = getFirstFileTripleAndFeatures();
ExitOnErr(sanitizeArguments(TT, argv[0]));
+ if (!SymbolicateWith.empty()) {
+ ExitOnErr(symbolicateBacktraces());
+ return 0;
+ }
+
auto S = ExitOnErr(Session::Create(TT, Features));
enableStatistics(*S, !OrcRuntime.empty());
>From f39a2b0bba7f25db4c4bd3408c107dcb3acf17c1 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Fri, 9 Jan 2026 12:06:31 +1100
Subject: [PATCH 2/3] Use PIC codegen for testcase, disable on Windows.
We should probably add equivalent backtrace symbolication options to lli and
move the generic testcase there, but that can be done in a follow-up patch.
---
.../JITLink/Generic/backtrace-symbolication.ll | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
index e857f5d3d09ed..c6dc085e53fa0 100644
--- a/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
+++ b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
@@ -1,5 +1,5 @@
; RUN: rm -rf %t && mkdir -p %t
-; RUN: llc -filetype=obj -o %t/crash.o %s
+; RUN: llc -relocation-model=pic -filetype=obj -o %t/crash.o %s
; RUN: not --crash llvm-jitlink -write-symtab %t/crash.symtab.txt %t/crash.o \
; RUN: > %t/backtrace.txt 2>&1
; RUN: llvm-jitlink -symbolicate-with %t/crash.symtab.txt %t/backtrace.txt \
@@ -8,6 +8,8 @@
; Deliberately crash by dereferencing an environment variable that should never
; be defined, then symbolicate the backtrace using the dumped symbol table.
+; UNSUPPORTED: system-windows
+
; CHECK: this_should_crash {{.*}} ({{.*}}crash.o)
@.str = private constant [52 x i8] c"a thousand curses upon anyone who dares define this\00", align 1
>From dbd8a28fcb931bd0de1032aad373b916e68b928a Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Fri, 9 Jan 2026 03:26:05 +0000
Subject: [PATCH 3/3] Disable debugger support in crashing testcase: Crash in
JIT'd code can cause an unrelated exception to be raised by std::future in
ELFDebugObjectPlugin.
---
.../ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
index c6dc085e53fa0..d5604581f0a2d 100644
--- a/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
+++ b/llvm/test/ExecutionEngine/JITLink/Generic/backtrace-symbolication.ll
@@ -1,6 +1,7 @@
; RUN: rm -rf %t && mkdir -p %t
; RUN: llc -relocation-model=pic -filetype=obj -o %t/crash.o %s
-; RUN: not --crash llvm-jitlink -write-symtab %t/crash.symtab.txt %t/crash.o \
+; RUN: not --crash llvm-jitlink -debugger-support=false \
+; RUN: -write-symtab %t/crash.symtab.txt %t/crash.o \
; RUN: > %t/backtrace.txt 2>&1
; RUN: llvm-jitlink -symbolicate-with %t/crash.symtab.txt %t/backtrace.txt \
; RUN: | FileCheck %s
More information about the llvm-commits
mailing list