Back to course

Delay-Load Imports

15 minintermediateModule 5: Imports & ExportsContinues from pe-exports
15 min · Module 5: Imports & Exports · Structures

Why This Matters

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.

A Simple Model

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".

The Actual Detail

Data directory index 13 (Delay Import Descriptor) points to an array of 32-byte descriptors. The spec's layout:

OffsetSizeFieldWhat it says
04AttributesBit 0 is RvaBased; all other bits reserved and must be zero
44NameRVA of the ASCII DLL name to delay-load
84Module HandleRVA of an HMODULE slot in .data where the helper stores the loaded module
124Delay Import Address TableRVA of the delay IAT the helper patches with real entry points
164Delay Import Name TableRVA of the delay INT — same layout as the ordinary import lookup table
204Bound Delay Import TableOptional RVA of a pre-bound thunk table
244Unload Delay Import TableOptional RVA of a pristine copy of the delay IAT, for unload
284Time StampTimestamp 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.

Common mistake. "Delay-loading hides the import." It does not. The DLL name and every function name still sit in the file as plain strings in the name table, and a zero Attributes or directory entry is easy to spot — static analysis sees delay-loads as clearly as ordinary imports. The real difference is when and whether resolution happens: cost and failure move from process start to first call. Also do not confuse it with hand-written LoadLibrary/GetProcAddress code, which records no static table at all.

A Real Example

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.

Try It

Check any PE for a delay directory in one command:

$ objdump -p cli-64.exe | grep "Delay Import"
Entry d 0000000000000000 00000000 Delay Import Directory

Or 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 0x0

Swap 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.

Check Your Understanding

Without looking back: what does bit 0 of the delay descriptor's Attributes field decide?

Apply It

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.

Connect

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.