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). When it is 0, the names can be read from the address table instead. |
| 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. |
0000BC50 A0 CB 00 00 00 00 00 00 00 00 00 00 82 D0 00 00
0000BC60 60 A0 00 00 B8 CC 00 00 00 00 00 00 00 00 00 00
0xBC50 (RVA 0xCA50): lookup table at 0xCBA0, not bound, name at 0xD082 (KERNEL32.dll), address table at 0xA060. The next descriptor starts at 0xBC64.The descriptor is the same 20 bytes as in a 32-bit file: only the lookup and address
tables it points at are wider. That file imports 123 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 8-byte entries in PE32+, one for each function, ending with a zero entry:
| Bits | When bit 63 is set | When bit 63 is clear |
|---|---|---|
| 63 | The ordinal flag: 1 imports by ordinal, 0 by name. | |
| 62–31 | 0 | 0 |
| 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, at RVA 0xCBA0 (file
0xBDA0), starts 48 D0 00 00 00 00 00 00 56 D0 00 00 00 00 00
00: two imports by name, whose hint/name entries are at RVAs
0xD048 and 0xD056. An import by ordinal such as
0x8000000000000011 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. |
0000C240 61 6E 64 6C 65 57 00 00 20 03 47 6C 6F 62 61 6C
0000C250 41 6C 6C 6F 63 00 2B 03 47 6C 6F 62 61 6C 4C 6F
0xD048 (file 0xC248): hint 0x320 (800), GlobalAlloc; then hint 0x32B (811), GlobalLockThe 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 8-byte slot. x64 code reaches the slot relative to the
instruction pointer, as in call qword ptr [rip + …], so the call needs
no base relocation; in the example the slot at 0x18000A060 holds
GlobalAlloc once it 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. Address tables that overlap one another are reported.
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.