Introduction
PE32+ is the format of programs and libraries on 64-bit
Microsoft Windows. It came first with
the Itanium edition of Windows XP in 2001, and became common with x64 Windows in 2005.
Every 64-bit Windows since uses it, for x64 and, from Windows 10 on ARM in 2017, for
ARM64. It is also the format of 64-bit UEFI firmware images, for x64 and ARM64. The
same layout is used for programs (.exe), libraries (.dll),
drivers (.sys), control panel items (.cpl), ActiveX controls
(.ocx) and EFI applications and drivers (.efi).
PE stands for Portable Executable, and PE32+ is its 64-bit form. Like the 32-bit
form it is built on COFF: an MS-DOS program, the
signature PE\0\0, a COFF file header, an optional header, and a table of
sections that hold the code, the data and the tables Windows needs to load it.
Microsoft publishes the specification.
The 32-bit form, PE32, has the same
structures, but PE32+ changes the optional header: its magic is 0x20B,
the BaseOfData field is gone, and the image base and the stack and heap sizes are 64
bits. The import tables, the TLS directory and most of the load configuration are 64
bits wide too. Those changes to existing fields make it a separate format here. Since
2001 PE32+ has only added: new machines, new flags and new tables. All the variations
of .exe are on the EXE page.
A program dropped on any page opens in the explorer. It shows the file’s structure, every header and table in the order of its bytes; its resources, listed by type, name and language; and its code, disassembled as x64 with an instruction decoded at every byte, laid out in columns, so that a jump into the middle of another instruction can be followed. The instructions are described in the x86 instruction reference.
Structure
All numbers are little-endian. Most addresses inside the file are relative virtual addresses (RVAs): 32-bit offsets from the address the image is loaded at, even in a 64-bit image. The section table maps an RVA back to a file offset; see Section table and section data.
0x00MS-DOS headerMZ, withe_lfanewat0x3C64 bytes0x40DOS stub a small DOS program that prints “This program cannot be run in DOS mode.”varies…Rich header written by Microsoft’s linker: the tools that built the programvariese_lfanewPE signature50 45 00 004 bytes+4COFF file header machine, number of sections, time stamp, flags20 bytes+24Optional header magic0x20B, entry point, 64-bit image base, alignments, subsystem, then the data directories240 bytes…Section table one entry for each section40 bytes each…Sections each section’s raw data, holding the code, the data and the tables the data directories point atvaries…After the image the certificate table, COFF symbols and any overlayvaries
The tables
The optional header ends with 16 data directories, each an RVA and a size. They locate the tables inside the sections:
| Directory | Table | Usually in |
|---|---|---|
| 0 | Exports | .edata or .rdata |
| 1, 12 | Imports and the import address tables | .idata or .rdata |
| 2 | Resources | .rsrc |
| 3 | Exception table, which x64 and ARM64 images have for their functions | .pdata |
| 4 | Certificates (a file offset, not loaded) | after the last section |
| 5 | Base relocations | .reloc |
| 6 | Debug directory | .rdata |
| 9 | Thread-local storage | .rdata |
| 10 | Load configuration | .rdata |
| 11 | Bound imports | the headers |
| 13 | Delay-load imports | .rdata |
| 14 | CLR header (.NET) | .text |
Directories 7, 8 and 15 are reserved or unused; see Data directories.
The code
A section whose flags include CNT_CODE or MEM_EXECUTE holds
machine code. For machine 0x8664 (x64, also called AMD64) the explorer
disassembles each such section in 64-bit mode, from its raw data up to its
VirtualSize. The addresses it shows are those the code runs at: the 64-bit image base
plus the RVA, such as 0x180001000.
An instruction is decoded at every byte, not only where the one before ends, and the instructions are laid out in columns: the first column is the plain disassembly from the start of the section, and the others hold the instructions that start inside it. So when a jump lands in the middle of an instruction, the code it reaches is there to read. Every instruction, including the REX, VEX and EVEX encodings that 64-bit code uses, is described in the x86 instruction reference.
The explorer names what it can: the entry point, every export, each import address
table slot as KERNEL32.dll!GlobalAlloc, so that a RIP-relative call
through the slot shows which function it calls, the TLS callbacks, and the functions in
a COFF symbol table, which MinGW and Wine builds keep.
- ARM64 and Itanium code is not disassembled; the explorer names the machine.
- An ARM64EC image says
0x8664in its header, as an x64 image does, so its ARM64 code is disassembled as x64 too, and is nonsense there. - An assembly compiled for one 64-bit processor holds IL bytecode and metadata in
its
.textsection, not machine code; see CLR header and .NET metadata.
In the explorer
The tree shows every byte of the file once, in file order:
- the headers as one part: the DOS header, the stub, the Rich header, the PE headers and the section table;
- each section’s raw data, with the tables that lie inside it, and the bytes no table covers as “Code” in an executable section and “Data” in the others;
- what lies after the image: the certificate table, the COFF symbols and the overlay.
Tables are found through the data directories and the RVAs inside them, then placed where their bytes are, not where they are referred to. Many small items of one kind, such as hint/name entries, export names, resource data and unwind records, are grouped.
The Program tab sums the file up: its kind, machine, subsystem, entry point, image base, link time, linker version, the Windows version it needs, its flags, whether its checksum is right, the Rich header, the PDB path, the .NET runtime, the TLS callbacks, the signature, the COFF symbols and the overlay. It then lists the code, the sections, the data directories, the imports and exports, the resources, the debug directory, the tools from the Rich header, and a .NET summary.
Reference pages
- MS-DOS header, stub and Rich header – the 64-byte DOS header, the stub program and Microsoft’s undocumented list of tools
- PE signature and COFF file header – machine types, the time stamp and the characteristics flags
- Optional header – the 64-bit layout, subsystems, DLL characteristics and the checksum
- Data directories – the 16 entries that locate every table
- Section table and section data – section headers, flags, names, and mapping RVAs to file offsets
- Imports – import descriptors, 8-byte lookup entries, hint/name entries and the import address table
- Delay-load imports – DLLs loaded on the first call
- Bound imports – the table that lets prebound addresses be used as they are
- Exports – the export directory, the address, name and ordinal tables, and forwarders
- Resources – the resource tree and the resource types
- Base relocations – DIR64 fixups, and why x64 images need so few
- Debug directory – CodeView PDB paths, POGO and the other debug entries
- Thread-local storage – the 40-byte TLS directory, the template and the callbacks
- Load configuration – the security cookie, Control Flow Guard and the rest, in the 64-bit layout
- Exception table and unwind information – RUNTIME_FUNCTION, x64 UNWIND_INFO and unwind codes, and ARM64 .xdata
- CLR header and .NET metadata – the runtime header, the metadata root, streams and heaps
- Attribute certificates (Authenticode) – the signature appended to the file
- COFF symbol and string tables – symbols that GNU linkers leave in images
- Overlay – data after the image, and the installers and archives found there
Telling it apart
Every .exe format starts with an MS-DOS header. The explorer’s parser
for PE32+ checks, in order, and turns the file down at the first check that fails:
- The file is at least 64 bytes long and starts with
MZ. e_lfanew, the 32-bit value at0x3C, is at least0x40and leaves room for 24 bytes before the end of the file.- The four bytes at
e_lfaneware50 45 00 00,PE\0\0. - The optional header’s magic, the two bytes at
e_lfanew+ 24, is0x20B. A PE32 file has0x10B. - SizeOfOptionalHeader is at least 112, the size of the fixed fields.
- The section table, 40 bytes for each section after the optional header, fits in the file.
A PE32+ file whose optional header is only 224 bytes, room for 14 data directories,
is valid, and the explorer accepts it. Anything wrong after these checks, such as a
table outside every section, two tables that overlap or a wrong checksum, is reported
as a problem rather than a reason to turn the file down. Windows itself accepts an
e_lfanew below 0x40, where the PE header overlaps the DOS
header; the explorer does not lay such files out.
Within PE32+, the machine field says x64, ARM64 or Itanium, the DLL
flag (0x2000) in the COFF header marks a library, the subsystem says
whether a program is a console program, a GUI program, a driver or an EFI image, and a
CLR header (directory 14) marks a .NET assembly.
History
PE has no version field. These are the additions that matter for PE32+; each one only added to the format.
| When | What changed |
|---|---|
| 2001 | Windows XP 64-bit Edition, for Itanium: the first PE32+ files, machine 0x0200. |
| 2005 | Windows XP Professional x64 Edition and Windows Server 2003 x64: machine 0x8664, whose images rely on the exception table to unwind the stack. |
| Around 2007 | Windows Vista loads images flagged DYNAMIC_BASE at a random address (ASLR). |
| Around 2015 | Control Flow Guard: the GUARD_CF flag and the load configuration’s function tables, from Windows 8.1 Update and Visual Studio 2015. |
| 2017 | Windows 10 on ARM: machine 0xAA64, ARM64. |
| Around 2020 | CET shadow stack compatibility, marked in an EX_DLLCHARACTERISTICS debug entry. |
| 2021 | Windows 11: ARM64EC and ARM64X images, which mix ARM64 and x64 code. |
The dates are those of the Windows releases in general, not from the PE specification. The Rich header, added in the late 1990s, is written in PE32+ files by Microsoft’s linkers as in 32-bit ones.
Variants
- MS-DOS executable (MZ) – 16-bit DOS programs: an MZ header, relocations and the code
- 16-bit Windows executable (NE) – Windows 3.x and early OS/2 programs and DLLs, in movable segments
- Linear Executable (LE) – 32-bit Windows VxD drivers and DOS extender programs, in paged objects
- OS/2 executable (LX) – 32-bit OS/2 programs and DLLs, in paged objects
- Windows executable (PE32) – 32-bit Windows programs and DLLs, with a 32-bit optional header (magic 0x10B)
- Windows 64-bit executable (PE32+) – 64-bit Windows programs and DLLs (x64, ARM64), with a 64-bit optional header (magic 0x20B)
Links
Software
- Windows – 64-bit Windows runs PE32+ programs directly
- Wine – runs Windows programs on other systems (Linux, macOS)
- Ghidra – the NSA’s free reverse engineering suite (Windows, macOS, Linux)
- IDA Pro – commercial disassembler and decompiler (Windows, macOS, Linux)
- x64dbg – free debugger for 64-bit Windows programs (Windows)
- PE-bear – free viewer for PE headers and sections (Windows, Linux)
- Resource Hacker – views and edits the resources in a PE file (Windows)
- Dependencies – shows the DLLs a program imports (Windows)
- 7-Zip – free archiver that can open a PE file’s sections and resources (Windows)
Developers
- pefile – reads and edits PE files (Python)
- LIEF – reads and modifies PE, ELF and Mach-O (C++, with Python and Rust bindings)
- goblin – parses PE, ELF and Mach-O (Rust)
- LLVM – its llvm-objdump and llvm-readobj tools dump PE headers, tables, unwind information and code (C++)
- richheader – reads Rich headers, with a list of the product IDs (Python)
Reference
- PE Format – Microsoft’s specification of PE and COFF
- winnt.h – Wine’s copy of the Windows header that defines every PE structure (C)
- x64 exception handling – Microsoft’s description of RUNTIME_FUNCTION, UNWIND_INFO and the unwind codes
- ARM64 exception handling – Microsoft’s description of the .pdata and .xdata records of ARM64 Windows
- ECMA-335 – the Common Language Infrastructure standard: the CLR header and .NET metadata