Back to course

Process Memory Layout

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

Why This Matters

A crash dump shows you addresses like 0x1400061A4. A hex editor shows file offsets like 0x3254. Debuggers, exploit writeups, and disassemblers speak in three dialects — file offset, RVA, and VA — and the memory layout is where they all meet. Get the layout right in your head and every address in every tool becomes the same number seen from a different window.

It also explains physical behavior: why .reloc can be thrown away after loading (it is marked discardable), why writing to .text faults (NX protection from section flags), and why your stack behaves like a reserve-plus-commit region rather than one giant allocation.

A Simple Model

Picture one process address space with three pieces:

  1. The image — one contiguous block from ImageBase up to ImageBase plus SizeOfImage. Headers first, then every section at its VirtualAddress, each padded out to SectionAlignment.
  2. The stack — a region sized by SizeOfStackReserve, of which only SizeOfStackCommit is committed up front; the rest appears one page at a time as the stack grows.
  3. The heap — sized by SizeOfHeapReserve with SizeOfHeapCommit handed out incrementally, the same pattern.

The model's missing piece: sections are adjacent in the section table but leave small gaps in memory. Those gaps are not wasted — they are the rounding to SectionAlignment, and they are why the last section can end at 0x8030 while the image still occupies a full page more.

The Actual Detail

The spec rules that build the map:

FieldSpec rule
ImageBasePreferred address of the first byte when loaded; must be a multiple of 64 K
Section placementSection VAs in ascending order and adjacent, each a multiple of SectionAlignment
SizeOfImageSize of the image including all headers; must be a multiple of SectionAlignment
SizeOfStackReserve / CommitOnly the commit size is committed; the rest of the reserve is made available one page at a time
SizeOfHeapReserve / CommitSame reserve-versus-commit pattern for the local heap

And the conversion that ties every tool together: VA = ImageBase + RVA, always. File offset is the odd one out — it comes from the section table, per section, and no single formula covers it.

Section characteristics become memory protection. Our samples decode straight from the spec flag table: .text reads 0x60000020 — IMAGE_SCN_CNT_CODE (0x20), MEM_EXECUTE (0x20000000), MEM_READ (0x40000000). .data reads 0xC0000040 — initialized data, read, and write. .reloc reads 0x42000040 — initialized data, read, and MEM_DISCARDABLE (0x02000000), so the loader may drop it once relocations are done.

Common mistake. Treating the file offset as if it were an RVA plus a constant. Every section has its own PointerToRawData versus VirtualAddress delta: in cli-64.exe that delta is 0xC00 for .text but 0x3C00 for .rsrc. One equation never converts across sections.

A Real Example

The loaded image of cli-64.exe, base 0x140000000 (computed as base plus each VirtualAddress):

VASectionEnds atHolds
0x140000000headers0x140000400DOS stub, PE headers, section table (SizeOfHeaders 0x400)
0x140001000.text0x1400027BCCode, including the entry at 0x140001D40
0x140003000.rdata0x14000432CImports, load config, read-only constants
0x140005000.data0x140005648Writable globals, including the security cookie slot at 0x140005008
0x140006000.pdata0x1400061EC41 RUNTIME_FUNCTION entries
0x140007000.rsrc0x1400071E0The manifest resource tree
0x140008000.reloc0x140008030Base relocations, discardable
0x140009000(image end)—SizeOfImage 0x9000 rounds the last section up to a page boundary

Watch the gaps: .text runs out at 0x27BC and .rdata does not start until 0x3000 — 0x844 bytes of padding, exactly the rounding to SectionAlignment. The stack and heap of this process each reserve 0x100000 bytes and commit only 0x1000 up front, read from the optional header we decoded earlier.

conpty.dll shows the same shape at its own base, 0x180000000: .text ends at RVA 0x10674, .rdata waits at 0x11000, and the image closes at SizeOfImage 0x1D000 after .reloc ends at 0x1C280.

Try It

Convert RVAs to VAs yourself — the whole trick is one addition:

$ python3 -c "print(hex(0x140000000 + 0x6000))"
0x140006000

$ python3 -c "print(hex(0x140000000 + 0x1d40))"
0x140001d40

Then confirm the image end from the section table: add the last VirtualAddress and VirtualSize, round up to SectionAlignment, and compare against SizeOfImage. For our sample: 0x8000 plus 0x30 rounds to 0x9000 — and the header says 0x9000.

What to look for: the second line is the entry point of the whole program. Every address you will disassemble in the next lesson lives in this map.

Check Your Understanding

Without looking back: cli-64.exe loads at 0x140000000 and you see a fault at VA 0x140005008. What is its RVA, and which section is it in?

Apply It

The last section of a PE32+ image ends at RVA 0x8030, and SectionAlignment is 0x1000. What SizeOfImage does the spec require?

The pattern: memory sizes round to SectionAlignment, file sizes round to FileAlignment — never mix the two rulers.

Connect

The stage is set: image mapped, protections applied, stack and heap reserved. Only one address has not fired yet — AddressOfEntryPoint.

Next: The Startup Sequence — from entry point to CRT initialization to main.