Portable Executable
The Portable Executable (PE) format is a file format developed by Microsoft for executable files, dynamic-link libraries (DLLs), object code, and other loadable modules used in Windows operating systems.[1] It specifies the structure of these files to enable the Windows loader to map them into memory, resolve imports, and execute code efficiently across supported hardware architectures.[1] The format emphasizes portability, allowing executables to run on various Windows platforms without modification, provided the architecture matches.[2] Introduced with the original Win32 API specifications in the early 1990s, the PE format extends the Common Object File Format (COFF), which originated from earlier systems like VAX/VMS, to support the demands of 32-bit Windows environments such as Windows NT and Windows 95.[2] Over time, it has evolved to include 64-bit variants (PE32+), with widened fields for larger address spaces, and adaptations for mobile platforms like Windows CE, as well as integration with .NET assemblies that embed metadata within PE structures.[2] Today, it supports multiple processor architectures, including x86, x64, ARM, ARM64, and RISC-V, ensuring broad compatibility in modern Windows ecosystems.[1] At its core, a PE file structure begins with an MS-DOS 2.0-compatible stub header for legacy compatibility, allowing older DOS loaders to recognize and potentially execute a simple message if run in a DOS environment.[1] This is followed by the PE signature ("PE\0\0"), the COFF file header (detailing machine type, number of sections, and timestamps), and an optional header that provides essential loader directives like the preferred image base address, entry point, and data directories for features such as imports, exports, resources, and exceptions.[1] The file then includes section headers—up to 96 per file—that describe the virtual size, raw size, characteristics (e.g., readable, writable, executable), and relative virtual addresses (RVAs) for sections like .text (code), .data (initialized data), .rdata (read-only data), .rsrc (resources), and specialized ones such as .tls (thread-local storage).[1] These components collectively facilitate dynamic linking, relocation, and runtime behaviors critical to Windows applications.[1]Introduction
Definition and Purpose
The Portable Executable (PE) format is the standard file format for native code executables, dynamic-link libraries (DLLs), object files, and device drivers in 32-bit and 64-bit Microsoft Windows operating systems.[1] It supports file extensions including .exe for applications, .dll for shared libraries, .obj for unlinked object code, and .sys for kernel-mode drivers, providing a consistent structure that accommodates both standalone programs and modular components.[1] This format is based on the Common Object File Format (COFF) and is designed to be architecture-independent, allowing compilation and execution across diverse hardware environments without requiring format modifications.[1] The core purpose of the PE format is to supply a structured layout that the Windows loader uses to map the file's sections into virtual memory, resolve external dependencies like imported functions from DLLs, apply address relocations for position-independent code, and facilitate program execution while handling embedded resources such as icons, menus, and strings.[1] By encapsulating code, data, and metadata in a predictable manner, it enables efficient loading, linking, and runtime management, reducing overhead during process initialization and supporting features like delayed loading of imports to optimize startup performance.[1] Notable characteristics of the PE format include its multi-architecture compatibility with processors such as x86, x64, and ARM, which is indicated by machine type fields in the COFF header to guide loader behavior.[1] For backward compatibility, every PE file begins with a minimal MS-DOS stub program, marked by the MZ magic number (hexadecimal 4D 5A), which allows older DOS systems to display a basic error message if the file is executed in that environment.[1] The format's true PE structure follows at an offset specified in the DOS header, starting with the PE signature (hexadecimal 50 45 00 00, or "PE\0\0" in ASCII), and it offers extensibility through optional headers and section tables for adding custom metadata like debug symbols or security attributes without disrupting core functionality.[1]Supported Architectures and Variants
The Portable Executable (PE) format supports several primary hardware architectures, enabling executables to run on diverse processor types within the Windows ecosystem. The core format is designed for portability across these architectures, with the machine type field in the COFF header specifying the target processor. For 32-bit Intel Architecture (IA-32), the PE32 variant is used, supporting the x86 instruction set with a 32-bit address space. This remains the standard for legacy 32-bit Windows applications. For 64-bit extensions of the x86 architecture (x86-64 or AMD64), the PE32+ variant extends the format to accommodate a 64-bit address space while maintaining backward compatibility with PE32 structures. ARM architectures are also supported, including 32-bit ARM (Thumb) via the PE32 format for embedded and legacy scenarios, and 64-bit ARM (AArch64) through PE32+ for modern Windows on ARM devices, which emphasize native 64-bit execution with emulation layers for x86 code.[3] Several variants of the PE format exist to address specific use cases and extensions. The original PE32 format targets 32-bit systems, featuring a 32-bit ImageBase and other fields suited to smaller address spaces. PE32+ serves as the 64-bit counterpart, widening key fields such as ImageBase to 64 bits, SizeOfImage to support larger executables up to 16 exabytes in theory, and adjusting stack and heap commit/reserve sizes to 64-bit values, while removing the BaseOfData field as unnecessary in flat address models. For embedded systems, Windows CE employs a compact PE variant optimized for resource-constrained devices, with adjusted default ImageBase values (e.g., 0x00010000 for executables) and support for ARM processors, though it shares the core PE structure. More recently, the Arm64X variant was introduced in the Windows 11 SDK to facilitate hybrid execution on ARM64 platforms, allowing a single binary to contain both native ARM64 code and emulated x64 (AMD64) segments for improved compatibility and performance in mixed workloads.[3] As of 2025, the PE format has seen incremental enhancements rather than fundamental overhauls. In builds using Windows SDK version 10.0.26100.0 (initially released May 2024), the Universal CRT introduces a new optional section named .fptable, which contains an array of function pointers serving as a compatibility cache for Windows API function addresses, populated dynamically to adapt to different Windows versions without hard link failures.[4] This addition supports advanced optimization techniques without altering the core PE layout. Broader compiler improvements in this SDK version include refined support for security-related flags, such as enhanced control flow guard (CFG) and address space layout randomization (ASLR) integrations, though the base PE specification remains stable. The PE format also ensures compatibility with firmware environments through a subset of its PE/COFF structure. UEFI firmware implementations utilize this subset for bootloaders and drivers, employing PE32+ images with modifications like an altered header signature (TE for Terse Executable in some cases) to minimize overhead while retaining essential loading mechanisms.[1]History
Origins in COFF
The Common Object File Format (COFF) originated in the early 1980s as a portable object file format developed by AT&T for Unix System V implementations, enabling cross-architecture compatibility for compiled code and libraries.[5] Microsoft adopted and extended COFF in the late 1980s for its own systems, drawing on the format's established structure to support advanced features in emerging operating environments.[2] A key milestone came in 1989, when Microsoft specified COFF as the foundational format for object files in the NT OS/2 project, an internal initiative to create a portable 32-bit operating system.[6] This adaptation built on COFF's Unix roots but incorporated Microsoft-specific extensions for enhanced tool compatibility, such as integration with existing linkers and debuggers.[2] The NT OS/2 specification, drafted in November 1989, outlined COFF's role in handling relocations, symbols, and sections to facilitate efficient compilation and linking processes.[6] The Portable Executable (PE) format emerged directly from this COFF foundation, with Microsoft introducing it in 1993 as the standard for 32-bit executables in Windows NT 3.1.[2] PE extended COFF by adding an optional header and image-specific structures, replacing earlier formats such as the New Executable (NE) for 16-bit Windows and the Linear Executable (LE) for 32-bit OS/2 to enable robust 32-bit support on Intel x86 architectures.[2] Initial design goals for PE emphasized portability across multiple CPU types and Windows variants, allowing executables to run without architecture-specific recompilation.[1] It also prioritized support for dynamic linking, which streamlined loading of shared libraries and reduced executable sizes through import tables.[2] Additionally, PE maintained backward compatibility with COFF-based tools, ensuring seamless integration with Microsoft compilers and linkers developed for prior systems.[2]Evolution and Adoption
The Portable Executable (PE) format debuted with the release of Windows NT 3.1 in 1993, establishing it as the foundational structure for 32-bit executables on the NT kernel and marking a shift from earlier formats like the New Executable (NE).[1] By 1995, PE gained traction in consumer Windows through the Win32s subsystem, which enabled 32-bit application support on Windows 3.1x, and was fully integrated into Windows 95 for native 32-bit programs.[2] This adoption accelerated with Windows 98 in 1998, where PE supplanted the NE format for the majority of executables, unifying the file structure across Windows platforms.[1] In the 2000s, PE evolved to accommodate emerging architectures and paradigms. The PE32+ variant, supporting 64-bit addressing, was introduced alongside Windows XP Professional x64 Edition in 2005, extending the format's longevity for AMD64 systems while maintaining backward compatibility with PE32 tools.[1] Concurrently, the release of the .NET Framework 1.0 in 2002 integrated PE as the container for managed code assemblies, appending CLR metadata to the standard headers to enable just-in-time compilation without altering the core executable layout.[2] The 2010s and 2020s brought further enhancements for diverse hardware and boot environments. ARM support arrived with Windows RT in 2012, allowing PE binaries to target ARM processors via the IMAGE_FILE_MACHINE_ARM machine type, initially for mobile devices.[1] UEFI integration began with Windows Vista in 2007, leveraging PE/COFF for bootloaders and applications under the EFI subsystem.[1] By 2025, updates in the Windows 11 SDK introduced Arm64X, a hybrid PE variant (IMAGE_FILE_MACHINE_ARM64X) embedding both native ARM64 and ARM64EC code for seamless x64 emulation on ARM devices.[7] In 2025, machine types for RISC-V (32-bit, 64-bit, and 128-bit) were added to the PE format, expanding support to this open-source instruction set architecture.[1] PE has become the de facto standard for all Windows executables, dynamic-link libraries (DLLs), and object files, powering the vast majority of software in the ecosystem as of 2025, with development tools like Visual Studio mandating compliance for native and managed builds.[1] Backward compatibility challenges, such as running 32-bit PE binaries on 64-bit systems, have been addressed through emulation layers like WoW64, which transparently translates calls and maintains execution fidelity across architectures.[1]File Format Structure
High-Level Layout
The Portable Executable (PE) file format employs a linear, sequential organization that enables the Windows loader to parse and map the file into memory systematically. It commences with a DOS header and stub for compatibility with older systems, immediately followed by the PE signature, COFF file header, optional header, section table, and the raw data sections themselves. This arrangement positions all metadata before the actual code and data, streamlining the loading process from disk to virtual memory.[1] A key distinction in the PE layout lies between file offsets and virtual addresses: on disk, components are stored at specific byte offsets within the file, with sections often padded to maintain alignment (typically 512 bytes), whereas in memory, they are relocated to virtual addresses aligned to page boundaries (commonly 4 KB) relative to the image base address. This separation accommodates efficient file storage without compromising runtime performance or security isolation. The DOS stub, in particular, ensures basic compatibility by executing a minimal program if the file is run under MS-DOS, though its primary role is to point to the PE header.[1] PE files exhibit a wide range of sizes depending on complexity, from minimal executables around 1 KB to several megabytes for comprehensive applications, with the initial headers and section table generally confined to the first kilobyte or less due to their fixed and variable-length structures. During loading, the operating system maps sections into contiguous virtual memory regions starting from the preferred base, adjusting alignments as needed; the entry point in the optional header then directs execution flow, initiating the program's runtime behavior. This high-level flow—from disk parsing to memory execution—relies on the padded, aligned layout to minimize fragmentation and support dynamic linking.[1]DOS Header and Stub
The Portable Executable (PE) file format incorporates a DOS header and stub at its beginning to maintain backward compatibility with MS-DOS environments, allowing the file to be recognized and handled by legacy 16-bit systems.[1] This design originated from the need to support mixed-use scenarios in early Windows deployments, where executables might be loaded on systems without full Windows support.[2] The DOS header is a fixed 64-byte structure defined asIMAGE_DOS_HEADER, which mimics the MS-DOS 2.0 executable header format.[1] Key fields include e_magic, a 2-byte value set to 0x5A4D (representing "MZ" for Mark Zbikowski, an early Microsoft developer), confirming the file as a valid DOS executable.[2] Another critical field is e_lfanew, a 4-byte integer at offset 0x3C, which stores the absolute file offset to the subsequent PE signature ("PE\0\0").[1] The remaining fields, such as e_cblp (bytes on last page) and e_cp (number of pages), are largely vestigial but preserve the original MS-DOS layout for compatibility with 16-bit tools like older linkers and debuggers.[1]
Immediately following the DOS header is the DOS stub, a compact 16-bit MS-DOS executable program, typically 64 bytes in length for the default stub, that functions as a valid MS-DOS executable which, when loaded by a DOS environment, executes its code after relocation by the DOS loader.[1] When loaded by a DOS interpreter, the stub prints an error message such as "This program cannot be run in DOS mode." and terminates gracefully, preventing crashes on incompatible systems.[1] The stub can be customized by developers—for instance, to include branding or additional logic—using the Microsoft linker option /STUB to specify an alternative executable file.[1] After the stub, padding bytes (often zeros) precede the PE header, ensuring the overall DOS-compatible prefix is typically 128 bytes in standard compiler outputs for streamlined file handling.[1] This legacy component persists in modern PE files primarily to support interoperability with historical tools and environments, though Windows loaders ignore it entirely during execution.[2]
Core Headers
COFF File Header
The COFF File Header is a mandatory 20-byte structure in Portable Executable (PE) files, located immediately after the PE signature ("PE\0\0", or bytes 50 45 00 00 in hexadecimal) at the file offset specified by thee_lfanew field in the preceding MS-DOS header.[1] This header provides fundamental metadata about the executable, enabling the Windows loader to identify the target architecture, file attributes, and basic layout before proceeding to subsequent structures.[1] Unlike object files, where the COFF header appears at the file's start and the optional header is absent, PE image files require both for runtime execution.[1]
The header consists of seven fields, each with a fixed offset and size, as detailed in the following table. These fields are defined in the Windows SDK header winnt.h and adhere to little-endian byte order.[8]
| Offset (bytes) | Size (bytes) | Field | Type | Description |
|---|---|---|---|---|
| 0 | 2 | Machine | WORD (unsigned short) | Specifies the target processor architecture; common values include 0x014C for Intel 386 or later (x86) and 0x8664 for AMD64 (x64).[1] |
| 2 | 2 | NumberOfSections | WORD (unsigned short) | Indicates the number of sections in the file, which determines the size of the following section header table; typical PE files have 5 to 10 sections.[1] |
| 4 | 4 | TimeDateStamp | DWORD (unsigned long) | A Unix timestamp (seconds since January 1, 1970, 00:00 UTC) recording when the file was created or linked, often used for versioning or debugging.[1] |
| 8 | 4 | PointerToSymbolTable | DWORD (unsigned long) | File offset to the COFF symbol table for debugging symbols; set to 0 in most PE images, as this feature is deprecated for executables.[1] |
| 12 | 4 | NumberOfSymbols | DWORD (unsigned long) | Count of entries in the symbol table; typically 0 for PE images, relevant only for object files.[1] |
| 16 | 2 | SizeOfOptionalHeader | WORD (unsigned short) | Length of the following optional header in bytes; 224 for PE32 (32-bit) and 240 for PE32+ (64-bit) formats.[1] |
| 18 | 2 | Characteristics | WORD (unsigned short) | A bitmask of flags defining file properties, such as IMAGE_FILE_EXECUTABLE_IMAGE (0x0002) for runnable images, IMAGE_FILE_DLL (0x2000) for dynamic-link libraries, IMAGE_FILE_RELOCS_STRIPPED (0x0001) indicating no base relocations (must load at preferred base address), and IMAGE_FILE_LARGE_ADDRESS_AWARE (0x0020) for support of addresses larger than 2 GB.[1] |
Optional Header
The Optional Header in the Portable Executable (PE) format immediately follows the COFF file header and provides essential information to the Windows loader for mapping the executable into memory and initiating execution.[1] Despite its name, this header is mandatory for PE image files (such as executables and DLLs) and is absent in object files; it is termed "optional" only in the context of the broader COFF specification, where it enhances functionality for executable images.[1] The header's size is specified in the COFF header's SizeOfOptionalHeader field and differs between 32-bit (PE32) and 64-bit (PE32+) variants: PE32 totals 224 bytes, while PE32+ extends to 240 bytes due to widened address fields accommodating larger memory spaces.[1] The standard fields, comprising the first 28 bytes in PE32 and 24 bytes in PE32+, establish the basic image characteristics and are common to both variants.[1] The Magic field (2 bytes) identifies the header type: 0x10B for PE32 and 0x20B for PE32+, signaling the loader to use 32-bit or 64-bit addressing accordingly.[1] Following this are the MajorLinkerVersion and MinorLinkerVersion (1 byte each), which record the linker tool's version used to build the image.[1] SizeOfCode (4 bytes) indicates the total size of executable code sections in bytes, while SizeOfInitializedData and SizeOfUninitializedData (4 bytes each) specify the aggregate sizes of initialized and zero-initialized data sections, respectively.[1] The AddressOfEntryPoint field (4 bytes) holds the relative virtual address (RVA) of the image's entry point, where execution begins after loading.[1] BaseOfCode (4 bytes) provides the RVA of the first code section, and in PE32 only, BaseOfData (4 bytes) marks the RVA of the first data section (omitted in PE32+ to streamline the structure).[1] Windows-specific fields, extending the header with platform-dependent details, occupy the subsequent 68 bytes in PE32 and 88 bytes in PE32+, focusing on memory layout and runtime behavior.[1] ImageBase (4 bytes in PE32, 8 bytes in PE32+) defines the preferred virtual base address for loading the image, with a default of 0x00400000 for most PE32 executables and 0x0000000140000000 for PE32+ to align with 64-bit conventions.[1] SectionAlignment (4 bytes) sets the alignment granularity for sections in virtual memory, typically 0x1000 (one page), ensuring efficient memory mapping.[1] FileAlignment (4 bytes) dictates raw data alignment on disk, defaulting to 0x200 (512 bytes) as a power of 2 between 512 and 64 KB for optimal file I/O.[1] MajorOperatingSystemVersion and MinorOperatingSystemVersion (2 bytes each), along with MajorImageVersion, MinorImageVersion, MajorSubsystemVersion, and MinorSubsystemVersion (2 bytes each), encode version information for compatibility, such as 6.02 for Windows 10 and later.[1] SizeOfImage (4 bytes) represents the total virtual size of the loaded image, a multiple of SectionAlignment, while SizeOfHeaders (4 bytes) covers the combined size of all headers plus section headers, aligned to FileAlignment.[1] Stack and heap management fields—SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, and SizeOfHeapCommit (4/8 bytes each, varying by variant)—reserve and commit initial memory for the thread stack and default heap.[1] The Subsystem field (2 bytes) specifies the runtime environment, with values like 2 for Windows GUI applications and 3 for console applications, influencing how the image interacts with the operating system.[1] DLLCharacteristics (2 bytes) is a bitfield of flags controlling DLL-specific behaviors, such as DYNAMIC_BASE (0x0040) enabling Address Space Layout Randomization (ASLR) for security.[1] Finally, NumberOfRvaAndSizes (4 bytes) indicates the count of data directory entries (typically 16), which follow the header and point to key tables like imports and exports.[1] Overall, the Optional Header directs the loader in establishing the virtual address space, aligning sections, and configuring subsystems, ensuring seamless execution without delving into the contents of referenced data structures.[1]Sections and Tables
Section Headers and Table
The section table in the Portable Executable (PE) format consists of an array of IMAGE_SECTION_HEADER structures that immediately follows the optional header, with the number of entries specified by the NumberOfSections field in the COFF file header.[1] Each entry is 40 bytes in length for both PE32 and PE32+ formats, defining the properties and locations of individual sections within the file.[1] This table enables the Windows loader to map sections into memory with appropriate protections and alignments, ensuring efficient execution and data access.[1] The IMAGE_SECTION_HEADER structure includes several key fields that describe each section's identity, size, positioning, and attributes. The Name field is an 8-byte ASCII string (null-padded if necessary), such as ".text" for code sections.[1] VirtualSize specifies the size of the section in memory as a relative virtual address (RVA), while VirtualAddress indicates the starting RVA of the section in the image's virtual address space.[1] SizeOfRawData represents the size of the section's initialized data on disk, and PointerToRawData provides the file offset to that data.[1] Additional fields include PointerToRelocations and PointerToLinenumbers (file offsets to relocation and line-number entries, respectively, typically zero for images), along with NumberOfRelocations and NumberOfLinenumbers (counts of those entries).[1] The Characteristics field is a 32-bit flags value that defines section attributes, such as IMAGE_SCN_CNT_CODE (0x00000020) for executable code, IMAGE_SCN_MEM_EXECUTE (0x20000000) for executable memory, IMAGE_SCN_MEM_READ (0x40000000) for readable memory, and IMAGE_SCN_MEM_WRITE (0x80000000) for writable memory.[1]| Field | Size (bytes) | Description |
|---|---|---|
| Name | 8 | Null-padded ASCII section name (e.g., .text). |
| VirtualSize | 4 | Size of the section in virtual memory (RVA-based). |
| VirtualAddress | 4 | RVA of the first byte of the section in memory. |
| SizeOfRawData | 4 | Size of the initialized data on disk. |
| PointerToRawData | 4 | File offset to the raw section data. |
| PointerToRelocations | 4 | File offset to relocation entries (0 for images). |
| PointerToLinenumbers | 4 | File offset to line-number entries (0 for images). |
| NumberOfRelocations | 2 | Number of relocation entries. |
| NumberOfLinenumbers | 2 | Number of line-number entries. |
| Characteristics | 4 | Flags for section attributes (e.g., code, execute, read, write). |
Common Section Types
The Portable Executable (PE) format organizes a file's content into sections, each serving a distinct purpose and protected by attributes that dictate memory access permissions when loaded. These attributes are bit flags defined in the section header, such asIMAGE_SCN_MEM_EXECUTE for executable regions, IMAGE_SCN_MEM_READ for readable areas, and IMAGE_SCN_MEM_WRITE for writable ones; combinations of these flags map to Windows memory protection constants like PAGE_EXECUTE_READ (executable and readable but not writable).[1] Sections are optional and can vary by compiler or build tool, but several are conventional in PE executables and DLLs.
Common sections include those for code, data, and supporting structures. The .text section holds the program's machine code instructions and typically contains the entry point where execution begins; it is marked executable and readable to allow the loader to map it into memory for running.[1] The .data section stores initialized global and static variables, such as those with non-zero starting values, and is configured as readable and writable to support runtime modifications.[1] In contrast, the .rdata section contains read-only initialized data, like constant strings or lookup tables, ensuring immutability with readable-only attributes.[1]
For uninitialized data, the .bss section reserves space for variables that start as zero-filled; unlike other sections, it occupies no space in the file itself and exists only in virtual memory when loaded, with readable and writable protections.[1] Import-related data resides in the .idata section, which includes thunks and lookup tables for resolving external function calls; it is generally readable, though some implementations add write permissions for loader adjustments.[1] Similarly, the .edata section manages export information for DLLs, listing available functions and their addresses, protected as readable.[1]
Resources such as icons, dialog templates, and string tables are bundled in the .rsrc section, which is readable and often compressed or aligned for efficient access during program initialization.[1] Relocation fixes are stored in the .reloc section, enabling the image to load at different base addresses; it is readable and marked discardable, meaning it can be freed after initial processing.[1] Optional sections like .pdata provide exception handling data, such as unwind information for stack traces, while .debug holds symbolic debug metadata; both are readable and typically discardable in release builds.[1]
Developers can define custom sections for specialized needs, such as .tls for thread-local storage variables that each thread initializes separately (readable and writable) or .gfids$xx for guard function IDs used in control flow guard mechanisms to prevent indirect call hijacking.[1] These custom sections inherit standard attribute flags but may include additional compiler-specific markings.
| Section Name | Purpose | Key Attributes (Flags) |
|---|---|---|
| .text | Executable machine code and entry point | IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ |
| .data | Initialized writable data (e.g., global variables) | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE |
| .rdata | Read-only initialized data (e.g., constants) | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ |
| .bss | Uninitialized data (zero-filled at load, virtual only) | IMAGE_SCN_CNT_UNINITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE |
| .idata | Import thunks and lookup tables | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ (write optional) |
| .edata | Export directory and names | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ |
| .rsrc | Embedded resources (e.g., icons, menus) | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ |
| .reloc | Base relocation blocks | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_DISCARDABLE |
| .pdata | Exception handling (e.g., unwind info) | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ |
| .debug | Debug symbols and line numbers | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_DISCARDABLE |
| .tls | Thread-local storage template | IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE |
| .gfids | Control flow guard function IDs | Custom; typically IMAGE_SCN_MEM_READ |
Data Directories
Import Table
The import table in the Portable Executable (PE) format specifies the external functions and data that an executable or DLL imports from other DLLs, enabling the Windows loader to resolve dependencies at load time.[1] It is located in the .idata section and referenced by the Import Table entry (directory index 1) in the optional header's data directories array, which provides the relative virtual address (RVA) and size of the table.[9] This setup allows the loader to efficiently map imports without scanning the entire file. The core structure is an array of IMAGE_IMPORT_DESCRIPTOR structures, each 20 bytes in size, describing imports from a single DLL and terminated by a null entry (all fields zero).[10] The fields are as follows:| Offset | Size (bytes) | Field | Description |
|---|---|---|---|
| 0 | 4 | OriginalFirstThunk | RVA to the Import Lookup Table (ILT, also known as the Hint/Name Table or INT); zero if unused. |
| 4 | 4 | TimeDateStamp | Set to zero if unbound; otherwise, holds the timestamp from binding for cached resolution. |
| 8 | 4 | ForwarderChain | Index of the first forwarder reference or -1 if none; used for chained imports across DLLs. |
| 12 | 4 | Name | RVA to the null-terminated ASCII string of the DLL name (e.g., "kernel32.dll"). |
| 16 | 4 | FirstThunk | RVA to the Import Address Table (IAT). |
Export Table
The export table in the Portable Executable (PE) format defines the functions and data that a dynamic-link library (DLL) or executable (EXE) exposes for use by other modules, facilitating dynamic linking and symbol resolution.[1] Primarily utilized in DLLs, it allows importing modules to access exported symbols either by name for readability or by ordinal for efficiency, with ordinals providing a compact indexing mechanism that avoids string comparisons during runtime loading.[1] EXEs rarely export symbols, as their primary role is execution rather than serving as shared libraries.[1] The export table resides in the.edata section of the PE file and is referenced by the first entry (index 0) in the Optional Header's Data Directory array, which provides the relative virtual address (RVA) and size of the table.[1] This directory entry points to the core structure, known as the IMAGE_EXPORT_DIRECTORY, a fixed 40-byte header that organizes the associated tables.[1] The structure begins with a Characteristics field (DWORD, reserved and must be set to 0), followed by a TimeDateStamp (DWORD) recording the module's build timestamp in seconds since January 1, 1970, which aids in versioning and dependency checks.[1] Subsequent fields include MajorVersion and MinorVersion (both WORD), indicating the export table's version; Name (DWORD), an RVA to an ASCII string of the module's name; and Base (DWORD), the starting ordinal value (typically 1) used as an offset for indexing.[1] The header concludes with NumberOfFunctions (DWORD), specifying the count of entries in the Export Address Table; NumberOfNames (DWORD), the count of named exports; and three RVAs: AddressOfFunctions (DWORD) to the Export Address Table, AddressOfNames (DWORD) to the Export Name Table, and AddressOfNameOrdinals (DWORD) to the Ordinal Table.[1]
| Field | Offset (bytes) | Type | Description |
|---|---|---|---|
| Characteristics | 0 | DWORD | Reserved; must be 0. |
| TimeDateStamp | 4 | DWORD | Timestamp of the image. |
| MajorVersion | 8 | WORD | Major version number. |
| MinorVersion | 10 | WORD | Minor version number. |
| Name | 12 | DWORD | RVA of ASCII string containing the module name. |
| Base | 16 | DWORD | Starting ordinal number (base index for exports). |
| NumberOfFunctions | 20 | DWORD | Number of entries in the Export Address Table. |
| NumberOfNames | 24 | DWORD | Number of entries in the Export Name Table. |
| AddressOfFunctions | 28 | DWORD | RVA of the Export Address Table. |
| AddressOfNames | 32 | DWORD | RVA of the Export Name Table. |
| AddressOfNameOrdinals | 36 | DWORD | RVA of the Ordinal Table. |
.edata section, it indicates a forwarder string RVA pointing to another DLL's export.[1] The Export Name Table (ENT) consists of an array of DWORD RVAs, each pointing to a null-terminated ASCII string representing an exported symbol's name, sorted alphabetically to enable binary search for efficient lookup.[1] Complementing these, the Ordinal Table is an array of WORD values that map each named export to its corresponding index in the EAT, offset by the Base value; this allows ordinal-based access without name resolution, reducing load-time overhead.[1] For example, if Base is 1 and an ordinal table entry is 5, it indexes the 5th function in the EAT (0-based internally).[1]
In practice, the export table supports flexible symbol exposure: loaders or linkers traverse the ENT to find a name, use the parallel Ordinal Table to get the index, and then retrieve the address from the EAT.[1] Forwarding enables modular design, such as a DLL re-exporting a function from the Windows kernel (e.g., forwarding HeapAlloc to ntdll.dll), which is indicated by EAT entries containing forwarder RVAs rather than direct function addresses.[1] The TimeDateStamp and version fields, while not enforcing strict semantics, help tools and loaders detect mismatches in linked modules during development or debugging.[1] No specific flag bits are defined in the Characteristics field, maintaining simplicity in the format.[1]
Loading and Security Features
Relocation Table
The base relocation table in the Portable Executable (PE) format enables the Windows loader to adjust absolute addresses within an executable image when it is loaded at a virtual address different from the preferred ImageBase specified in the optional header. This table is essential for supporting position-independent loading, allowing the image to be relocated dynamically without requiring recompilation. It resides in the.reloc section of the PE file and is referenced by the fifth entry (index 5) in the optional header's data directory array, which provides the relative virtual address (RVA) and size of the table.[9][14]
The table consists of one or more contiguous blocks, each described by an IMAGE_BASE_RELOCATION structure aligned on a 32-bit boundary. This structure includes a 4-byte VirtualAddress field, which holds the RVA of the 4 KB page (4096-byte boundary) containing the addresses to be relocated, and a 4-byte SizeOfBlock field, indicating the total size of the block in bytes, including the header and all subsequent relocation entries. Following the header is an array of 16-bit (2-byte) entries packed into DWORDs (four entries per 32 bits), where each entry encodes a 4-bit relocation type in the high bits and a 12-bit offset in the low bits; the offset specifies the byte displacement from the page's VirtualAddress where the adjustment must be applied. The number of entries in a block is calculated as (SizeOfBlock - 8) / 2, excluding any padding entries of type IMAGE_REL_BASED_ABSOLUTE (value 0), which are skipped by the loader.[15][14]
Relocation types are architecture-specific and determine how the base adjustment is applied to the target field. For 32-bit PE32 images (e.g., x86), the primary type is IMAGE_REL_BASED_HIGHLOW (value 3), which adds the full 32-bit signed delta to an absolute address field, effectively combining high and low 16-bit adjustments. In 64-bit PE32+ images (e.g., x64), IMAGE_REL_BASED_DIR64 (value 10) is used instead, applying the 64-bit delta to the address. Other types include IMAGE_REL_BASED_HIGH (1) for the high 16 bits of the delta and IMAGE_REL_BASED_LOW (2) for the low 16 bits, while architecture-specific variants exist for architectures like ARM, such as IMAGE_REL_BASED_ARM_MOV32 (value 5) for adjusting MOV32 instructions. These types ensure compatibility across processors by tailoring the fixup to the instruction set's addressing modes.[16][14]
During loading, the Windows image loader computes the relocation delta as the difference between the actual load base address and the preferred ImageBase from the optional header. For each block, it iterates through the type/offset entries, computes the target virtual address as VirtualAddress + [offset](/page/Offset) + [delta](/page/Delta), and applies the type-specific adjustment directly to the memory at that location, typically modifying pointers, jump targets, or data references within the page. If the delta is zero (i.e., loaded at the preferred base), the table is ignored, optimizing for the common case. The table's size is variable and can be omitted or stripped in release builds where a fixed base address is assumed, but it is required for executables supporting dynamic relocation, such as those enabling address space layout randomization.[15][14]
This structure, as defined in the PECOFF specification, forms the basis for each relocation block, with the array extending until the block size is exhausted.[14]ctypedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; // Variable-length array of 16-bit entries } IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; // Variable-length array of 16-bit entries } IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
Address Space Layout Randomization (ASLR)
Address Space Layout Randomization (ASLR) is a security mitigation in the Portable Executable (PE) format that randomizes the loading addresses of executable images, libraries, stacks, heaps, and process environment block (PEB) structures to hinder memory corruption exploits, such as buffer overflows, by making return-to-libc or similar attacks less predictable.[17][18] This feature is opt-in for PE files, enabled by setting theIMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag (0x0040) in the DLL Characteristics field of the optional header, which signals the Windows loader to rebase the image dynamically at runtime.[19][20]
ASLR operates at varying levels depending on configuration: partial or "bottom-up" randomization applies entropy to memory allocations like stacks and heaps starting from lower addresses, providing limited but additional unpredictability even for non-opted-in images when system-wide settings are enabled.[21] Full ASLR, triggered by the /DYNAMICBASE linker flag during compilation, randomizes the preferred ImageBase address for the entire PE image and requires the presence of a base relocation table (.reloc section) to adjust absolute addresses, imports, and other fixups after applying the offset.[18] Without relocations, the image loads at its fixed base, rendering ASLR ineffective.
During loading, the Windows loader selects a random offset—typically aligned to 64 KB boundaries for 32-bit images and larger for 64-bit—and adds it to the ImageBase, updating the import address table (IAT) and applying relocations to ensure correct execution at the new location.[17] This process also randomizes stack and heap bases, as well as the PEB, which contains process metadata like loaded module lists, to obscure pointers exploitable by attackers.[17][22]
Enhancements introduced in Windows 8 and later include high-entropy ASLR for 64-bit processes, enabled via the /HIGHENTROPYVA linker flag, which expands randomization entropy to up to 28 bits by supporting larger address spaces and coarser alignments, such as 256 MB steps for image bases, significantly increasing the search space for exploits.[23] ASLR is fully compatible with Data Execution Prevention (DEP), another PE security feature that marks non-executable regions as non-writable, allowing layered defenses without conflict.[24] It complements other mitigations like Control Flow Guard (CFG) by randomizing targets, though CFG enforces additional validation.[25]
As of 2025, ASLR remains a standard feature in Windows 11 and later, with mandatory system-wide enforcement configurable via Exploit Protection settings in Windows Security, ensuring even legacy modules are rebased when possible.[26] Recent Windows SDK versions provide refined linker flags like /DYNAMICBASE:NO for opt-out in debugging scenarios, but the core PE format and ASLR mechanics have seen no fundamental changes.[21]
Limitations include incomplete coverage for statically linked executables or modules lacking relocation tables, which load at fixed addresses and reduce overall entropy; such cases can be identified and measured using legacy tools like the Enhanced Mitigation Experience Toolkit (EMET), though modern diagnostics rely on built-in PowerShell cmdlets or Exploit Protection audits.[27]