Back to course

The Export Tables

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

Why This Matters

When conpty.dll exports CreatePseudoConsole, some other image's import descriptor will say "DLL name: conpty.dll, symbol: CreatePseudoConsole". The loader already knows the name — now it has to find the actual code inside this file and hand the address back. A string search over the whole DLL would work but would be slow and fragile, so PE packages exports as a small header plus three arrays designed for lookup.

This is also what GetProcAddress walks at runtime, what dumpbin /exports prints, and what a reverse engineer reads first when asking "what does this DLL actually offer?". Understanding it explains why ordinals break between versions, why some functions appear twice in an export list, and how one DLL can pretend to export code it does not contain.

A Simple Model

Think of the export directory as one header row and three parallel columns:

Slot i of the name table and slot i of the ordinal table are the same row: name i maps to EAT index ordinal[i], and the caller-visible ordinal is that index plus the Ordinal Base. A single header (40 bytes) stores the DLL's own name and the RVAs of all three arrays. Data directory index 0 points at it.

The model's missing piece: there are two lookup routes into the same EAT — by name (search, then translate) and by ordinal (direct index). Names are convenience; ordinals are the real addressing scheme.

The Actual Detail

The spec's export directory table, one row, 40 bytes total:

OffsetSizeFieldWhat it says
04Export FlagsReserved, must be 0
44Time/Date StampWhen the export data was created (0 or 0xFFFFFFFF means "not meaningful")
82Major VersionUser-settable
102Minor VersionUser-settable
124Name RVAASCII name of this DLL itself
164Ordinal BaseStarting ordinal of the EAT, usually 1
204Address Table EntriesLength of the EAT (NumberOfFunctions)
244Number of Name PointersLength of the name table and of the ordinal table
284Export Address Table RVAWhere the RVAs are
324Name Pointer RVAWhere the sorted name RVAs are
364Ordinal Table RVAWhere the 16-bit indexes are

The spec's lookup algorithm for a name: binary-search the name pointer table for a match at index i, read ordinal = OrdinalTable[i], take rva = ExportAddressTable[ordinal], and report ordinal + OrdinalBase to the caller. The reverse path subtracts Base first. The EAT entry itself has two meanings: normally an export RVA into code or data — but if it falls inside the export directory's own address range, it is a forwarder RVA, a pointer to a string like "MYDLL.expfunc" or "MYDLL.#27" naming a symbol in a different DLL. The spec's own example: Windows XP's Kernel32.HeapAlloc forwards to "NTDLL.RtlAllocateHeap", so applications keep importing only Kernel32.

Common mistake. Treating export ordinals as stable across releases, or assuming every function has a name. The EAT can be longer than the name table (exports with no name), several names can point at the same EAT slot (aliases), and only the name table is sorted — reordering ordinals between DLL versions silently redirects ordinal importers. That is why import-by-name is the default and ordinal import is reserved for frozen interfaces.

A Real Example

The export directory of GitHub Copilot's conpty.dll, at file offset 0x150D0 (data directory index 0: RVA 0x164D0, size 0x214):

000150d0: 0000 0000 ffff ffff 0000 0000 8465 0100  .............e..
000150e0: 0100 0000 0e00 0000 0e00 0000 f864 0100  .............d..
000150f0: 3065 0100 6865 0100 205f 0000            0e..he.. _..
  1. 0000 0000 — Export Flags, zero as required. Next dword ffffffff — a non-meaningful timestamp per the spec.
  2. 0000 0000 — Major/Minor version 0/0.
  3. 8465 0100 = 0x16584 — Name RVA; the string there is conpty.dll.
  4. 0100 0000 — Ordinal Base = 1.
  5. 0e00 0000 twice — 14 address-table entries and 14 names (every export is named here).
  6. f864 0100 / 3065 0100 / 6865 0100 — RVAs of the EAT (0x164F8), name pointer table (0x16530), ordinal table (0x16568), all packed into the 0x214-byte export directory.

The 14 names, exactly as objdump prints them (ordinal = EAT index + Base):

[   0] +base[   1]  0000 ClearPseudoConsole
[   1] +base[   2]  0001 ClosePseudoConsole
[   4] +base[   5]  0004 ConptyCreatePseudoConsole
[  11] +base[  12]  000b CreatePseudoConsole
[  13] +base[  14]  000d ResizePseudoConsole

Note the aliasing: EAT slot for ordinal 5 (ConptyCreatePseudoConsole) and slot for ordinal 12 (CreatePseudoConsole) both hold RVA 0x05BD0 — two names, one function. And no entry in this EAT falls inside the directory range 0x164D0-0x166E4, so none of the fourteen exports is a forwarder.

Try It

objdump prints the header and both name/ordinal columns:

$ objdump -x conpty.dll | sed -n '/Export Tables/,+14p'
The Export Tables (interpreted .rdata section contents)

Export Flags 			0
Time/Date stamp 		ffffffff
Major/Minor 			0/0
Name 				0000000000016584 conpty.dll
Ordinal Base 			1
Number in:
	Export Address Table 		0000000e
	[Name Pointer/Ordinal] Table	0000000e
$ objdump -x conpty.dll | sed -n '/\[Ordinal\/Name Pointer\]/,+6p'
[Ordinal/Name Pointer] Table -- Ordinal Base 1
	          Ordinal   Hint Name
	[   0] +base[   1]  0000 ClearPseudoConsole
	[   1] +base[   2]  0001 ClosePseudoConsole
	[   2] +base[   3]  0002 ConptyClearPseudoConsole
	[   3] +base[   4]  0003 ConptyClosePseudoConsole

Decode the header yourself — the directory starts at file 0x150D0 because .rdata has RVA 0x11000 at file 0xFC00 and the export RVA is 0x164D0:

$ python3 -c "import struct;d=open('conpty.dll','rb').read();v=struct.unpack_from('<IIHHIIIIIII',d,0x150d0);print('base',v[5],'funcs',v[6],'names',v[7],hex(v[8]),hex(v[9]),hex(v[10]))"
base 1 funcs 14 names 14 0x164f8 0x16530 0x16568

What to look for: Address Table Entries versus Number of Name Pointers — if they differ, some exports are ordinal-only. Then spot any EAT value that lies inside the directory's own RVA range: that is a forwarder.

Check Your Understanding

Without looking back: a loader asks conpty.dll for ordinal 12. Which export does it get, and how does it find it?

Apply It

You are auditing an EAT entry whose value is 0x16590, and the export directory covers RVAs 0x164D0 to 0x166E4. What have you found?

The pattern: read the directory size as a fence. EAT values inside the fence are strings; values outside it are code.

Connect

Both tables so far are eager: the loader resolves everything before main runs. But a DLL pulled in for one rare code path still taxes every process start — and if it is missing, the process will not start at all. The next lesson shows the structure that moves that cost and that failure to the first call instead.

Next: Delay-Load Imports — the second, parallel import machinery and the helper that runs it.