[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