Every DLL in the ordinary import directory is mapped and resolved before your entry point runs — even the one used only by a rarely-taken error path. That costs startup time, and it makes an optional dependency fatal: if the DLL is absent, the process fails to launch, whether or not your program ever needed it.
Delay-load imports change both facts. The spec states the purpose directly: the tables were added "to support a uniform mechanism for applications to delay the loading of a DLL until the first call into that DLL". Startup gets faster, an unused optional DLL is never opened, and a missing DLL turns a launch failure into a first-call failure. For anyone analyzing binaries it also matters that this second import machinery exists at all — a file can carry two parallel sets of import information, and only one of them is walked by the loader at process start.
Compare the two schemes:
The descriptor mirrors the ordinary import descriptor: a DLL name RVA, a name table (like the ILT), an address table (like the IAT), plus fields the ordinary table does not need — a place to store the DLL's module handle, and an unload table used to undo everything.
The model's missing piece: the OS loader does not read this table. The helper linked into your image does, at call time. That is the entire point — resolution work moves from "always, at startup" to "only if used, on first call".
Data directory index 13 (Delay Import Descriptor) points to an array of 32-byte descriptors. The spec's layout:
| Offset | Size | Field | What it says |
|---|---|---|---|
| 0 | 4 | Attributes | Bit 0 is RvaBased; all other bits reserved and must be zero |
| 4 | 4 | Name | RVA of the ASCII DLL name to delay-load |
| 8 | 4 | Module Handle | RVA of an HMODULE slot in .data where the helper stores the loaded module |
| 12 | 4 | Delay Import Address Table | RVA of the delay IAT the helper patches with real entry points |
| 16 | 4 | Delay Import Name Table | RVA of the delay INT — same layout as the ordinary import lookup table |
| 20 | 4 | Bound Delay Import Table | Optional RVA of a pre-bound thunk table |
| 24 | 4 | Unload Delay Import Table | Optional RVA of a pristine copy of the delay IAT, for unload |
| 28 | 4 | Time Stamp | Timestamp of the DLL this image was bound to |
The Attributes bit is the compatibility hinge. In winnt.h the field is IMAGE_DELAYLOAD_DESCRIPTOR.Attributes whose low bit is RvaBased; in delayimp.h it is grAttrs with the dlattrRva flag (value 0x1). When set, every address field above is an RVA — the form Visual C++ 7.0 and later emit. When clear, those fields are raw virtual addresses (the pre-VC7 form), and a reader must interpret them differently. The data directory's Size tells you how many descriptors there are, and readers also stop at an all-zero descriptor, the same convention as the ordinary import directory.
The unload table exists because the process can unbind: on an unload request the helper frees the DLL, clears the module handle, and copies the unload table back over the delay IAT so subsequent calls fall into the thunks again instead of jumping into a freed module.
LoadLibrary/GetProcAddress code, which records no static table at all.Neither sample image delay-loads anything. In cli-64.exe and conpty.dll, data directory index 13 is all zeros:
cli-64.exe: Entry d 0000000000000000 00000000 Delay Import Directory conpty.dll: Entry d 0000000000000000 00000000 Delay Import Directory
That is the honest state of most ordinary binaries — delay-loading only exists when the linker is asked for it (the MSVC linker's /DELAYLOAD:dllname.dll switch, whose helper ships in delayimp.h/delayimp.lib). So this lesson's example is the structure itself, taken from the spec above. Picture one descriptor for a hypothetical PRINTER.dll with four imported names: Attributes 0x00000001 (RVA-based), Name pointing at the string PRINTER.dll in read-only data, Module Handle pointing at an eight-byte zero in .data, Delay IAT holding four slots that currently contain helper-stub addresses, Delay INT holding four thunk entries exactly like ordinary ILT entries (hint/name RVAs or the ordinal flag), Bound and Unload tables zero, Time Stamp zero.
Reading that descriptor tells you: nothing has been loaded yet, the helper knows where to write the four resolved addresses, and the ordinary import directory (index 1) will show none of these four symbols — they live only here.
Check any PE for a delay directory in one command:
$ objdump -p cli-64.exe | grep "Delay Import"
Entry d 0000000000000000 00000000 Delay Import DirectoryOr read data directory index 13 directly. The data directories start at optional-header offset 112 in PE32+, and each entry is 8 bytes, so index 13 sits at offset 112 + 13*8 = 216:
$ python3 -c "import struct;d=open('cli-64.exe','rb').read();e=struct.unpack_from('<I',d,0x3c)[0];oh=e+24;rva,sz=struct.unpack_from('<II',d,oh+112+13*8);print(hex(rva),hex(sz))"
0x0 0x0Swap in a binary built with /DELAYLOAD and both numbers become non-zero; then walk the descriptor array rva-by-32-bytes until an all-zero row, and you have the list of deferred DLLs.
Without looking back: what does bit 0 of the delay descriptor's Attributes field decide?
You open a PE and data directory index 13 reads VirtualAddress = 0, Size = 0. What can you conclude?
The pattern: an empty data directory entry means "this feature is absent", not "look harder". Every PE table in this course is announced by an address/size pair — read the pair first.
Imports, exports, and delay-loads all assume the image loads exactly where the linker put it — ImageBase 0x140000000 in our exe. Windows would rather randomize that address, and the addresses baked into .data and .text break the moment it moves. Module 6 starts with the table that repairs them.
Next: Base Relocations — the .reloc blocks that make ASLR possible.