Your program calls CreateFileA, but no machine code for it exists in your .exe. That function lives inside KERNEL32.dll, and the address where Windows maps that DLL is not decided until the process starts. The file on disk can only record which DLLs it needs and which functions inside them — it cannot record final addresses.
The import tables are that shopping list. The Windows loader walks them before the first instruction of your entry point runs: it maps each DLL, resolves every name, and patches the call targets. Everything that depends on linking against other modules — dumpbin /imports, Dependency Walker, objdump, malware analysis write-ups of "API hashing", even the error dialog when a DLL is missing — is reading this one structure.
For each DLL the image uses, there is a small descriptor — five 32-bit fields that answer three questions: what is the DLL called, where is the list of names I import from it, and where does the resolved addresses table live?
Those last two fields point at two parallel arrays:
The descriptor array itself is simply an array terminated by an all-zero descriptor. Data directory index 1 points at it; index 12 points at the IAT alone.
The model's missing piece: names and addresses are separate. That is the whole design — a read-only list of symbols, plus a writable table the operating system is allowed to fill in.
The Microsoft PE specification ("The .idata Section") fixes the descriptor at exactly 20 bytes:
| Offset | Size | Field | What it says |
|---|---|---|---|
| 0 | 4 | Import Lookup Table RVA (a.k.a. Characteristics in winnt.h) | RVA of the ILT — one entry per imported symbol |
| 4 | 4 | Time/Date Stamp | Zero until the image is bound; after binding, the stamp of the DLL |
| 8 | 4 | Forwarder Chain | Index of the first forwarder reference |
| 12 | 4 | Name RVA | RVA of the ASCII DLL name string |
| 16 | 4 | Import Address Table RVA (Thunk Table) | RVA of the IAT; identical to the ILT until the loader binds it |
Each ILT/IAT entry is 8 bytes in PE32+ (4 in PE32). The spec's bit layout: the top bit (bit 63) is the Ordinal/Name flag, masked as 0x8000000000000000 for PE32+ and 0x80000000 for PE32. If it is set, the low 16 bits are an ordinal and the import skips name lookup entirely. If it is clear, the entry holds a 31-bit RVA of a hint/name entry (bits 62-31 must be zero). The table ends with a zero entry.
A hint/name entry is: 2-byte Hint, then the null-terminated ASCII Name, then an optional pad byte so the next entry starts on an even boundary. The hint is an index into the exporting DLL's name pointer table — the loader tries that index first and only falls back to a full binary search if it misses. One hint/name table serves the whole import section.
0x3D30; in a running process the IAT slots hold resolved virtual addresses such as 0x00007FF812345678, and the ILT still holds the RVAs. Reading the file shows you names; reading memory shows you addresses. This is also why the IAT directory (index 12) exists separately — patching tools and hooking detectors target exactly that writable table.First import descriptor of setuptools' cli-64.exe, at file offset 0x2604 (data directory index 1 says RVA 0x3A04, size 0xDC):
00002604: e03a 0000 0000 0000 0000 0000 e23d 0000 .:...........=.. 00002614: 0030 0000 a03b 0000 .0...;..
Walk-through, five little-endian dwords plus the first dword of the next descriptor:
e03a 0000 = 0x00003AE0 — ILT RVA. File offset 0x26E0.0000 0000 twice — Time/Date Stamp and Forwarder Chain are zero (not bound).e23d 0000 = 0x00003DE2 — Name RVA, which lands on KERNEL32.dll at file 0x29E2.0030 0000 = 0x00003000 — IAT RVA, matching data directory index 12 (size 0x250).First ILT entry (file 0x26E0) is 303d 0000 0000 0000 = 0x3D30 — the top bit is clear, so it is a hint/name RVA. At file 0x2930:
00002930: d200 4372 6561 7465 4669 6c65 4100 6b02 ..CreateFileA.k. 00002940: 4765 7446 696e 616c GetFinal
Hint 0x00D2, then CreateFileA and its null terminator — no pad byte was needed because the next entry begins immediately after. The IAT at file 0x1C00 starts with the very same value 0x3D30, because nothing has been resolved yet. Ten descriptors follow (directory size 0xDC = 11 x 20 bytes: ten DLLs plus the all-zero terminator): KERNEL32.dll, VCRUNTIME140.dll, and eight api-ms-win-crt-* DLLs.
One more hint/name, packed right after the KERNEL32 string: 3e00 6d65 6d73 6574 00 at file 0x29F0 — hint 0x003E, name memset, imported from VCRUNTIME140.dll. Hint/name entries are shared across all DLLs; only the descriptor tells you which DLL a thunk belongs to.
objdump decodes the descriptor array for you (sample A, cli-64.exe):
$ objdump -x cli-64.exe | sed -n '/The Import Tables/,+16p'
The Import Tables (interpreted .rdata section contents)
vma: Hint Time Forward DLL First
Table Stamp Chain Name Thunk
00003a04 00003ae0 00000000 00000000 00003de2 00003000
DLL Name: KERNEL32.dll
vma: Ordinal Hint Member-Name Bound-To
00003000 <none> 00d2 CreateFileA
00003008 <none> 026b GetFinalPathNameByHandleA
00003010 <none> 0610 WaitForSingleObjectRead the same five fields with one line of Python:
$ python3 -c "import struct;d=open('cli-64.exe','rb').read();print([hex(x) for x in struct.unpack_from('<5I',d,0x2604)])"
['0x3ae0', '0x0', '0x0', '0x3de2', '0x3000']What to look for: a zero Time/Date Stamp means unbound; the Name RVA must resolve inside a section; and the IAT RVA should match data directory index 12.
Without looking back: what lives in a PE32+ thunk entry whose value is 0x800000000000002A?
In cli-64.exe, the first KERNEL32 thunk value on disk is 0x0000000000003D30. Which statement is true?
The pattern: decode the top bit first, then either read 16 bits as an ordinal or follow 31 bits as a hint/name RVA. Every import in every PE resolves through that one branch.
Imports only make sense if the exporting side records something to find. The next lesson flips the file around: how conpty.dll publishes fourteen names, how ordinals index them, and how one export can secretly be a string pointing at another DLL.
Next: The Export Tables — the directory header, the three parallel arrays, and forwarders.