Back to course

Why the Portable Executable Format Exists

15 minbeginnerModule 1: Fundamentals
15 min · Module 1: Fundamentals · Foundations

Why This Matters

Double-click notepad.exe and Windows loads it into memory and runs it. But the file on disk is just bytes. How does Windows know that these bytes are executable, which byte is the first instruction to run, where the code ends and the data begins, and which functions it must resolve from KERNEL32.dll before your program can call anything?

The Portable Executable format answers all of those questions. It is the contract between the linker that builds a Windows program and the Windows loader that runs it. If you have ever wondered what a hex editor sees when it opens an .exe, or how malware injection, code signing, or Dumpbin work under the hood, this format is the whole story.

Without a defined layout, no program could be loaded — every compiler would invent its own file structure, and the operating system could not run any of them.

A Simple Model

Think of a PE file as two documents glued together, front to back:

  1. A 1980s MS-DOS program — a tiny stub that starts with the letters MZ. If you run a modern .exe under MS-DOS it prints "This program cannot be run in DOS mode." This keeps the file loadable by 40-year-old software.
  2. The real Windows image — starting at a pointer hidden inside the DOS header, marked by the signature PE. This part holds the COFF header, the optional header, a table of sections, and the section data itself.

The model's missing piece: those two documents do not overlap arbitrarily. A single 4-byte field at DOS offset 0x3C is the hinge — it tells you exactly where the Windows half begins. Follow that pointer and you are inside the modern format.

The Actual Detail

The Microsoft PE/COFF specification ("File Headers") describes the on-disk order exactly:

LayerStarts atSizeWhat it says
MS-DOS headerFile offset 064 bytese_magic = "MZ"; at offset 0x3C, e_lfanew points at the PE signature
MS-DOS stubOffset 64VariablePrints "This program cannot be run in DOS mode." (customizable with the linker /STUB option)
PE signatureFile offset = e_lfanew4 bytes"PE\0\0" — letters P, E, then two nulls
COFF file headerSignature + 420 bytesMachine, section count, timestamp, size of optional header, characteristics
Optional headerCOFF + 20224 (PE32) or 240 (PE32+) bytesEntry point, image base, alignments, subsystem, 16 data directories
Section tableAfter optional header40 bytes per sectionOne header per section: name, virtual size, raw pointer, flags
Section dataPer-section file pointersAligned to FileAlignmentCode and data — .text, .rdata, .data, ...

The spec also fixes the vocabulary you will use all course: a file pointer is a position on disk; a virtual address (VA) is where something lands in memory; a relative virtual address (RVA) is that memory address with the image base subtracted. Keep those three apart — most PE bugs are one of them confused for another.

Common mistake. You might think the PE signature is always at a fixed offset. It is not. It sits wherever e_lfanew points — 0x80, 0x100, or anywhere else the linker chose. Always read offset 0x3C first.

A Real Example

First 64 bytes of a real 64-bit PE executable (setuptools' cli-64.exe, 14336 bytes):

00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
00000010: b800 0000 0000 0000 4000 0000 0000 0000  ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0001 0000 0000  ................

Walk-through:

  1. Bytes 0-1: 4d 5a — "MZ", the DOS magic named after Mark Zbikowski.
  2. Bytes 0x3C-0x3F: 00 01 00 00 — little-endian 32-bit value 0x00000100 = 256. The PE signature lives at file offset 256.
  3. And indeed, at offset 0x100 the file reads 50 45 00 00 — "PE" plus two nulls, exactly as the spec requires.
00000100: 5045 0000 6486 0600 e427 6864 0000 0000  PE..d....'hd....
00000110: 0000 0000 f000 2200                       ......"....

That second dump already contains a complete COFF header: machine 0x8664 (x64), 6 sections, optional-header size 0x00F0 (240 bytes = PE32+), characteristics 0x0022. You will decode every field in the next concepts.

Try It

Any Windows executable works. On Linux or macOS:

$ file notepad.exe
notepad.exe: PE32+ executable (GUI) x86-64, for MS Windows

$ xxd -l 64 notepad.exe
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
...

On Windows, PowerShell gets you the same first bytes:

> [System.IO.File]::ReadAllBytes(".\notepad.exe")[0..3]
77 90 144 0   # 0x4D 0x5A 0x90 0x00

What to look for: every PE file begins 4d 5a, and every four-byte group you read from a file like this is little-endian — the lowest-address byte is the least significant. That single rule decodes half the format.

Check Your Understanding

Without looking back: what four bytes open every PE file, and where does the PE signature itself live?

Apply It

A PE file has 00 08 00 00 at offset 0x3C. Before opening anything else, at what file offset will you find the bytes 50 45 00 00?

The pattern: read 4 bytes at 0x3C, swap them to little-endian, and you have the next address in the chain. Every PE parse starts this way.

Connect

This course follows the same spine the ELF course did: headers first (what the loader reads), then addressing (how disk maps to memory), then sections and the tables inside them (imports, exports, relocations, resources), and finally the loader itself.

Next: PE File Layout — the full map from byte 0 to the last section, before any individual field is decoded.