[LLD][COFF] /OPT:REF strips symbols referenced only via __imp_ local imports; IAT slots are written as ImageBase with no diagnostic
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
The below AI analysis is the first of two issues that are preventing DTLTO from working with our DLL setup and crashing. I'm currently using `/OPT:NOREF` and `/ignore:4217` as a workaround but the generated code is not optimal. I'll link the other issue in the comments.
---
When an undefined __imp_X is fulfilled by a locally defined X (local import), lld-link's /OPT:REF does not treat the local import as a reference to X. If X has no other live reference, its section is stripped. The local import pointer is still emitted and contains ImageBase + 0, with a base relocation. There is no diagnostic for the stripped definition; the "locally defined symbol imported" warning is emitted as usual, in both the broken and working configurations. Calling through the slot jumps to the image header.
MSVC link.exe keeps the definition for identical inputs.
Repro (no LTO):
```
// def.cpp
extern "C" __declspec(noinline) int target(int x) { volatile int v = x; return v * 7; }
extern "C" __declspec(noinline) int keeper(int x) { volatile int v = x; return v + 1; }
```
```
// user.cpp
extern "C" __declspec(dllimport) int target(int);
extern "C" int keeper(int);
extern "C" int entry() { return target(1) + keeper(2); }
```
`clang-cl -c -O2 def.cpp user.cpp`
`lld-link /entry:entry /subsystem:console /opt:ref /out:t.exe def.obj user.obj`
Result: target's body is absent from .text; entry calls through a slot containing 0x140000000 (ImageBase), base-relocated:
`llvm-objdump -d --section=.text t.exe` -> callq *0x…(%rip) # slot RVA
`llvm-objdump -s -j .rdata t.exe` -> slot bytes: 00000040 01000000
Comparisons on the same objects:
- lld-link /opt:noref (or /debug, which implies it): body present, slot correct.
- MSVC link /opt:ref /nodefaultlib: body present, slot correct, LNK4217 warning.
All variants warn locally defined symbol imported: target; only lld with /opt:ref strips the definition.
Analysis (current main):
- SymbolTable::resolveRemainingUndefines replaces `__imp_X` with a DefinedLocalImport wrapping the defined X and queues its LocalImportChunk. This runs before markLive (Driver.cpp).
- MarkLive.cpp addSym handles DefinedRegular, DefinedImportData, and DefinedImportThunk; there is no case for DefinedLocalImport, so a live section referencing `__imp_X` marks nothing.
- The Writer emits local import chunks unconditionally. LocalImportChunk::writeTo writes sym->getRVA() + imageBase; a discarded symbol's RVA is 0. getBaserels registers a relocation for the slot unconditionally.
Suggested fix: add a DefinedLocalImport case to addSym that marks the wrapped Defined live. The wrapped symbol is currently only stored in the private LocalImportChunk::sym, so an accessor is needed.
This pattern occurs at scale with distributed ThinLTO, where backends emit `__imp_` references for symbols that resolve within the link unit (). In one large mixed application, 26,910 of 72,641 import slots contained the image base; the link reported no errors.
Verified with lld 23.1.0; the cited code matches current main.
Contributor guide
Research direction
Start with the no-LTO repro using clang-cl and lld-link, then inspect MarkLive.cpp's addSym handling and the LocalImportChunk path described in the issue; Driver.cpp identifies when resolution precedes markLive. Compare /opt:ref with /opt:noref and confirm the target body remains present and the import slot contains the target address rather than ImageBase.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100