A program calls into DLLs, such as KERNEL32.dll and
USER32.dll, through the import tables. Data directory 1 points at a list of
import descriptors, one for each DLL. Each descriptor points at two parallel arrays: the
import lookup table, which names the functions wanted, and the import address table
(IAT), where the loader writes each function’s address. The code calls through the IAT.
Data directory 12 covers all the import address tables together. All the addresses here
are RVAs.
Import descriptors
The import directory table is an array of IMAGE_IMPORT_DESCRIPTOR, 20 bytes each, ending with one that is all zero:
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | OriginalFirstThunk | RVA of the import lookup table (named Characteristics in winnt.h). 0 in some old Borland files, which have only the address table. |
| 4 | 4 | u32 | TimeDateStamp | 0 until bound; see Binding. |
| 8 | 4 | u32 | ForwarderChain | For old-style binding; otherwise 0 or −1. |
| 12 | 4 | u32 | Name | RVA of the DLL’s name, in ASCII, ending in a zero byte. |
| 16 | 4 | u32 | FirstThunk | RVA of the DLL’s import address table. |
0000A270 90 AF 00 00 00 00 00 00 00 00 00 00 82 B2 00 00
0000A280 30 90 00 00 10 B0 00 00 00 00 00 00 00 00 00 00
0xA270 (RVA 0xAE70): lookup table at 0xAF90, not bound, name at 0xB282 (KERNEL32.dll), address table at 0x9030. The next descriptor starts at 0xA284.That file imports 121 functions from eleven DLLs: KERNEL32.dll,
USER32.dll, GDI32.dll, SHELL32.dll,
VCRUNTIME140.dll and six of the api-ms-win-crt-… DLLs of the
Universal C Runtime.
Import lookup table
An array of 4-byte entries in PE32, one for each function, ending with a zero entry:
| Bits | When bit 31 is set | When bit 31 is clear |
|---|---|---|
| 31 | The ordinal flag: 1 imports by ordinal, 0 by name. | |
| 30–16 | 0 | The RVA of a hint/name entry (bits 0 to 30). |
| 15–0 | The ordinal number | |
KERNEL32’s lookup table in the example starts 48 B2 00 00 56 B2 00 00 64 B2 00
00: three imports by name, whose hint/name entries are at RVAs
0xB248, 0xB256 and 0xB264. An import by ordinal
such as 0x80000011 asks for ordinal 17 and carries no name.
Hint/name entries
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 2 | u16 | Hint | An index into the DLL’s export name pointer table, tried first; if the name there does not match, the loader searches. |
| 2 | varies | char[] | Name | The function’s name in ASCII, ending in a zero byte. |
| – | 0 or 1 | u8 | Pad | A zero byte, when needed, so that the next entry starts at an even offset. |
0000A640 61 6E 64 6C 65 57 00 00 17 03 47 6C 6F 62 61 6C
0000A650 41 6C 6C 6F 63 00 22 03 47 6C 6F 62 61 6C 4C 6F
0000A660 63 6B 00 00 29 03 47 6C 6F 62 61 6C 55 6E 6C 6F
0xB248 (file 0xA648): hint 0x317 (791), GlobalAlloc; hint 0x322 (802), GlobalLock and a pad byte; hint 0x329 (809), GlobalUnlockThe explorer groups the hint/name entries of a file, with the DLL names among them, into one “Hint/name table”. Some packers make entries share bytes, with a hint overlapping the end of the name before it; the explorer shows the first entry there and names the others only in the import tables.
Import address table
The import address table is laid out exactly like the lookup table, and in the file
it is usually a copy of it. When the loader loads the DLL, it overwrites each entry
with the address of the function it names. Code calls an imported function
indirectly, through its slot, as in call dword ptr [0x10009030], which
calls GlobalAlloc in the example once the slot is filled. Data directory
12 covers all the address tables together, so that the loader can make them writable
while it fills them and read-only after.
The explorer names every slot in the disassembly, as
KERNEL32.dll!GlobalAlloc, so that each indirect call shows the function
it reaches; see The code. When a
descriptor’s OriginalFirstThunk is 0, it reads the names from the address table
instead. In one family of old FFmpeg builds examined for this site, two descriptors
for the same DLL had overlapping address tables at unaligned addresses; the explorer
reports such overlaps.
Binding
Binding writes the functions’ addresses into the import address table in advance, for a particular version of each DLL, so that the loader can skip looking them up when that version is present. A bound descriptor’s TimeDateStamp says which kind of binding was done:
| TimeDateStamp | Meaning |
|---|---|
| 0 | Not bound. The address table holds the same entries as the lookup table. |
0xFFFFFFFF (−1) | Bound the new way: the real time stamps are in the bound import table. |
| anything else | Bound the old way: the time stamp of the DLL it was bound to. ForwarderChain is then the index of the first entry that is a forwarder, which the loader must still resolve, or −1 when there is none. |
If the DLL found at load time has a different time stamp, or is loaded at a different address, the loader ignores the prebound addresses and resolves every import itself. The lookup table is what lets it do so, which is why a bound file needs both arrays.