Back to course

The Import Tables

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

Why This Matters

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.

A Simple Model

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 Actual Detail

The Microsoft PE specification ("The .idata Section") fixes the descriptor at exactly 20 bytes:

OffsetSizeFieldWhat it says
04Import Lookup Table RVA (a.k.a. Characteristics in winnt.h)RVA of the ILT — one entry per imported symbol
44Time/Date StampZero until the image is bound; after binding, the stamp of the DLL
84Forwarder ChainIndex of the first forwarder reference
124Name RVARVA of the ASCII DLL name string
164Import 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.

Common mistake. "The ILT and the IAT are the same table." They are two arrays with identical initial contents. On disk both hold RVAs like 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.
Spec aside: bound imports. Because binding is just "write addresses into the IAT", it can be pre-computed: if the Time/Date Stamp and data directory index 11 (Bound Import) are filled in, the loader can reuse those addresses without name lookups. Both sample images leave directory 11 empty, which is the common case today.

A Real Example

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:

  1. e03a 0000 = 0x00003AE0 — ILT RVA. File offset 0x26E0.
  2. 0000 0000 twice — Time/Date Stamp and Forwarder Chain are zero (not bound).
  3. e23d 0000 = 0x00003DE2 — Name RVA, which lands on KERNEL32.dll at file 0x29E2.
  4. 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.

Try It

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  WaitForSingleObject

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

Check Your Understanding

Without looking back: what lives in a PE32+ thunk entry whose value is 0x800000000000002A?

Apply It

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.

Connect

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.