[Mlir-commits] [llvm] [mlir] [AArch64][RuntimeDyld] Handle far external symbols in COFF JIT code (PR #229719)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Wed Oct 7 03:34:11 PDT 2026
https://github.com/yafet-a created https://github.com/llvm/llvm-project/pull/229719
External symbols on Windows can be more than 4 GiB from JIT allocations and therefore outside ADRP's signed 21-bit page range. RuntimeDyld currently truncates the relocation, producing an invalid address without reporting the error.
For an external address materialised with an ADRP+ADD pair, we now create a pointer slot in the section's stub area and rewrite the ADD to an LDR from that slot. This leaves non-zero addends and other instruction patterns unchanged. We then report any remianing ADR/ADRP relocation overflows instead of silently truncating them.
Also added `llvm-rtdyld` coverage and a WoA MLIR ExecutionEngine test that tests this by placing JIT allocations more than 4 GiB from external symbols.
This PR is related to #173269 which mentions a similar AArch64 ADRP range failure. It's just that it's scoped to fix COFF external-symbol relocations.
>From 41b022726b4287cbfd20702c7056bfab20ec8f6a Mon Sep 17 00:00:00 2001
From: Reid Kleckner <rkleckner at nvidia.com>
Date: Tue, 22 Sep 2026 18:05:24 +0000
Subject: [PATCH] [AArch64][RuntimeDyld] Handle far external symbols in COFF
JIT code
External symbols on Windows can be more than 4 GiB from JIT allocations and
therefore outside ADRP's signed 21-bit page range. Detect ADRP relocation
overflow instead of silently truncating it.
For an external address materialized with an ADRP+ADD pair, create a pointer
slot in the section's stub area and rewrite the ADD to an LDR from that slot.
Leave non-zero addends and other instruction patterns unchanged.
Add llvm-rtdyld coverage and a Windows-on-Arm MLIR ExecutionEngine test that
places JIT allocations more than 4 GiB from external symbols.
---
.../Targets/RuntimeDyldCOFFAArch64.h | 94 ++++++++++
.../RuntimeDyld/AArch64/COFF_AArch64.s | 31 ++-
mlir/unittests/ExecutionEngine/CMakeLists.txt | 1 +
.../ExecutionEngine/WindowsArm64.cpp | 177 ++++++++++++++++++
4 files changed, 302 insertions(+), 1 deletion(-)
create mode 100644 mlir/unittests/ExecutionEngine/WindowsArm64.cpp
diff --git a/llvm/lib/ExecutionEngine/RuntimeDyld/Targets/RuntimeDyldCOFFAArch64.h b/llvm/lib/ExecutionEngine/RuntimeDyld/Targets/RuntimeDyldCOFFAArch64.h
index 3ca6309874de5f..c674bdc65a03a7 100644
--- a/llvm/lib/ExecutionEngine/RuntimeDyld/Targets/RuntimeDyldCOFFAArch64.h
+++ b/llvm/lib/ExecutionEngine/RuntimeDyld/Targets/RuntimeDyldCOFFAArch64.h
@@ -66,6 +66,15 @@ static void write32AArch64Addr(void *T, uint64_t s, uint64_t p, int shift) {
using namespace llvm::support::endian;
uint64_t Imm = (s >> shift) - (p >> shift);
+ // ADRP's page offset is 21 bits signed (+/-4 GiB); fail loudly rather than
+ // truncate into a wild pointer.
+ if (!isInt<21>(static_cast<int64_t>(Imm))) {
+ report_fatal_error(Twine("ADRP target out of range: target=") + Twine(s) +
+ " pc=" + Twine(p) +
+ " delta=" + Twine(s > p ? s - p : p - s) +
+ (s > p ? " (target above pc)" : " (target below pc)") +
+ " -- needs a 21-bit signed page offset (+/-4 GiB)");
+ }
uint32_t ImmLo = (Imm & 0x3) << 29;
uint32_t ImmHi = (Imm & 0x1FFFFC) << 3;
uint64_t Mask = (0x3 << 29) | (0x1FFFFC << 3);
@@ -108,6 +117,49 @@ class RuntimeDyldCOFFAArch64 : public RuntimeDyldCOFF {
unsigned getMaxStubSize() const override { return 20; }
+ /// GOT slot in this section's stub area holding the address of \p Name, so
+ /// an ADRP can reach external symbols (e.g. DLL exports) beyond +/-4 GiB.
+ /// computeSectionStubBufSize reserves getMaxStubSize() per relocation, which
+ /// covers the 8 bytes.
+ uint64_t getGOTEntryOffset(unsigned SectionID, StubMap &Stubs, StringRef Name,
+ bool CreateIfMissing) {
+ // Key on the symbol alone; ~0 cannot collide with a branch stub's offset.
+ RelocationValueRef Key;
+ Key.SectionID = SectionID;
+ Key.Offset = ~uint64_t(0);
+ Key.SymbolName = Name.data();
+
+ // RelocationValueRef compares symbol-name pointers rather than their
+ // contents, but paired COFF relocations need not carry the same pointer.
+ for (const auto &[StubKey, StubOffset] : Stubs)
+ if (StubKey.SectionID == SectionID && StubKey.Offset == ~uint64_t(0) &&
+ StubKey.SymbolName && StringRef(StubKey.SymbolName) == Name)
+ return StubOffset;
+ if (!CreateIfMissing)
+ return ~uint64_t(0);
+
+ SectionEntry &Sec = Sections[SectionID];
+ uint64_t EntryOffset = alignTo(Sec.getStubOffset(), 8);
+ Sec.advanceStubOffset(EntryOffset + 8 - Sec.getStubOffset());
+ Stubs[Key] = EntryOffset;
+
+ // Have the resolver write the symbol's absolute address into the slot.
+ RelocationEntry RE(SectionID, EntryOffset, COFF::IMAGE_REL_ARM64_ADDR64, 0,
+ /*IsPCRel=*/false, Log2_64(8));
+ addRelocationForSymbol(RE, Name);
+
+ LLVM_DEBUG(dbgs() << " Created GOT entry for " << Name << " at offset "
+ << EntryOffset << "\n");
+ return EntryOffset;
+ }
+
+ /// ADD (immediate, no shift): the second half of the ADRP+ADD idiom.
+ static bool isAddImmediate(uint32_t Inst) {
+ // sf 0 0 100010 sh imm12 Rn Rd with sh == 0; excludes ADDS (bit 29).
+ return (Inst & 0x7FC00000) == 0x11000000;
+ }
+
+
std::tuple<uint64_t, uint64_t, uint64_t>
generateRelocationStub(unsigned SectionID, StringRef TargetName,
uint64_t Offset, uint64_t RelType, uint64_t Addend,
@@ -231,14 +283,56 @@ class RuntimeDyldCOFFAArch64 : public RuntimeDyldCOFF {
case COFF::IMAGE_REL_ARM64_PAGEBASE_REL21: {
uint32_t orig = read32le(Displacement);
Addend = ((orig >> 29) & 0x3) | ((orig >> 3) & 0x1FFFFC);
+
+ // External symbols are routinely beyond ADRP's +/-4 GiB on Windows and
+ // there is no veneer, so route ADRP+ADD through a GOT slot. Only that
+ // idiom qualifies: ADRP+LDR reads the symbol's contents, and a non-zero
+ // addend cannot be expressed by a slot holding plain `sym`. LLVM codegen
+ // emits direct addresses as a MOVaddr pseudo and expands it after
+ // scheduling into this adjacent ADRP+ADD pair.
+ if (IsExtern && Addend == 0 &&
+ RelType == COFF::IMAGE_REL_ARM64_PAGEBASE_REL21) {
+ uint32_t NextInst = read32le(Displacement + 4);
+ if (isAddImmediate(NextInst) && ((NextInst >> 10) & 0xFFF) == 0) {
+ TargetOffset = getGOTEntryOffset(SectionID, Stubs, TargetName,
+ /*CreateIfMissing=*/true);
+ TargetSectionID = SectionID;
+ TargetName = StringRef();
+ IsExtern = false;
+ Addend = 0;
+ }
+ }
break;
}
case COFF::IMAGE_REL_ARM64_PAGEOFFSET_12L:
case COFF::IMAGE_REL_ARM64_PAGEOFFSET_12A: {
uint32_t orig = read32le(Displacement);
Addend = ((orig >> 10) & 0xFFF);
+
+ // Partner of the ADRP above; no slot means that ADRP was left alone.
+ if (IsExtern && Addend == 0 &&
+ RelType == COFF::IMAGE_REL_ARM64_PAGEOFFSET_12A) {
+ uint64_t GOTOffset = getGOTEntryOffset(SectionID, Stubs, TargetName,
+ /*CreateIfMissing=*/false);
+ if (GOTOffset != ~uint64_t(0) && isAddImmediate(orig)) {
+ // ADD Xd, Xn, #imm -> LDR Xd, [Xn, #imm]: load the slot instead.
+ uint32_t Rd = orig & 0x1F;
+ uint32_t Rn = (orig >> 5) & 0x1F;
+ write32le(AddendSection.getAddressWithOffset(Offset),
+ 0xF9400000u | (Rn << 5) | Rd);
+
+ // 12L so the resolver scales the immediate for a 64-bit load.
+ RelType = COFF::IMAGE_REL_ARM64_PAGEOFFSET_12L;
+ TargetOffset = GOTOffset;
+ TargetSectionID = SectionID;
+ TargetName = StringRef();
+ IsExtern = false;
+ Addend = 0;
+ }
+ }
break;
}
+
case COFF::IMAGE_REL_ARM64_ADDR64: {
Addend = read64le(Displacement);
break;
diff --git a/llvm/test/ExecutionEngine/RuntimeDyld/AArch64/COFF_AArch64.s b/llvm/test/ExecutionEngine/RuntimeDyld/AArch64/COFF_AArch64.s
index 9a31952b69973e..57ad249048ac4a 100644
--- a/llvm/test/ExecutionEngine/RuntimeDyld/AArch64/COFF_AArch64.s
+++ b/llvm/test/ExecutionEngine/RuntimeDyld/AArch64/COFF_AArch64.s
@@ -1,5 +1,6 @@
+
// RUN: llvm-mc -triple aarch64-windows -filetype obj -o %t.obj %s
-// RUN: llvm-rtdyld -triple aarch64-windows -dummy-extern dummy=0x79563413 -dummy-extern dummyA=0x78566879 -target-addr-start=40960000000000 -verify -check %s %t.obj
+// RUN: llvm-rtdyld -triple aarch64-windows -dummy-extern dummy=0x79563413 -dummy-extern dummyA=0x78566879 -dummy-extern far_data=0x1122334455667788 -dummy-extern near_data=40960000001000 -target-addr-start=40960000000000 -verify -check %s %t.obj
.text
.def _bnamed
@@ -80,6 +81,34 @@ ldr1:
ldr x0, [x0, #:lo12:_const]
ret
+# A far external address formed with ADRP+ADD is rewritten to ADRP+LDR through
+# a pointer slot in this section's stub area.
+# rtdyld-check: decode_operand(far_adrp, 1) = (stub_addr(COFF_AArch64.s.tmp.obj/.text, far_data)[32:12] - far_adrp[32:12])
+# rtdyld-check: (*{4}far_ldr)[31:22] = 0x3e5
+# rtdyld-check: decode_operand(far_ldr, 2) = stub_addr(COFF_AArch64.s.tmp.obj/.text, far_data)[11:3]
+# rtdyld-check: *{8}stub_addr(COFF_AArch64.s.tmp.obj/.text, far_data) = 0x1122334455667788
+ .globl _test_far_external_address
+ .align 2
+_test_far_external_address:
+far_adrp:
+ adrp x0, far_data
+far_ldr:
+ add x0, x0, far_data at PAGEOFF
+ ret
+
+# A non-zero page-offset addend cannot use a slot holding plain `sym`. Keep the
+# original ADRP+ADD pair instead of partially rewriting it to address the slot.
+# rtdyld-check: (*{4}near_addend_add)[31:22] = 0x244
+# rtdyld-check: decode_operand(near_addend_add, 2) = (near_data+8)[11:0]
+ .globl _test_external_address_with_addend
+ .align 2
+_test_external_address_with_addend:
+ adrp x0, near_data
+near_addend_add:
+ add x0, x0, near_data at PAGEOFF+8
+ ret
+
+
.globl _test_add_reloc
.align 2
diff --git a/mlir/unittests/ExecutionEngine/CMakeLists.txt b/mlir/unittests/ExecutionEngine/CMakeLists.txt
index b83163e39c0142..e1b3a79dd939fd 100644
--- a/mlir/unittests/ExecutionEngine/CMakeLists.txt
+++ b/mlir/unittests/ExecutionEngine/CMakeLists.txt
@@ -9,6 +9,7 @@ add_mlir_unittest(MLIRExecutionEngineTests
DynamicMemRef.cpp
StridedMemRef.cpp
Invoke.cpp
+ WindowsArm64.cpp
)
mlir_target_link_libraries(MLIRExecutionEngineTests
diff --git a/mlir/unittests/ExecutionEngine/WindowsArm64.cpp b/mlir/unittests/ExecutionEngine/WindowsArm64.cpp
new file mode 100644
index 00000000000000..f1e01b7ad7b6ed
--- /dev/null
+++ b/mlir/unittests/ExecutionEngine/WindowsArm64.cpp
@@ -0,0 +1,177 @@
+//===- WindowsArm64.cpp - Windows on Arm execution engine tests -----------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#if defined(_WIN32) && (defined(_M_ARM64) || defined(__aarch64__))
+
+#include "mlir/ExecutionEngine/ExecutionEngine.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/MLIRContext.h"
+#include "llvm/ExecutionEngine/SectionMemoryManager.h"
+#include "llvm/IR/Constants.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
+
+#include "gtest/gtest.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <limits>
+#include <system_error>
+
+using namespace mlir;
+
+namespace {
+
+constexpr uintptr_t FourGiB = uintptr_t{4} << 30;
+constexpr uintptr_t EightGiB = uintptr_t{8} << 30;
+
+static int FarData;
+static int CtorCount;
+static int DtorCount;
+
+static void ctorCallback() { ++CtorCount; }
+static void dtorCallback() { ++DtorCount; }
+
+// Places JIT allocations at least 8 GiB from Anchor. This makes the test
+// independent of Windows ASLR's usual (but not guaranteed) separation of the
+// module-image, heap, and VirtualAlloc regions.
+class FarMemoryMapper final : public llvm::SectionMemoryManager::MemoryMapper {
+public:
+ explicit FarMemoryMapper(uintptr_t Anchor)
+ : Anchor(Anchor),
+ NextHint(Anchor > EightGiB ? Anchor - EightGiB : Anchor + EightGiB) {}
+
+ llvm::sys::MemoryBlock
+ allocateMappedMemory(llvm::SectionMemoryManager::AllocationPurpose,
+ size_t NumBytes, const llvm::sys::MemoryBlock *,
+ unsigned Flags, std::error_code &EC) override {
+ // LLVM's Windows mapper passes the end of Near as a VirtualAlloc address
+ // hint. A zero-sized fake block therefore gives this test direct control
+ // of the requested address without committing a multi-gigabyte range.
+ llvm::sys::MemoryBlock Hint(reinterpret_cast<void *>(NextHint), 0);
+ llvm::sys::MemoryBlock Block =
+ llvm::sys::Memory::allocateMappedMemory(NumBytes, &Hint, Flags, EC);
+ if (EC)
+ return Block;
+
+ uintptr_t Address = reinterpret_cast<uintptr_t>(Block.base());
+ uintptr_t Distance = Address > Anchor ? Address - Anchor : Anchor - Address;
+ if (Distance <= FourGiB) {
+ llvm::sys::Memory::releaseMappedMemory(Block);
+ EC = std::make_error_code(std::errc::not_enough_memory);
+ return {};
+ }
+
+ MinimumDistance = std::min(MinimumDistance, Distance);
+ // Keep all JIT objects close to each other while preserving their distance
+ // from the external symbols in this executable.
+ NextHint = Address + Block.allocatedSize();
+ return Block;
+ }
+
+ std::error_code protectMappedMemory(const llvm::sys::MemoryBlock &Block,
+ unsigned Flags) override {
+ return llvm::sys::Memory::protectMappedMemory(Block, Flags);
+ }
+
+ std::error_code releaseMappedMemory(llvm::sys::MemoryBlock &Block) override {
+ return llvm::sys::Memory::releaseMappedMemory(Block);
+ }
+
+ uintptr_t getMinimumDistance() const { return MinimumDistance; }
+
+private:
+ uintptr_t Anchor;
+ uintptr_t NextHint;
+ uintptr_t MinimumDistance = std::numeric_limits<uintptr_t>::max();
+};
+
+static std::unique_ptr<llvm::Module>
+buildFarAddressModule(Operation *, llvm::LLVMContext &context) {
+ auto module = std::make_unique<llvm::Module>("windows-arm64-far", context);
+ llvm::IRBuilder<> builder(context);
+ auto *voidFnTy = llvm::FunctionType::get(builder.getVoidTy(), false);
+
+ auto addCtorOrDtor = [&](llvm::StringRef callbackName,
+ llvm::StringRef functionName, bool isCtor) {
+ auto *callback = llvm::Function::Create(
+ voidFnTy, llvm::GlobalValue::ExternalLinkage, callbackName, *module);
+ auto *function = llvm::Function::Create(
+ voidFnTy, llvm::GlobalValue::InternalLinkage, functionName, *module);
+ builder.SetInsertPoint(
+ llvm::BasicBlock::Create(context, "entry", function));
+ builder.CreateCall(callback);
+ builder.CreateRetVoid();
+ if (isCtor)
+ llvm::appendToGlobalCtors(*module, function, 0);
+ else
+ llvm::appendToGlobalDtors(*module, function, 0);
+ };
+ addCtorOrDtor("ctor_callback", "ctor", true);
+ addCtorOrDtor("dtor_callback", "dtor", false);
+
+ auto *farData = new llvm::GlobalVariable(*module, builder.getInt32Ty(), false,
+ llvm::GlobalValue::ExternalLinkage,
+ nullptr, "far_data");
+ auto *addressFnTy = llvm::FunctionType::get(builder.getInt64Ty(), false);
+ auto *addressFn =
+ llvm::Function::Create(addressFnTy, llvm::GlobalValue::ExternalLinkage,
+ "get_far_data_address", *module);
+ builder.SetInsertPoint(llvm::BasicBlock::Create(context, "entry", addressFn));
+ builder.CreateRet(builder.CreatePtrToInt(farData, builder.getInt64Ty()));
+ return module;
+}
+
+TEST(MLIRExecutionEngine, ConstructorsAndFarExternalAddressOnWindowsArm64) {
+ CtorCount = 0;
+ DtorCount = 0;
+
+ MLIRContext context;
+ OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
+ FarMemoryMapper mapper(reinterpret_cast<uintptr_t>(&FarData));
+ ExecutionEngineOptions options;
+ options.llvmModuleBuilder = buildFarAddressModule;
+ options.sectionMemoryMapper = &mapper;
+
+ auto jitOrError = ExecutionEngine::create(*module, options);
+ if (!jitOrError)
+ FAIL() << llvm::toString(jitOrError.takeError());
+ std::unique_ptr<ExecutionEngine> jit = std::move(*jitOrError);
+ jit->registerSymbols([](llvm::orc::MangleAndInterner interner) {
+ llvm::orc::SymbolMap symbols;
+ symbols[interner("far_data")] = {llvm::orc::ExecutorAddr::fromPtr(&FarData),
+ llvm::JITSymbolFlags::Exported};
+ symbols[interner("ctor_callback")] = {
+ llvm::orc::ExecutorAddr::fromPtr(&ctorCallback),
+ llvm::JITSymbolFlags::Exported};
+ symbols[interner("dtor_callback")] = {
+ llvm::orc::ExecutorAddr::fromPtr(&dtorCallback),
+ llvm::JITSymbolFlags::Exported};
+ return symbols;
+ });
+
+ EXPECT_EQ(CtorCount, 0);
+ jit->initialize();
+ EXPECT_EQ(CtorCount, 1);
+
+ auto addressOrError = jit->lookup("get_far_data_address");
+ if (!addressOrError)
+ FAIL() << llvm::toString(addressOrError.takeError());
+ auto getAddress = reinterpret_cast<uintptr_t (*)()>(*addressOrError);
+ EXPECT_EQ(getAddress(), reinterpret_cast<uintptr_t>(&FarData));
+ EXPECT_GT(mapper.getMinimumDistance(), FourGiB);
+
+ jit.reset();
+ EXPECT_EQ(DtorCount, 1);
+}
+
+} // namespace
+
+#endif // Windows/AArch64
More information about the Mlir-commits
mailing list