[compiler-rt] [llvm] [ORC][MachO] Use a per-graph compact-unwind dso_base (PR #208931)
Yaxing Cai via llvm-commits
llvm-commits at lists.llvm.org
Wed Jul 15 22:53:36 PDT 2026
https://github.com/cyx-6 updated https://github.com/llvm/llvm-project/pull/208931
>From a223b1e88241d70825c537a83afa0c35fcd401a8 Mon Sep 17 00:00:00 2001
From: Yaxing Cai <caiyaxing666 at gmail.com>
Date: Sun, 12 Jul 2026 06:43:40 +0000
Subject: [PATCH] [ORC][MachO] Use a per-graph compact-unwind dso_base
MachOPlatformPlugin::modifyPassConfig injected
"__jitlink$libunwind_dso_base" as an absolute symbol pointing at the
per-JITDylib MachO header, so all objects in a JITDylib shared one
compact-unwind base. CompactUnwindManager encodes each function / LSDA /
personality entry in __TEXT,__unwind_info as an *unsigned* 32-bit delta
from that base (CompactUnwindSupport.h).
The header and each user graph are independent JITLinkMemoryManager
allocations, mmap'd with no guaranteed relative order. When a user graph
lands below the header -- reliably reproducible on Darwin arm64 under the
mmap pressure of a process that has already dlopen'd several libraries --
the uint64_t delta underflows, isUInt<32> rejects it, and linking aborts
with e.g. "... exceeds 32 bits" / "out of 32-bit delta range of
compact-unwind base". |P - B| is often only a few KB; this is unrelated
to 4GB PC-relative limits.
The on-disk format is genuinely unsigned u32 offsets that libunwind adds
to the registered dso_base at runtime, so widening the writer to a signed
delta is not an option: the base itself must be at or below every covered
address.
Fix (LLVM + orc-rt, coordinated):
* Stop injecting the absolute symbol.
CompactUnwindManager::getOrCreateCompactUnwindBase then falls through
to getOrCreateLocalMachOHeader, anchoring a per-graph
__TEXT,__lcl_macho_hdr inside the user graph's own slab. Within one
graph BasicLayout::segments() orders R -> RW -> RX in ascending
addresses, so this base is always the lowest covered address and the
u32 deltas stay non-negative at all four writer sites.
* Because libunwind decodes addresses as offset + dso_base, the base
used to encode the deltas must equal the dso_base used to decode them.
The runtime previously hard-coded dso_base to the JITDylib header.
Capture the per-graph base in findUnwindSectionInfo and thread it
through the register/deregister_object_platform_sections wire (a new
DSOBase field on UnwindSectionInfo), falling back to the JITDylib
header when no base was recorded (e.g. DWARF-only registration).
JD.Header keeps its role as the JITDylib identity for bookkeeping but
stops doubling as the unwind dso_base.
The newer UnwindInfoRegistrationPlugin / UnwindInfoManager standalone
path already threads an explicit DSOBase; this brings the built-in
MachOPlatform path in line. Cost is one ~32-byte local header per user
object.
Add JITLink -noexec regression tests (x86-64 and AArch64) covering the
local-header fallback and the underflow, and Darwin orc-rt execution
tests (arm64 and x86-64) exercising an exception thrown across a
separately-linked object's frames.
---
compiler-rt/lib/orc/macho_platform.cpp | 28 +++++++----
.../lljit-per-graph-compact-unwind-base.cpp | 40 ++++++++++++++++
.../lljit-per-graph-compact-unwind-base.cpp | 40 ++++++++++++++++
.../llvm/ExecutionEngine/Orc/MachOPlatform.h | 1 +
.../lib/ExecutionEngine/Orc/MachOPlatform.cpp | 34 +++++++++-----
.../AArch64/MachO_compact_unwind_dso_base.s | 39 ++++++++++++++++
.../x86-64/MachO_compact_unwind_dso_base.s | 46 +++++++++++++++++++
7 files changed, 209 insertions(+), 19 deletions(-)
create mode 100644 compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
create mode 100644 compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
create mode 100644 llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
create mode 100644 llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
diff --git a/compiler-rt/lib/orc/macho_platform.cpp b/compiler-rt/lib/orc/macho_platform.cpp
index 1443c6c078fde..7cbfe3684e9ff 100644
--- a/compiler-rt/lib/orc/macho_platform.cpp
+++ b/compiler-rt/lib/orc/macho_platform.cpp
@@ -114,30 +114,34 @@ class SPSSerializationTraits<SPSMachOJITDylibDepInfo, MachOJITDylibDepInfo> {
struct UnwindSectionInfo {
std::vector<ExecutorAddrRange> CodeRanges;
+ ExecutorAddr DSOBase;
ExecutorAddrRange DwarfSection;
ExecutorAddrRange CompactUnwindSection;
};
using SPSUnwindSectionInfo =
- SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddrRange,
- SPSExecutorAddrRange>;
+ SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddr,
+ SPSExecutorAddrRange, SPSExecutorAddrRange>;
template <>
class SPSSerializationTraits<SPSUnwindSectionInfo, UnwindSectionInfo> {
public:
static size_t size(const UnwindSectionInfo &USI) {
- return SPSUnwindSectionInfo::AsArgList::size(
- USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ return SPSUnwindSectionInfo::AsArgList::size(USI.CodeRanges, USI.DSOBase,
+ USI.DwarfSection,
+ USI.CompactUnwindSection);
}
static bool serialize(SPSOutputBuffer &OB, const UnwindSectionInfo &USI) {
return SPSUnwindSectionInfo::AsArgList::serialize(
- OB, USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ OB, USI.CodeRanges, USI.DSOBase, USI.DwarfSection,
+ USI.CompactUnwindSection);
}
static bool deserialize(SPSInputBuffer &IB, UnwindSectionInfo &USI) {
return SPSUnwindSectionInfo::AsArgList::deserialize(
- IB, USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ IB, USI.CodeRanges, USI.DSOBase, USI.DwarfSection,
+ USI.CompactUnwindSection);
}
};
@@ -171,9 +175,11 @@ class MachOPlatformRuntimeState {
struct UnwindSections {
UnwindSections(const UnwindSectionInfo &USI)
- : DwarfSection(USI.DwarfSection.toSpan<char>()),
+ : DSOBase(USI.DSOBase.getValue()),
+ DwarfSection(USI.DwarfSection.toSpan<char>()),
CompactUnwindSection(USI.CompactUnwindSection.toSpan<char>()) {}
+ uintptr_t DSOBase;
span<char> DwarfSection;
span<char> CompactUnwindSection;
};
@@ -968,7 +974,13 @@ bool MachOPlatformRuntimeState::lookupUnwindSections(
auto &JD = KV.second;
auto I = JD.UnwindSections.find(reinterpret_cast<char *>(Addr));
if (I != JD.UnwindSections.end()) {
- Info.dso_base = reinterpret_cast<uintptr_t>(JD.Header);
+ // Decode unwind-info offsets against the same base the JITLink
+ // compact-unwind writer used to encode them (a per-graph local Mach-O
+ // header). Fall back to the JITDylib header if no base was recorded
+ // (e.g. DWARF-only registration).
+ Info.dso_base = I->second.DSOBase
+ ? I->second.DSOBase
+ : reinterpret_cast<uintptr_t>(JD.Header);
Info.dwarf_section =
reinterpret_cast<uintptr_t>(I->second.DwarfSection.data());
Info.dwarf_section_length = I->second.DwarfSection.size();
diff --git a/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp b/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
new file mode 100644
index 0000000000000..fb713ce033bab
--- /dev/null
+++ b/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
@@ -0,0 +1,40 @@
+// Exercise compact-unwind through JIT-linked frames under MachOPlatform.
+//
+// Each object gets its own per-graph compact-unwind base (a local Mach-O
+// header) rather than sharing the JITDylib header, so unwinding works no
+// matter where in the address space a graph is emitted -- including below the
+// header. A throw that crosses a separately-linked object's frame forces the
+// unwinder to decode __unwind_info for a graph other than the one holding the
+// header.
+//
+// RUN: %clangxx -fexceptions -fPIC -emit-llvm -c -o %t.throw.bc %s
+// RUN: %clangxx -DMAIN -fexceptions -fPIC -emit-llvm -c -o %t.main.bc %s
+// RUN: %lli_orc_jitlink -relocation-model=pic -extra-module %t.throw.bc \
+// RUN: %t.main.bc | FileCheck %s
+
+// CHECK: in throw_it
+// CHECK-NEXT: caught 42
+
+#include <stdio.h>
+
+#ifdef MAIN
+
+void throw_it();
+
+int main() {
+ try {
+ throw_it();
+ } catch (int X) {
+ printf("caught %d\n", X);
+ }
+ return 0;
+}
+
+#else
+
+void throw_it() {
+ puts("in throw_it");
+ throw 42;
+}
+
+#endif
diff --git a/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp b/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
new file mode 100644
index 0000000000000..fb713ce033bab
--- /dev/null
+++ b/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
@@ -0,0 +1,40 @@
+// Exercise compact-unwind through JIT-linked frames under MachOPlatform.
+//
+// Each object gets its own per-graph compact-unwind base (a local Mach-O
+// header) rather than sharing the JITDylib header, so unwinding works no
+// matter where in the address space a graph is emitted -- including below the
+// header. A throw that crosses a separately-linked object's frame forces the
+// unwinder to decode __unwind_info for a graph other than the one holding the
+// header.
+//
+// RUN: %clangxx -fexceptions -fPIC -emit-llvm -c -o %t.throw.bc %s
+// RUN: %clangxx -DMAIN -fexceptions -fPIC -emit-llvm -c -o %t.main.bc %s
+// RUN: %lli_orc_jitlink -relocation-model=pic -extra-module %t.throw.bc \
+// RUN: %t.main.bc | FileCheck %s
+
+// CHECK: in throw_it
+// CHECK-NEXT: caught 42
+
+#include <stdio.h>
+
+#ifdef MAIN
+
+void throw_it();
+
+int main() {
+ try {
+ throw_it();
+ } catch (int X) {
+ printf("caught %d\n", X);
+ }
+ return 0;
+}
+
+#else
+
+void throw_it() {
+ puts("in throw_it");
+ throw 42;
+}
+
+#endif
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h b/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
index 944f099c38b61..936a360c8623c 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
@@ -242,6 +242,7 @@ class LLVM_ABI MachOPlatform : public Platform {
private:
struct UnwindSections {
SmallVector<ExecutorAddrRange> CodeRanges;
+ ExecutorAddr DSOBase;
ExecutorAddrRange DwarfSection;
ExecutorAddrRange CompactUnwindSection;
};
diff --git a/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp b/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
index d7c0eb6bba2fd..0bee63121e8ba 100644
--- a/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
+++ b/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
@@ -834,12 +834,13 @@ void MachOPlatform::MachOPlatformPlugin::modifyPassConfig(
if (auto *CUSec = LG.findSectionByName(MachOCompactUnwindSectionName))
LG.removeSection(*CUSec);
- // Point the libunwind dso-base absolute symbol at the header for the
- // JITDylib. This will prevent us from synthesizing a new header for
- // every object.
- if (HeaderAddr)
- LG.addAbsoluteSymbol("__jitlink$libunwind_dso_base", HeaderAddr, 0,
- Linkage::Strong, Scope::Local, true);
+ // Note: we intentionally do *not* pin "__jitlink$libunwind_dso_base" to the
+ // JITDylib header. A shared base makes the __unwind_info writer encode
+ // unsigned 32-bit deltas from the header, so any object emitted below it
+ // underflows ("... exceeds 32 bits"). Instead the compact-unwind writer
+ // synthesizes a per-graph local header base (getOrCreateCompactUnwindBase),
+ // which always lays out first, and findUnwindSectionInfo threads that base to
+ // the runtime so the encode and decode (dso_base) bases match.
// If we're in the bootstrap phase then increment the active graphs.
if (LLVM_UNLIKELY(InBootstrapPhase))
@@ -1320,6 +1321,17 @@ MachOPlatform::MachOPlatformPlugin::findUnwindSectionInfo(
if (CodeBlocks.empty())
return std::nullopt;
+ // Record the compact-unwind base that the __unwind_info writer used to encode
+ // its deltas, so the runtime decodes them against the same base. This is the
+ // per-graph "__jitlink$libunwind_dso_base" symbol; if absent (e.g.
+ // DWARF-only) US.DSOBase stays null and the runtime falls back to the
+ // JITDylib header.
+ auto DSOBaseName = G.intern("__jitlink$libunwind_dso_base");
+ if (auto *DSOBaseSym = G.findAbsoluteSymbolByName(DSOBaseName))
+ US.DSOBase = DSOBaseSym->getAddress();
+ else if (auto *DSOBaseSym = G.findDefinedSymbolByName(DSOBaseName))
+ US.DSOBase = DSOBaseSym->getAddress();
+
// We have info to register. Sort the code blocks into address order and
// build a list of contiguous address ranges covering them all.
llvm::sort(CodeBlocks, [](const Block *LHS, const Block *RHS) {
@@ -1413,12 +1425,12 @@ Error MachOPlatform::MachOPlatformPlugin::registerObjectPlatformSections(
MachOPlatformSecs.push_back({SecName, R.getRange()});
}
- std::optional<std::tuple<SmallVector<ExecutorAddrRange>, ExecutorAddrRange,
- ExecutorAddrRange>>
+ std::optional<std::tuple<SmallVector<ExecutorAddrRange>, ExecutorAddr,
+ ExecutorAddrRange, ExecutorAddrRange>>
UnwindInfo;
if (auto UI = findUnwindSectionInfo(G))
- UnwindInfo = std::make_tuple(std::move(UI->CodeRanges), UI->DwarfSection,
- UI->CompactUnwindSection);
+ UnwindInfo = std::make_tuple(std::move(UI->CodeRanges), UI->DSOBase,
+ UI->DwarfSection, UI->CompactUnwindSection);
if (!MachOPlatformSecs.empty() || UnwindInfo) {
// Dump the scraped inits.
@@ -1431,7 +1443,7 @@ Error MachOPlatform::MachOPlatformPlugin::registerObjectPlatformSections(
assert(HeaderAddr && "Null header registered for JD");
using SPSRegisterObjectPlatformSectionsArgs = SPSArgList<
SPSExecutorAddr,
- SPSOptional<SPSTuple<SPSSequence<SPSExecutorAddrRange>,
+ SPSOptional<SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddr,
SPSExecutorAddrRange, SPSExecutorAddrRange>>,
SPSSequence<SPSTuple<SPSString, SPSExecutorAddrRange>>>;
diff --git a/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s b/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
new file mode 100644
index 0000000000000..cd583c74a5429
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
@@ -0,0 +1,39 @@
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=arm64-apple-darwin -filetype=obj -o %t/pos.o %s
+# RUN: llvm-mc -triple=arm64-apple-darwin --defsym DSO_BASE_ABOVE=1 \
+# RUN: -filetype=obj -o %t/neg.o %s
+#
+# Check that the __unwind_info writer chooses a compact-unwind base that is at
+# or below every covered address, so that the unsigned 32-bit deltas it encodes
+# never underflow -- even when the object's code is emitted at a high address.
+#
+# Positive: with no "__jitlink$libunwind_dso_base" symbol the writer synthesizes
+# a per-graph local Mach-O header (laid out first, so lowest), and linking the
+# object at a high slab address succeeds.
+# RUN: llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x800000000000 -slab-page-size 4096 \
+# RUN: %t/pos.o
+#
+# Negative: pinning the base *above* the code (as a shared JITDylib header does
+# when a graph lands below it) underflows and aborts linking. This is the bug
+# that a per-graph base avoids.
+# RUN: not llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x1000 -slab-page-size 4096 \
+# RUN: %t/neg.o 2>&1 | FileCheck %s
+#
+# CHECK: exceeds 32 bits
+
+ .section __TEXT,__text,regular,pure_instructions
+ .globl _main
+ .p2align 2
+_main:
+ .cfi_startproc
+ ret
+ .cfi_endproc
+
+.ifdef DSO_BASE_ABOVE
+ .globl __jitlink$libunwind_dso_base
+ .set __jitlink$libunwind_dso_base, 0xffff000000000000
+.endif
+
+.subsections_via_symbols
diff --git a/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s b/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
new file mode 100644
index 0000000000000..299e7651cb818
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
@@ -0,0 +1,46 @@
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=x86_64-apple-darwin11 -filetype=obj -o %t/pos.o %s
+# RUN: llvm-mc -triple=x86_64-apple-darwin11 --defsym DSO_BASE_ABOVE=1 \
+# RUN: -filetype=obj -o %t/neg.o %s
+#
+# Check that the __unwind_info writer chooses a compact-unwind base that is at
+# or below every covered address, so that the unsigned 32-bit deltas it encodes
+# never underflow -- even when the object's code is emitted at a high address.
+#
+# Positive: with no "__jitlink$libunwind_dso_base" symbol the writer synthesizes
+# a per-graph local Mach-O header (laid out first, so lowest), and linking the
+# object at a high slab address succeeds.
+# RUN: llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x800000000000 -slab-page-size 4096 \
+# RUN: %t/pos.o
+#
+# Negative: pinning the base *above* the code (as a shared JITDylib header does
+# when a graph lands below it) underflows and aborts linking. This is the bug
+# that a per-graph base avoids.
+# RUN: not llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x1000 -slab-page-size 4096 \
+# RUN: %t/neg.o 2>&1 | FileCheck %s
+#
+# CHECK: exceeds 32 bits
+
+ .section __TEXT,__text,regular,pure_instructions
+ .globl _main
+ .p2align 4, 0x90
+_main:
+ .cfi_startproc
+ pushq %rbp
+ .cfi_def_cfa_offset 16
+ .cfi_offset %rbp, -16
+ movq %rsp, %rbp
+ .cfi_def_cfa_register %rbp
+ xorl %eax, %eax
+ popq %rbp
+ retq
+ .cfi_endproc
+
+.ifdef DSO_BASE_ABOVE
+ .globl __jitlink$libunwind_dso_base
+ .set __jitlink$libunwind_dso_base, 0xffff000000000000
+.endif
+
+.subsections_via_symbols
More information about the llvm-commits
mailing list