The loader already knows where the entry point is, but a modern program also needs relocations so it can be rebased, an import table so it can call KERNEL32, resources for icons and version info, and an exception table so the CPU can unwind stack frames. All of those tables are scattered through the sections. Without an index, finding them would mean scanning the whole image for patterns — slow, ambiguous, and wrong whenever a table sits in the middle of unrelated data.
The data directories are that index: a fixed, always-in-the-same-place list of address-and-size pairs at the tail of the optional header. Every serious PE consumer reads them — the Windows loader, exception dispatch, debuggers, signature verifiers, dumpbin, objdump -p, Python's pefile. If you can read these 16 pairs, you know what a binary actually contains before you look at a single section.
Think of a table of contents with sixteen numbered slots. Each slot is exactly 8 bytes: a VirtualAddress (really an RVA) telling where the table lives in memory, and a Size telling how many bytes it spans.
NumberOfRvaAndSizes — normally 16, but the spec warns it is not fixed, so always read the field before trusting a slot exists.The model's missing piece: the directories locate tables; they do not tell you which section holds them. You still need the section table for that — which is the next lesson's job.
The spec declares each entry as IMAGE_DATA_DIRECTORY: a 4-byte VirtualAddress plus a 4-byte Size. For PE32+ the array begins at optional-header offset 112, immediately after NumberOfRvaAndSizes at offset 108. In Sample A the optional header starts at file 0x118, so NumberOfRvaAndSizes sits at file 0x184 (value 16), the sixteen pairs occupy 0x188 through 0x207, and the section table begins at 0x208.
| # | Directory | PE32+ offset | Who consumes it |
|---|---|---|---|
| 0 | Export | 112 | Loaders and GetProcAddress resolving names this DLL exposes |
| 1 | Import | 120 | Loader building the import address table from DLL names |
| 2 | Resource | 128 | Explorer and FindResource walking the .rsrc tree |
| 3 | Exception | 136 | x64 unwinder reading RUNTIME_FUNCTION entries (.pdata) |
| 4 | Security (Certificate) | 144 | Authenticode verifiers — first field is a FILE OFFSET |
| 5 | Base Relocation | 152 | Loader rebasing the image when its preferred base is taken |
| 6 | Debug | 160 | Debuggers locating CodeView/PDB pointers |
| 7 | Architecture | 168 | Reserved, must be zero |
| 8 | Global Ptr | 176 | Value for the global pointer register; size must be zero |
| 9 | TLS | 184 | Loader setting up thread-local storage templates |
| 10 | Load Config | 192 | Security cookie, SEH, Control Flow Guard data |
| 11 | Bound Import | 200 | Legacy pre-resolved imports (rarely used now) |
| 12 | IAT | 208 | Loader patching function addresses; calls jump through here |
| 13 | Delay Import | 216 | Delay-load helper resolving DLLs on first call |
| 14 | CLR Runtime Header | 224 | .NET runtime bootstrapping a managed image |
| 15 | Reserved | 232 | Must be zero |
Two rules the spec states plainly: check NumberOfRvaAndSizes and SizeOfOptionalHeader before probing any entry, and "do not assume that the RVAs in this table point to the beginning of a section or that the sections that contain specific tables have specific names."
.rdata, and there is no .idata section at all. Second, treating an all-zero entry as damage — nine of Sample A's sixteen entries are zero and the file runs perfectly. Third, reading Security as an RVA and converting it through the section table — the spec says its first field "is a file pointer instead," because certificates are not loaded into memory.All sixteen pairs from Sample A (cli-64.exe), read at file 0x188, with the section each RVA actually lands in:
| # | Directory | RVA / file offset | Size | Lands in |
|---|---|---|---|---|
| 0 | Export | 0x00000000 | 0x0000 | absent — EXEs export nothing |
| 1 | Import | 0x00003A04 | 0x00DC | .rdata, file 0x2604 |
| 2 | Resource | 0x00007000 | 0x01E0 | .rsrc, file 0x3400 |
| 3 | Exception | 0x00006000 | 0x01EC | .pdata, file 0x3200 |
| 4 | Security | 0x00000000 | 0x0000 | unsigned (OpenConsole.exe: file offset 0x100E00, size 0x3D20) |
| 5 | Base Relocation | 0x00008000 | 0x0030 | .reloc, file 0x3600 |
| 6 | Debug | 0x00003510 | 0x001C | .rdata, file 0x2110 |
| 7 | Architecture | 0x00000000 | 0x0000 | reserved, must stay zero |
| 8 | Global Ptr | 0x00000000 | 0x0000 | absent |
| 9 | TLS | 0x00000000 | 0x0000 | absent — no thread-local variables |
| 10 | Load Config | 0x000033D0 | 0x0140 | .rdata, file 0x1FD0 |
| 11 | Bound Import | 0x00000000 | 0x0000 | absent |
| 12 | IAT | 0x00003000 | 0x0250 | .rdata, file 0x1C00 |
| 13 | Delay Import | 0x00000000 | 0x0000 | absent |
| 14 | CLR Runtime Header | 0x00000000 | 0x0000 | absent — native image, not .NET |
| 15 | Reserved | 0x00000000 | 0x0000 | must be zero |
The raw bytes for the first four slots (entries 0 through 3), little-endian throughout:
00000188: 0000 0000 0000 0000 043a 0000 dc00 0000 .........:...... 00000198: 0070 0000 e001 0000 0060 0000 ec01 0000 .p.......`...... 000001a8: 0000 0000 0000 0000 0080 0000 3000 0000 ............0... 000001b8: 1035 0000 1c00 0000 0000 0000 0000 0000 .5..............
Slot 0 is eight zero bytes. Slot 1 reads 04 3a 00 00 = 0x3A04 then dc 00 00 00 = 0xDC — the import directory is 220 bytes long. Slot 4 (at 0x1A8) is zero here; in a signed binary that same position holds a raw file offset you seek to directly.
Dump the array yourself, or let a disassembler decode it:
$ xxd -s 0x188 -l 128 cli-64.exe
00000188: 0000 0000 0000 0000 043a 0000 dc00 0000 .........:......
...
$ objdump -p cli-64.exe | sed -n '/The Data Directory/,/^There is an import/p'
Entry 1 0000000000003a04 000000dc Import Directory [parts of .idata]
Entry 4 0000000000000000 00000000 Security DirectoryOr decode all sixteen pairs in one line of Python:
$ python3 -c "import struct; d=open('cli-64.exe','rb').read(); names='EXPORT IMPORT RESOURCE EXCEPTION SECURITY BASERELOC DEBUG ARCH GLOBALPTR TLS LOAD_CONFIG BOUND_IMPORT IAT DELAY CLR RESERVED'.split(); p=struct.unpack_from('<32I', d, 0x188); [print('%2d %-12s 0x%08x 0x%04x' % (i, names[i], p[2*i], p[2*i+1])) for i in range(16)]"
0 EXPORT 0x00000000 0x0000
1 IMPORT 0x00003a04 0x00dc
...What to look for: the pairs are strictly 8 bytes each in index order, so slot N lives at file 0x188 + 8N in this file — no searching, no heuristics.
Without looking back: what does a data-directory entry whose two dwords are both zero mean, and which slot is the exception to the RVA rule?
A verifier tool reads the Security directory of OpenConsole.exe and gets VirtualAddress = 0x100E00, Size = 0x3D20 (ImageBase = 0x140000000, .reloc sits at RVA 0x106000). What should the tool do with 0x100E00?
The pattern: every other directory entry is an RVA you convert; this one you seek. Knowing which is which is the difference between a verifier that works and one that reads garbage.
You have now met three kinds of number in one lesson: file offsets (the Security slot, and everything xxd shows you), RVAs (the other fifteen slots), and — soon — full virtual addresses once the image is loaded. The next lesson separates those three address spaces cleanly.
Next: VA, RVA, and File Offset — the three coordinate systems a PE lives in, and when each one appears.