Back to course

How the Windows Loader Maps a PE

15 minadvancedModule 8: Loading & ExecutionContinues from pe-exceptions
15 min · Module 8: Loading & Execution · Runtime

Why This Matters

Every file format you have studied so far is inert. The loader is where it stops being inert: the component that reads those headers, allocates memory, copies bytes, patches addresses, and transfers control. Understand it and you understand why ASLR exists, why an import is a memory write, why a signature is checked but never executed, and what malware actually manipulates when it injects or hollows a process.

You also stop being surprised by load failures. A relocation stripped image cannot move. A certificate is not a section. A section that ends at 0x8030 still occupies pages up to 0x9000. None of that is arbitrary — it falls out of fields you can read.

A Simple Model

What follows is a model of the documented contract. The PE specification defines the data the loader consumes; it does not publish the operating system algorithm, and kernel internals are out of scope here. Each step below is anchored to a spec rule or a documented API — treat the sequence as the shape of the contract, not source code.

  1. Probe and parse. Check MZ, follow e_lfanew, check the PE signature, read the COFF and optional headers (magic 0x20B for PE32+).
  2. Choose a base. ImageBase is the preferred address. If it is free, use it. If not and base relocations exist, load elsewhere and fix up pointers; DllCharacteristics DYNAMIC_BASE advertises that the image can be relocated at load time.
  3. Map the headers. SizeOfHeaders — DOS stub plus PE header plus section headers, rounded to FileAlignment — is mapped first.
  4. Map the sections. Each section lands at base plus its VirtualAddress, zero-filling any tail past SizeOfRawData.
  5. Apply relocations if the chosen base differs from ImageBase.
  6. Resolve imports. The import address table entries are overwritten with the actual addresses of the imported symbols — the spec says the loader typically processes the binding.
  7. Start TLS. Write the index, seed each thread from the template, invoke registered callbacks.
  8. Apply protections from section characteristics and DllCharacteristics flags such as NX_COMPAT.
  9. Transfer control to AddressOfEntryPoint — for program images, the spec calls this the starting address. For DLLs the linker default entry is the CRT wrapper _DllMainCRTStartup, which calls DllMain (Microsoft, "/ENTRY").
Common mistake. Thinking the certificate is mapped like a section. The spec is explicit: attribute certificates and debug information must sit at the very end of the file because the loader does not map these into memory. Signature data is read from the file, never executed from the image.

The Actual Detail

The spec rules that pin the model down:

RuleSpec text (condensed)
ImageBasePreferred address of the first byte when loaded; must be a multiple of 64 K
SectionAlignmentAlignment of sections in memory; at least FileAlignment; default is the page size
FileAlignmentPower of 2 between 512 and 64 K; default 512
SizeOfImageSize of the image including all headers; must be a multiple of SectionAlignment
Section placementSection VAs are assigned in ascending order and adjacent, each a multiple of SectionAlignment
Zero fillIf SizeOfRawData is less than VirtualSize, the remainder is padded with zeros
Relocations strippedIMAGE_FILE_RELOCS_STRIPPED: no base relocations; must load at the preferred base or the loader reports an error
Certificate tableThe virtual address in that data directory is a file offset to the first certificate entry

Two verified files, same rules, different choices:

Fieldcli-64.execonpty.dll
ImageBase0x1400000000x180000000
SectionAlignment / FileAlignment0x1000 / 0x2000x1000 / 0x200
SizeOfHeaders / SizeOfImage0x400 / 0x90000x400 / 0x1D000
COFF Characteristics0x0022 (executable, large-address-aware)0x2022 (plus DLL)
DllCharacteristics0x8160 (HIGH_ENTROPY_VA, DYNAMIC_BASE, NX_COMPAT, TERMINAL_SERVER_AWARE)0x4160 (same, with GUARD_CF instead of TERMINAL_SERVER_AWARE)
Subsystem3 (console)2 (Windows GUI)

The flag values come straight from the spec table: HIGH_ENTROPY_VA is 0x0020, DYNAMIC_BASE 0x0040, NX_COMPAT 0x0100, GUARD_CF 0x4000, TERMINAL_SERVER_AWARE 0x8000. Mask and add — 0x8160 and 0x4160 decompose exactly as the table says.

The certificate directory proves the file-offset rule. In conpty.dll, data directory index 4 holds the pair 0x18600, 0x3D20. That is not an RVA: naive RVA conversion would map 0x18600 into .data and show you the wrong bytes. Read it as a file offset instead, and 0x18600 + 0x3D20 equals 0x1C320 — the exact file size, one quadword-aligned WIN_CERTIFICATE entry covering the Authenticode signature. The first bytes confirm the structure: dwLength 0x00003D20, wRevision 0x0200, wCertificateType 0x0002, which the spec defines as WIN_CERT_TYPE_PKCS_SIGNED_DATA.

00018600: 203d 0000 0002 0200 3082 3d10 0609 2a86
00018610: 4886 f70d 0107 02a0 823d 0130 823c fd02

A Real Example

Run the model against cli-64.exe and watch the numbers close:

  1. Headers occupy 0x400 bytes (SizeOfHeaders, rounded to FileAlignment 0x200) at the base, so the first section starts at file offset 0x400 and RVA 0x1000.
  2. Six sections, all ascending: .text 0x1000, .rdata 0x3000, .data 0x5000, .pdata 0x6000, .rsrc 0x7000, .reloc 0x8000 — each exactly a multiple of SectionAlignment.
  3. The last section ends at 0x8000 + 0x30 = 0x8030. SizeOfImage must be a multiple of 0x1000, so the image rounds up to 0x9000 — the remaining 0xFD0 bytes complete the final page.
  4. Relocations are present (.reloc at index 5, RVA 0x8000, and DYNAMIC_BASE set), so this image is free to load away from 0x140000000.
  5. Imports are resolved into the IAT at RVA 0x3000 before the entry point runs — that is how the entry we will disassemble in the next lesson can call GetSystemTimeAsFileTime by name at build time and by address at run time.

Same arithmetic for conpty.dll: its sections end at 0x1C000 + 0x280 = 0x1C280, and SizeOfImage reads 0x1D000 — the next SectionAlignment boundary.

Try It

objdump reports the format, flags, and the computed start address (image base plus entry RVA):

$ objdump -f cli-64.exe
file format pei-x86-64
architecture: i386:x86-64, flags 0x0000012f:
HAS_RELOC, EXEC_P, HAS_LINENO, HAS_DEBUG, HAS_LOCALS, D_PAGED
start address 0x0000000140001d40

HAS_RELOC confirms step 2 can rebase, and the start address is ImageBase 0x140000000 plus AddressOfEntryPoint 0x1D40. On Windows, dumpbin /headers prints the same optional-header fields in one place.

What to look for: whenever start address minus ImageBase does not equal AddressOfEntryPoint, you have misread a header — that single subtraction validates two fields at once.

Check Your Understanding

Without looking back: a PE file carries IMAGE_FILE_RELOCS_STRIPPED and its preferred ImageBase is already taken. What happens?

Apply It

A tools reads data directory index 4 of conpty.dll as RVA 0x18600 and converts it through the section table. What went wrong?

The pattern: every data directory is an RVA except the certificate table, which is a file offset the loader never maps.

Connect

The loader has mapped the image. Now that you know the rules, you can picture the result: one contiguous region from ImageBase to ImageBase plus SizeOfImage, with the stack and heap growing beside it.

Next: Process Memory Layout — ImageBase, sections in memory, heap, and stack.