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.
Think of a PE file as two documents glued together, front to back:
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.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 Microsoft PE/COFF specification ("File Headers") describes the on-disk order exactly:
| Layer | Starts at | Size | What it says |
|---|---|---|---|
| MS-DOS header | File offset 0 | 64 bytes | e_magic = "MZ"; at offset 0x3C, e_lfanew points at the PE signature |
| MS-DOS stub | Offset 64 | Variable | Prints "This program cannot be run in DOS mode." (customizable with the linker /STUB option) |
| PE signature | File offset = e_lfanew | 4 bytes | "PE\0\0" — letters P, E, then two nulls |
| COFF file header | Signature + 4 | 20 bytes | Machine, section count, timestamp, size of optional header, characteristics |
| Optional header | COFF + 20 | 224 (PE32) or 240 (PE32+) bytes | Entry point, image base, alignments, subsystem, 16 data directories |
| Section table | After optional header | 40 bytes per section | One header per section: name, virtual size, raw pointer, flags |
| Section data | Per-section file pointers | Aligned to FileAlignment | Code 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.
e_lfanew points — 0x80, 0x100, or anywhere else the linker chose. Always read offset 0x3C first.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:
4d 5a — "MZ", the DOS magic named after Mark Zbikowski.00 01 00 00 — little-endian 32-bit value 0x00000100 = 256. The PE signature lives at file offset 256.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.
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 0x00What 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.
Without looking back: what four bytes open every PE file, and where does the PE signature itself live?
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.
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.