[llvm] [JITLink][COFF] Synthesize __imp_ IAT entries for dllimport references (PR #203906)
via llvm-commits
llvm-commits at lists.llvm.org
Mon Jun 15 06:33:32 PDT 2026
https://github.com/mkovacevic99 created https://github.com/llvm/llvm-project/pull/203906
Adds a default COFF/x86_64 JITLink pass that synthesizes `__imp_` Import
Address Table entries for dllimport references, so COFF objects using dllimport
JIT-link without a hand-built import library or a special generator.
On COFF, `__declspec(dllimport)` codegen emits indirect accesses through a named
`__imp_X` symbol (`callq *__imp_bar(%rip)`; `movq __imp_g(%rip)` for data), with
`__imp_X` left undefined. JITLink had no handling for this. The new pass — the
COFF counterpart of the ELF/Mach-O GOT builder — defines each undefined external
`__imp_X` over an 8-byte slot holding the address of `X`, and leaves `X` as an
ordinary external to be resolved normally (import library, dynamic-library
search generator, etc.). Both the call and data-access forms then resolve
indirectly through the slot.
Rather than the `GOTTableManager` pattern (anonymous entry + edge redirection),
the pass defines the *named* `__imp_X` symbol over the slot. ELF GOT references
are nameless edge kinds, so that builder must create an anonymous entry and
redirect edges; COFF references `__imp_X` by name, so defining it is simpler —
no edge rewriting, no orphaned-external cleanup, sharing is automatic, and the
call/data-access forms are handled identically.
x86_64 only (runs in the COFF/x86_64 backend's default pass pipeline).
New lit test `COFF_dllimport_iat.s`: assembles an object referencing
`__imp_bar` (call) and `__imp_foo` (data load), supplies `foo`/`bar` via `-abs`,
links with `-noexec`, and uses `jitlink-check` to verify each `__imp_` slot
holds the target's address and that the references resolve through the slot.
Partly implements github issue: https://github.com/llvm/llvm-project/issues/190122
In the comment section of the github issue there is this comment https://github.com/llvm/llvm-project/issues/190122#issuecomment-4617328036
This PR implements point 2 Synthesis IAT entries.
>From 451920eab681fb8c8158e90357545cb09577c2ec Mon Sep 17 00:00:00 2001
From: Milica Kovacevic <mkovacevic at baylibre.com>
Date: Mon, 15 Jun 2026 15:21:58 +0200
Subject: [PATCH] [JITLink][COFF] Synthesize __imp_ IAT entries for dllimport
references
COFF dllimport references are emitted as accesses through a named __imp_X
symbol (callq *__imp_bar(%rip), or movq __imp_g(%rip) for data), where
__imp_X is an undefined external. JITLink previously had no handling for
these, so dllimport objects could only be linked if some generator or
import library supplied the __imp_ definitions.
Add a default COFF/x86_64 JITLink pass that, for each undefined external
__imp_X, defines __imp_X over an 8-byte IAT slot holding the address of X
(X is left external and resolved normally). This is the COFF analog of
the ELF/Mach-O GOT builder. Unlike that builder, it defines the named
__imp_X symbol over the slot rather than redirecting edges to an
anonymous entry, since COFF references __imp_X by name -- which needs no
edge rewriting and handles the call and data-access forms uniformly.
x86_64 only. Direct (non-dllimport) references are out of scope.
---
.../ExecutionEngine/JITLink/COFF_x86_64.cpp | 78 +++++++++++++++++++
.../JITLink/x86-64/COFF_dllimport_iat.s | 55 +++++++++++++
2 files changed, 133 insertions(+)
create mode 100644 llvm/test/ExecutionEngine/JITLink/x86-64/COFF_dllimport_iat.s
diff --git a/llvm/lib/ExecutionEngine/JITLink/COFF_x86_64.cpp b/llvm/lib/ExecutionEngine/JITLink/COFF_x86_64.cpp
index aa91ac053bb50..e31bd2dc86a77 100644
--- a/llvm/lib/ExecutionEngine/JITLink/COFF_x86_64.cpp
+++ b/llvm/lib/ExecutionEngine/JITLink/COFF_x86_64.cpp
@@ -247,6 +247,79 @@ class COFFLinkGraphLowering_x86_64 {
GetImageBaseSymbol GetImageBase;
DenseMap<Section *, orc::ExecutorAddr> SectionStartCache;
};
+
+// Synthesize COFF __imp_ Import Address Table (IAT) entries.
+//
+// For a dllimport reference, codegen emits an indirect access through a named
+// __imp_X symbol, e.g.
+//
+// callq *__imp_bar(%rip) ; or, for data: movq __imp_g(%rip), %rax
+//
+// where __imp_X is an undefined external. This pass supplies the missing IAT
+// entry by defining __imp_X over an 8-byte pointer slot that holds X's address:
+//
+// __imp_bar:
+// .quad bar ; X is resolved as an ordinary external
+//
+// X is left external, so its address is provided by whatever resolves the
+// JITDylib's externals (an import library, a DynamicLibrarySearchGenerator,
+// AutoImportGenerator, ...). If X is unresolvable the link fails, exactly as a
+// static link against the corresponding import library would.
+//
+// This is the COFF analog of the ELF/Mach-O GOT builder, but deliberately NOT
+// written as a TableManager/visitEdge pass like x86_64::GOTTableManager. ELF's
+// GOT references are *nameless* edge kinds, so that builder has to create an
+// anonymous entry and redirect every edge to it (and, for our case, would then
+// have to delete the now-orphaned __imp_X external so it isn't looked up).
+// COFF instead references a *named* __imp_X symbol, so the simpler and more
+// natural thing is to define that symbol over the slot: edges to __imp_X then
+// resolve to it with no edge rewriting and no orphan cleanup, call and
+// data-access references are handled identically, and sharing is automatic
+// because there is exactly one __imp_X symbol per import.
+//
+// Direct (non-dllimport) references such as `callq foo` are intentionally not
+// handled here: those are either kept in range by the slab allocator or thunked
+// by the opt-in AutoImportGenerator -- both outside this pass.
+Error synthesizeIATEntries_COFF_x86_64(LinkGraph &G) {
+ static constexpr StringRef ImpPrefix = "__imp_";
+
+ // Collect the external __imp_ symbols up front: we mutate the symbol lists
+ // below (makeDefined / addExternalSymbol).
+ SmallVector<Symbol *, 8> Imps;
+ for (auto *Sym : G.external_symbols())
+ if (Sym->hasName() && (*Sym->getName()).starts_with(ImpPrefix))
+ Imps.push_back(Sym);
+ if (Imps.empty())
+ return Error::success();
+
+ auto FindByName = [&](StringRef Name) -> Symbol * {
+ for (auto *Sym : G.external_symbols())
+ if (Sym->hasName() && *Sym->getName() == Name)
+ return Sym;
+ for (auto *Sym : G.defined_symbols())
+ if (Sym->hasName() && *Sym->getName() == Name)
+ return Sym;
+ return nullptr;
+ };
+
+ Section &IATSec = G.createSection("$__IAT", orc::MemProt::Read);
+
+ for (auto *Imp : Imps) {
+ StringRef Base = (*Imp->getName()).drop_front(ImpPrefix.size());
+
+ // Find the real target X, or add it as an external to be resolved normally.
+ Symbol *Target = FindByName(Base);
+ if (!Target)
+ Target = &G.addExternalSymbol(Base, 0, /*IsWeaklyReferenced=*/false);
+
+ // 8-byte slot holding &X, with __imp_X defined over it.
+ Symbol &Slot = x86_64::createAnonymousPointer(G, IATSec, Target);
+ G.makeDefined(*Imp, Slot.getBlock(), 0, G.getPointerSize(), Linkage::Strong,
+ Scope::Local, /*IsLive=*/true);
+ }
+
+ return Error::success();
+}
} // namespace
namespace llvm {
@@ -303,6 +376,11 @@ void link_COFF_x86_64(std::unique_ptr<LinkGraph> G,
} else
Config.PrePrunePasses.push_back(markAllSymbolsLive);
+ // Synthesize __imp_X IAT entries for dllimport references, like the GOT/PLT
+ // builders for ELF/Mach-O. Runs in PostPrune (before external-symbol
+ // lookup) so the X targets it introduces are resolved normally.
+ Config.PostPrunePasses.push_back(synthesizeIATEntries_COFF_x86_64);
+
// Add COFF edge lowering passes.
Config.PreFixupPasses.push_back(COFFLinkGraphLowering_x86_64());
}
diff --git a/llvm/test/ExecutionEngine/JITLink/x86-64/COFF_dllimport_iat.s b/llvm/test/ExecutionEngine/JITLink/x86-64/COFF_dllimport_iat.s
new file mode 100644
index 0000000000000..754fa956758a5
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/x86-64/COFF_dllimport_iat.s
@@ -0,0 +1,55 @@
+# Verify the COFF __imp_ IAT synthesis pass: for a dllimport reference to an
+# undefined __imp_X symbol, JITLink should define __imp_X over an 8-byte pointer
+# slot that holds the address of X (resolved as an ordinary external). Both the
+# call form (callq *__imp_X) and the data-access form (movq __imp_X) resolve
+# indirectly through that slot.
+#
+# X (foo/bar) is supplied as an absolute symbol, so no real library is needed --
+# this exercises the pass itself, not any resolution mechanism.
+#
+# RUN: llvm-mc -filetype=obj -triple=x86_64-windows-msvc %s -o %t.o
+# RUN: llvm-jitlink -noexec \
+# RUN: -slab-allocate 100Kb -slab-address 0xfff00000 -slab-page-size 4096 \
+# RUN: -abs foo=0x7ff700000000 \
+# RUN: -abs bar=0x7ff700001000 \
+# RUN: -check %s %t.o
+
+ .text
+
+ .def main;
+ .scl 2;
+ .type 32;
+ .endef
+ .globl main
+ .p2align 4, 0x90
+main:
+ retq
+
+# The synthesized __imp_bar slot holds bar's address...
+# jitlink-check: *{8}(__imp_bar) = bar
+# ... and the dllimport call reads through that slot (RIP-relative displacement
+# of the indirect call's memory operand, MCInst operand 3).
+# jitlink-check: decode_operand(test_call, 3) = __imp_bar - next_pc(test_call)
+ .def test_call;
+ .scl 2;
+ .type 32;
+ .endef
+ .globl test_call
+ .p2align 4, 0x90
+test_call:
+ callq *__imp_bar(%rip)
+ retq
+
+# Same for a data access: the __imp_foo slot holds foo's address, and the load
+# reads through it (displacement is MCInst operand 4 for `movq mem, reg`).
+# jitlink-check: *{8}(__imp_foo) = foo
+# jitlink-check: decode_operand(test_load, 4) = __imp_foo - next_pc(test_load)
+ .def test_load;
+ .scl 2;
+ .type 32;
+ .endef
+ .globl test_load
+ .p2align 4, 0x90
+test_load:
+ movq __imp_foo(%rip), %rax
+ retq
More information about the llvm-commits
mailing list