Back to course

The Data Directories

15 minintermediateModule 3: Addressing & Data DirectoriesContinues from pe-optional-header
15 min · Module 3: Addressing & Data Directories · Foundations

Why This Matters

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.

A Simple Model

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.

  1. Numbered, not named. Slot 1 is always imports, slot 5 is always base relocations, slot 12 is always the IAT. The count of valid slots is NumberOfRvaAndSizes — normally 16, but the spec warns it is not fixed, so always read the field before trusting a slot exists.
  2. All zeros means absent. An empty pair does not signal corruption. It means the image simply does not use that feature: no exports in an EXE, no TLS, no delay-loads, no .NET header.
  3. One slot lies about its first field. Slot 4 (Security/Certificate) stores a file offset, not an RVA, because certificates are never mapped into memory.

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

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.

#DirectoryPE32+ offsetWho consumes it
0Export112Loaders and GetProcAddress resolving names this DLL exposes
1Import120Loader building the import address table from DLL names
2Resource128Explorer and FindResource walking the .rsrc tree
3Exception136x64 unwinder reading RUNTIME_FUNCTION entries (.pdata)
4Security (Certificate)144Authenticode verifiers — first field is a FILE OFFSET
5Base Relocation152Loader rebasing the image when its preferred base is taken
6Debug160Debuggers locating CodeView/PDB pointers
7Architecture168Reserved, must be zero
8Global Ptr176Value for the global pointer register; size must be zero
9TLS184Loader setting up thread-local storage templates
10Load Config192Security cookie, SEH, Control Flow Guard data
11Bound Import200Legacy pre-resolved imports (rarely used now)
12IAT208Loader patching function addresses; calls jump through here
13Delay Import216Delay-load helper resolving DLLs on first call
14CLR Runtime Header224.NET runtime bootstrapping a managed image
15Reserved232Must 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."

Common mistakes. First, assuming directory number N lives in a section named for it — in Sample A the import directory (RVA 0x3A04) sits in .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.

A Real Example

All sixteen pairs from Sample A (cli-64.exe), read at file 0x188, with the section each RVA actually lands in:

#DirectoryRVA / file offsetSizeLands in
0Export0x000000000x0000absent — EXEs export nothing
1Import0x00003A040x00DC.rdata, file 0x2604
2Resource0x000070000x01E0.rsrc, file 0x3400
3Exception0x000060000x01EC.pdata, file 0x3200
4Security0x000000000x0000unsigned (OpenConsole.exe: file offset 0x100E00, size 0x3D20)
5Base Relocation0x000080000x0030.reloc, file 0x3600
6Debug0x000035100x001C.rdata, file 0x2110
7Architecture0x000000000x0000reserved, must stay zero
8Global Ptr0x000000000x0000absent
9TLS0x000000000x0000absent — no thread-local variables
10Load Config0x000033D00x0140.rdata, file 0x1FD0
11Bound Import0x000000000x0000absent
12IAT0x000030000x0250.rdata, file 0x1C00
13Delay Import0x000000000x0000absent
14CLR Runtime Header0x000000000x0000absent — native image, not .NET
15Reserved0x000000000x0000must 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.

Try It

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 Directory

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

Check Your Understanding

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?

Apply It

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.

Connect

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.